├── .editorconfig ├── .github ├── CODEOWNERS ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── android.yml ├── .gitignore ├── .sign └── debug.keystore.jks ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── benchmark-rules.pro ├── build.gradle.kts ├── google-services.json ├── libs │ └── renderscript-toolkit.aar ├── lint-baseline.xml ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── kotlin │ └── io │ │ └── getstream │ │ └── android │ │ └── video │ │ └── chat │ │ └── compose │ │ ├── App.kt │ │ ├── DeeplinkingActivity.kt │ │ ├── DirectCallActivity.kt │ │ ├── IncomingCallActivity.kt │ │ ├── MainActivity.kt │ │ ├── data │ │ ├── repositories │ │ │ └── GoogleAccountRepository.kt │ │ └── services │ │ │ ├── google │ │ │ └── ListDirectoryPeopleResponse.kt │ │ │ └── stream │ │ │ ├── GetAuthDataResponse.kt │ │ │ └── StreamService.kt │ │ ├── di │ │ └── AppModule.kt │ │ ├── models │ │ └── GoogleAccount.kt │ │ ├── tooling │ │ ├── extensions │ │ │ ├── ContextExtensions.kt │ │ │ └── Utils.kt │ │ ├── ui │ │ │ ├── ExceptionTraceActivity.kt │ │ │ └── ExceptionTraceScreen.kt │ │ └── util │ │ │ └── StreamFlavors.kt │ │ ├── ui │ │ ├── DogfoodingNavHost.kt │ │ ├── call │ │ │ ├── AvailableDeviceMenu.kt │ │ │ ├── CallActivity.kt │ │ │ ├── CallScreen.kt │ │ │ ├── CallStats.kt │ │ │ ├── ChatDialog.kt │ │ │ ├── ChatOverly.kt │ │ │ ├── CustomReactionContent.kt │ │ │ ├── FeedbackDialog.kt │ │ │ ├── LandscapeControls.kt │ │ │ ├── LayoutChooser.kt │ │ │ ├── ParticipantsDialog.kt │ │ │ ├── ReactionsMenu.kt │ │ │ └── ShareCall.kt │ │ ├── join │ │ │ ├── CallJoinScreen.kt │ │ │ ├── CallJoinViewModel.kt │ │ │ └── barcode │ │ │ │ └── BardcodeScanner.kt │ │ ├── lobby │ │ │ ├── CallLobbyScreen.kt │ │ │ └── CallLobbyViewModel.kt │ │ ├── login │ │ │ ├── GoogleSignIn.kt │ │ │ ├── GoogleSignInLauncher.kt │ │ │ ├── LoginScreen.kt │ │ │ └── LoginViewModel.kt │ │ ├── menu │ │ │ ├── MenuDefinitions.kt │ │ │ ├── SettingsMenu.kt │ │ │ ├── VideoFiltersMenu.kt │ │ │ └── base │ │ │ │ ├── DynamicMenu.kt │ │ │ │ └── MenuTypes.kt │ │ └── outgoing │ │ │ ├── DirectCallJoinScreen.kt │ │ │ └── DirectCallJoinViewModel.kt │ │ └── util │ │ ├── FeedbackSender.kt │ │ ├── LockOrientation.kt │ │ ├── NetworkMonitor.kt │ │ ├── StreamVideoInitHelper.kt │ │ ├── UserHelper.kt │ │ ├── config │ │ ├── AppConfig.kt │ │ └── types │ │ │ └── StreamEnvironment.kt │ │ └── filters │ │ ├── SampleAudioFilter.kt │ │ └── SampleVideoFilter.kt │ └── res │ ├── drawable │ ├── amsterdam1.webp │ ├── amsterdam2.webp │ ├── boulder1.webp │ ├── boulder2.webp │ ├── feedback_artwork.png │ ├── google_button_logo.xml │ ├── gradient1.webp │ ├── ic_blur_off.xml │ ├── ic_blur_on.xml │ ├── ic_default_avatar.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_layout_grid.xml │ ├── ic_layout_spotlight.xml │ ├── ic_mic.xml │ ├── ic_scan_qr.xml │ ├── ic_stream_video_meeting_logo.xml │ └── stream_calls_logo.png │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── getstream │ └── android │ └── video │ └── chat │ └── compose │ └── Configuration.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── previews ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png └── readme.jpg ├── settings.gradle.kts └── spotless ├── copyright.kt ├── copyright.kts └── copyright.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | # Most of the standard properties are supported 4 | max_line_length=100 5 | 6 | # don't use wildcard for Java/Kotlin imports 7 | [*.{java,kt}] 8 | wildcard_import_limit = 999 9 | ij_kotlin_name_count_to_use_star_import = 999 10 | ij_kotlin_name_count_to_use_star_import_for_members = 999 -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # More details are here: https://help.github.com/articles/about-codeowners/ 5 | 6 | # The '*' pattern is global owners. 7 | # Not adding in this PR, but I'd like to try adding a global owner set with the entire team. 8 | # One interpretation of their docs is that global owners are added only if not removed 9 | # by a more local rule. 10 | 11 | # Order is important. The last matching pattern has the most precedence. 12 | # The folders are ordered as follows: 13 | 14 | # In each subsection folders are ordered first by depth, then alphabetically. 15 | # This should make it easy to add new rules without breaking existing ones. 16 | * @skydoves -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | # Allow up to 10 open pull requests for pip dependencies 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### 🎯 Goal 2 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 3 | 4 | ### 🛠 Implementation details 5 | Describe the implementation details for this Pull Request. 6 | 7 | ### ✍️ Explain examples 8 | Explain examples with code for this updates. 9 | 10 | ### Preparing a pull request for review 11 | Ensure your change is properly formatted by running: 12 | 13 | ```gradle 14 | $ ./gradlew spotlessApply 15 | ``` 16 | 17 | Please correct any failures before requesting a review. -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: set up JDK 16 | uses: actions/setup-java@v1 17 | with: 18 | distribution: adopt 19 | java-version: 17 20 | 21 | - name: Cache Gradle and wrapper 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.gradle/caches 26 | ~/.gradle/wrapper 27 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} 28 | restore-keys: | 29 | ${{ runner.os }}-gradle- 30 | - name: Make Gradle executable 31 | run: chmod +x ./gradlew 32 | 33 | - name: Build with Gradle 34 | run: ./gradlew build 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .composite 3 | buildSrc/build 4 | .gradle 5 | /local.properties 6 | .env.properties 7 | 8 | # Built application files 9 | *.apk 10 | *.aar 11 | *.ap_ 12 | *.aab 13 | *.exec 14 | 15 | library/.env 16 | 17 | # Files for the ART/Dalvik VM 18 | *.dex 19 | 20 | # Java class files 21 | *.class 22 | 23 | # Generated files 24 | bin/ 25 | gen/ 26 | out/ 27 | # Uncomment the following line in case you need and you don't have the release build type files in your app 28 | # release/ 29 | 30 | # Gradle files 31 | /.idea 32 | .gradle/ 33 | build/ 34 | 35 | # Local configuration file (sdk path, etc) 36 | local.properties 37 | 38 | # Proguard folder generated by Eclipse 39 | proguard/ 40 | 41 | # Log Files 42 | *.log 43 | 44 | # Android Studio Navigation editor temp files 45 | .navigation/ 46 | 47 | # Android Studio captures folder 48 | captures/ 49 | 50 | # Keystore files 51 | # Uncomment the following lines if you do not want to check your keystore files in. 52 | #*.jks 53 | #*.keystore 54 | 55 | # External native build folder generated in Android Studio 2.2 and later 56 | .externalNativeBuild 57 | .cxx/ 58 | 59 | # Google Services (e.g. APIs or Firebase) 60 | # google-services.json 61 | 62 | # Freeline 63 | freeline.py 64 | freeline/ 65 | freeline_project_description.json 66 | 67 | # fastlane 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots 71 | fastlane/test_output 72 | fastlane/readme.md 73 | 74 | # Version control 75 | vcs.xml 76 | .gitignore.swp 77 | 78 | # lint 79 | lint/intermediates/ 80 | lint/generated/ 81 | lint/outputs/ 82 | lint/tmp/ 83 | # lint/reports/ 84 | 85 | /.idea/* 86 | !/.idea/codeInsightSettings.xml 87 | !/.idea/codeStyles/ 88 | !/.idea/inspectionProfiles/ 89 | !/.idea/scopes/ 90 | .DS_Store 91 | /build 92 | /captures 93 | /attachments/* 94 | .cxx 95 | /projectFilesBackup/ 96 | /projectFilesBackup1/ 97 | *.gpg 98 | 99 | # Ignore Dokka files into docusaurus project 100 | docusaurus/docs/Android/Dokka 101 | # Ignore Algolia credentials 102 | docusaurus/.env 103 | 104 | # Ignore keysotore files 105 | .sign/keystore.properties 106 | .sign/release.keystore 107 | 108 | # Ignore Google Play Publisher files 109 | .sign/service-account-credentials.json 110 | demo-app/src/production/play/ 111 | 112 | # Environment Variables 113 | .env.properties 114 | -------------------------------------------------------------------------------- /.sign/debug.keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/.sign/debug.keystore.jks -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | GetStream. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. 3 | 4 | ## Preparing a pull request for review 5 | Ensure your change is properly formatted by running: 6 | 7 | ```gradle 8 | ./gradlew spotlessApply 9 | ``` 10 | 11 | Please correct any failures before requesting a review. 12 | 13 | ## Code reviews 14 | All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) for more information on using pull requests. 15 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | !/libs/** -------------------------------------------------------------------------------- /app/benchmark-rules.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate 2 | -dontwarn com.google.errorprone.annotations.InlineMe 3 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @file:Suppress("UnstableApiUsage") 17 | 18 | import io.getstream.android.video.chat.compose.Configuration 19 | import java.io.FileInputStream 20 | import java.util.* 21 | 22 | @Suppress("DSL_SCOPE_VIOLATION") 23 | plugins { 24 | id(libs.plugins.android.application.get().pluginId) 25 | id(libs.plugins.kotlin.android.get().pluginId) 26 | id(libs.plugins.firebase.crashlytics.get().pluginId) 27 | id(libs.plugins.kotlin.serialization.get().pluginId) 28 | id(libs.plugins.compose.compiler.get().pluginId) 29 | id(libs.plugins.hilt.get().pluginId) 30 | id(libs.plugins.ksp.get().pluginId) 31 | id(libs.plugins.spotless.get().pluginId) 32 | id(libs.plugins.baseline.profile.get().pluginId) 33 | id("com.google.gms.google-services") 34 | } 35 | 36 | android { 37 | namespace = "io.getstream.android.video.chat.compose" 38 | compileSdk = Configuration.compileSdk 39 | 40 | defaultConfig { 41 | applicationId = "io.getstream.android.video.chat.compose" 42 | minSdk = Configuration.minSdk 43 | targetSdk = Configuration.targetSdk 44 | versionCode = Configuration.versionCode 45 | versionName = Configuration.versionName 46 | vectorDrawables { 47 | useSupportLibrary = true 48 | } 49 | } 50 | 51 | compileOptions { 52 | sourceCompatibility = JavaVersion.VERSION_17 53 | targetCompatibility = JavaVersion.VERSION_17 54 | } 55 | 56 | kotlinOptions { 57 | jvmTarget = "17" 58 | } 59 | 60 | composeCompiler { 61 | enableStrongSkippingMode = true 62 | } 63 | 64 | val signFile: File = rootProject.file(".sign/keystore.properties") 65 | if (signFile.exists()) { 66 | val properties = Properties() 67 | properties.load(FileInputStream(signFile)) 68 | 69 | signingConfigs { 70 | create("release") { 71 | keyAlias = properties["keyAlias"] as? String 72 | keyPassword = properties["keyPassword"] as? String 73 | storeFile = rootProject.file(properties["keystore"] as String) 74 | storePassword = properties["storePassword"] as? String 75 | } 76 | } 77 | } else { 78 | signingConfigs { 79 | create("release") { 80 | keyAlias = "androiddebugkey" 81 | keyPassword = "android" 82 | storeFile = rootProject.file(".sign/debug.keystore.jks") 83 | storePassword = "android" 84 | } 85 | } 86 | } 87 | 88 | signingConfigs { 89 | getByName("debug") { 90 | keyAlias = "androiddebugkey" 91 | keyPassword = "android" 92 | storeFile = rootProject.file(".sign/debug.keystore.jks") 93 | storePassword = "android" 94 | } 95 | } 96 | 97 | buildTypes { 98 | getByName("debug") { 99 | versionNameSuffix = "-DEBUG" 100 | applicationIdSuffix = ".debug" 101 | isDebuggable = true 102 | isMinifyEnabled = false 103 | isShrinkResources = false 104 | signingConfig = signingConfigs.getByName("debug") 105 | } 106 | getByName("release") { 107 | isMinifyEnabled = true 108 | proguardFiles( 109 | getDefaultProguardFile("proguard-android-optimize.txt"), 110 | "proguard-rules.pro" 111 | ) 112 | signingConfig = signingConfigs.getByName("release") 113 | } 114 | create("benchmark") { 115 | isDebuggable = true 116 | isMinifyEnabled = false 117 | isShrinkResources = false 118 | signingConfig = signingConfigs.getByName("debug") 119 | matchingFallbacks += listOf("release") 120 | proguardFiles("benchmark-rules.pro") 121 | } 122 | } 123 | 124 | flavorDimensions += "environment" 125 | productFlavors { 126 | create("development") { 127 | dimension = "environment" 128 | applicationIdSuffix = ".dogfooding" 129 | } 130 | create("production") { 131 | dimension = "environment" 132 | } 133 | } 134 | 135 | buildFeatures { 136 | resValues = true 137 | buildConfig = true 138 | } 139 | 140 | packaging { 141 | jniLibs.pickFirsts.add("lib/*/librenderscript-toolkit.so") 142 | } 143 | 144 | lint { 145 | abortOnError = false 146 | } 147 | 148 | baselineProfile { 149 | mergeIntoMain = true 150 | } 151 | } 152 | 153 | dependencies { 154 | // Stream Video SDK 155 | implementation(libs.stream.video.compose) 156 | implementation(libs.stream.video.filter) 157 | implementation(libs.stream.video.previewdata) 158 | 159 | // Stream Chat SDK 160 | implementation(libs.stream.chat.compose) 161 | implementation(libs.stream.chat.offline) 162 | implementation(libs.stream.chat.state) 163 | implementation(libs.stream.chat.ui.utils) 164 | 165 | implementation(libs.stream.push.firebase) 166 | implementation(libs.stream.log.android) 167 | 168 | implementation(libs.androidx.material) 169 | implementation(libs.androidx.core.ktx) 170 | implementation(libs.androidx.lifecycle.runtime) 171 | 172 | // Network 173 | implementation(libs.okhttp) 174 | implementation(libs.retrofit) 175 | implementation(libs.kotlinx.coroutines.android) 176 | implementation(libs.kotlinx.serialization.json) 177 | implementation(libs.kotlinx.serialization.converter) 178 | 179 | // Compose 180 | implementation(platform(libs.androidx.compose.bom)) 181 | implementation(libs.androidx.activity.compose) 182 | implementation(libs.androidx.compose.ui) 183 | implementation(libs.androidx.compose.ui.tooling) 184 | implementation(libs.androidx.compose.runtime) 185 | implementation(libs.androidx.compose.navigation) 186 | implementation(libs.androidx.compose.foundation) 187 | implementation(libs.androidx.compose.material) 188 | implementation(libs.androidx.compose.material.iconsExtended) 189 | implementation(libs.androidx.hilt.navigation) 190 | implementation(libs.androidx.lifecycle.runtime.compose) 191 | implementation(libs.accompanist.permission) 192 | implementation(libs.landscapist.coil) 193 | 194 | // QR code scanning 195 | implementation(libs.androidx.camera.core) 196 | implementation(libs.play.services.mlkit.barcode.scanning) 197 | implementation(libs.androidx.camera.view) 198 | implementation(libs.androidx.camera.lifecycle) 199 | implementation(libs.androidx.camera.camera2) 200 | 201 | // Hilt 202 | implementation(libs.hilt.android) 203 | ksp(libs.hilt.compiler) 204 | 205 | // Firebase 206 | implementation(platform(libs.firebase.bom)) 207 | implementation(libs.firebase.crashlytics) 208 | implementation(libs.firebase.config) 209 | implementation(libs.firebase.analytics) 210 | 211 | // Moshi 212 | implementation(libs.moshi.kotlin) 213 | 214 | // Video Filters 215 | implementation(libs.google.mlkit.selfie.segmentation) 216 | implementation(files("libs/renderscript-toolkit.aar")) 217 | 218 | // Play 219 | implementation(libs.play.auth) 220 | } -------------------------------------------------------------------------------- /app/libs/renderscript-toolkit.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/libs/renderscript-toolkit.aar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 37 | 38 | 41 | 42 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 85 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 119 | 120 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/App.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose 18 | 19 | import android.app.Application 20 | import android.content.Context 21 | import dagger.hilt.android.HiltAndroidApp 22 | import io.getstream.android.video.chat.compose.util.StreamVideoInitHelper 23 | import io.getstream.video.android.datastore.delegate.StreamUserDataStore 24 | import kotlinx.coroutines.runBlocking 25 | 26 | @HiltAndroidApp 27 | class App : Application() { 28 | 29 | override fun onCreate() { 30 | super.onCreate() 31 | 32 | // We use the provided StreamUserDataStore in the demo app for user data storage. 33 | // This is a convenience class provided for storage but the SDK itself is not aware of 34 | // this instance and doesn't use it. You can use it to store the logged in user and then 35 | // retrieve the information for SDK initialisation. 36 | StreamUserDataStore.install(this, isEncrypted = true) 37 | 38 | // Demo helper for initialising the Video and Chat SDK instances from one place. 39 | // For simpler code we "inject" the Context manually instead of using DI. 40 | StreamVideoInitHelper.init(this) 41 | 42 | // Prepare the Video SDK if we already have a user logged in the demo app. 43 | // If you need to receive push messages (incoming call) then the SDK must be initialised 44 | // in Application.onCreate. Otherwise it doesn't know how to init itself when push arrives 45 | // and will ignore the push messages. 46 | // If push messages are not used then you don't need to init here - you can init 47 | // on-demand (initialising here is usually less error-prone). 48 | runBlocking { 49 | StreamVideoInitHelper.loadSdk( 50 | dataStore = StreamUserDataStore.instance(), 51 | useRandomUserAsFallback = false, 52 | ) 53 | } 54 | } 55 | } 56 | 57 | val Context.app get() = applicationContext as App 58 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose 18 | 19 | import android.os.Bundle 20 | import androidx.activity.ComponentActivity 21 | import androidx.activity.compose.setContent 22 | import androidx.lifecycle.lifecycleScope 23 | import dagger.hilt.android.AndroidEntryPoint 24 | import io.getstream.android.video.chat.compose.ui.AppNavHost 25 | import io.getstream.android.video.chat.compose.ui.AppScreens 26 | import io.getstream.video.android.compose.theme.VideoTheme 27 | import io.getstream.video.android.datastore.delegate.StreamUserDataStore 28 | import kotlinx.coroutines.flow.firstOrNull 29 | import kotlinx.coroutines.launch 30 | import javax.inject.Inject 31 | 32 | @AndroidEntryPoint 33 | class MainActivity : ComponentActivity() { 34 | @Inject 35 | lateinit var dataStore: StreamUserDataStore 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | 40 | lifecycleScope.launch { 41 | val isLoggedIn = dataStore.user.firstOrNull() != null 42 | 43 | setContent { 44 | VideoTheme { 45 | AppNavHost( 46 | startDestination = if (!isLoggedIn) { 47 | AppScreens.Login.routeWithArg(true) // Pass true for autoLogIn 48 | } else { 49 | AppScreens.CallJoin.route 50 | }, 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/data/repositories/GoogleAccountRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.data.repositories 18 | 19 | import android.content.Context 20 | import android.util.Log 21 | import com.google.android.gms.auth.GoogleAuthUtil 22 | import com.google.android.gms.auth.api.signin.GoogleSignIn 23 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount 24 | import com.google.android.gms.auth.api.signin.GoogleSignInClient 25 | import com.squareup.moshi.JsonAdapter 26 | import com.squareup.moshi.Moshi 27 | import com.squareup.moshi.adapter 28 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 29 | import dagger.hilt.android.qualifiers.ApplicationContext 30 | import io.getstream.android.video.chat.compose.data.services.google.ListDirectoryPeopleResponse 31 | import io.getstream.android.video.chat.compose.data.services.google.asDomainModel 32 | import io.getstream.android.video.chat.compose.models.GoogleAccount 33 | import kotlinx.coroutines.Dispatchers 34 | import kotlinx.coroutines.tasks.await 35 | import kotlinx.coroutines.withContext 36 | import okhttp3.HttpUrl.Companion.toHttpUrl 37 | import okhttp3.OkHttpClient 38 | import okhttp3.Request 39 | import javax.inject.Inject 40 | 41 | class GoogleAccountRepository @Inject constructor( 42 | @ApplicationContext private val context: Context, 43 | private val googleSignInClient: GoogleSignInClient, 44 | ) { 45 | private val baseUrl = "https://people.googleapis.com/v1/people:listDirectoryPeople" 46 | 47 | suspend fun getAllAccounts(): List? { 48 | val readMask = "readMask=emailAddresses,names,photos" 49 | val sources = "sources=DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE" 50 | val pageSize = "pageSize=1000" 51 | 52 | return if (signInSilently()) { 53 | GoogleSignIn.getLastSignedInAccount(context)?.let { account -> 54 | withContext(Dispatchers.IO) { 55 | getAccessToken(account)?.let { accessToken -> 56 | val urlString = "$baseUrl?access_token=$accessToken&$readMask&$sources&$pageSize" 57 | val request = buildRequest(urlString) 58 | val okHttpClient = buildOkHttpClient() 59 | var responseBody: String? 60 | 61 | okHttpClient.newCall(request).execute().let { response -> 62 | if (response.isSuccessful) { 63 | responseBody = response.body?.string() 64 | responseBody?.let { body -> 65 | parseUserListJson(body)?.sortedBy { user -> user.name } 66 | } 67 | } else { 68 | null 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } else { 75 | null 76 | } 77 | } 78 | 79 | private suspend fun signInSilently(): Boolean { 80 | val task = googleSignInClient.silentSignIn() 81 | 82 | return if (task.isSuccessful) { 83 | Log.d("Google Silent Sign In", "Successful") 84 | true 85 | } else { 86 | try { 87 | task.await() 88 | Log.d("Google Silent Sign In", "Successful after await") 89 | true 90 | } catch (exception: Exception) { 91 | Log.d("Google Silent Sign In", "Failed") 92 | false 93 | } 94 | } 95 | } 96 | 97 | private fun getAccessToken(account: GoogleSignInAccount) = 98 | try { 99 | GoogleAuthUtil.getToken( 100 | context, 101 | account.account!!, 102 | "oauth2:profile email", 103 | ) 104 | } catch (e: Exception) { 105 | null 106 | } 107 | 108 | private fun buildRequest(urlString: String) = Request.Builder() 109 | .url(urlString.toHttpUrl()) 110 | .build() 111 | 112 | private fun buildOkHttpClient() = OkHttpClient.Builder() 113 | .retryOnConnectionFailure(true) 114 | .build() 115 | 116 | @OptIn(ExperimentalStdlibApi::class) 117 | private fun parseUserListJson(jsonString: String): List? { 118 | val moshi: Moshi = Moshi.Builder() 119 | .add(KotlinJsonAdapterFactory()) 120 | .build() 121 | val jsonAdapter: JsonAdapter = moshi.adapter() 122 | 123 | val response = jsonAdapter.fromJson(jsonString) 124 | return response?.people?.map { it.asDomainModel() } 125 | } 126 | 127 | fun getCurrentUser(): GoogleAccount { 128 | val currentUser = GoogleSignIn.getLastSignedInAccount(context) 129 | return GoogleAccount( 130 | email = currentUser?.email ?: "", 131 | id = currentUser?.id ?: "", 132 | name = currentUser?.displayName ?: "", 133 | photoUrl = currentUser?.photoUrl?.toString(), 134 | isFavorite = false, 135 | ) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/data/services/google/ListDirectoryPeopleResponse.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.data.services.google 18 | 19 | import io.getstream.android.video.chat.compose.models.GoogleAccount 20 | import io.getstream.android.video.chat.compose.util.UserHelper 21 | 22 | data class ListDirectoryPeopleResponse( 23 | val people: List, 24 | ) 25 | 26 | data class GoogleAccountDto( 27 | val photos: List?, 28 | val emailAddresses: List, 29 | ) 30 | 31 | data class PhotoDto( 32 | val url: String, 33 | ) 34 | 35 | data class EmailAddressDto( 36 | val value: String, 37 | ) 38 | 39 | fun GoogleAccountDto.asDomainModel(): GoogleAccount { 40 | val email = emailAddresses.firstOrNull()?.value 41 | 42 | return GoogleAccount( 43 | email = email, 44 | id = email?.let { UserHelper.getUserIdFromEmail(it) }, 45 | name = email?.let { UserHelper.getFullNameFromEmail(it) }, 46 | photoUrl = photos?.firstOrNull()?.url, 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/data/services/stream/GetAuthDataResponse.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.data.services.stream 18 | 19 | import kotlinx.serialization.Serializable 20 | 21 | @Serializable data class GetAuthDataResponse(val userId: String, val apiKey: String, val token: String) 22 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/data/services/stream/StreamService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.data.services.stream 18 | 19 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 20 | import kotlinx.serialization.json.Json 21 | import okhttp3.MediaType.Companion.toMediaType 22 | import retrofit2.Retrofit 23 | import retrofit2.create 24 | import retrofit2.http.GET 25 | import retrofit2.http.Query 26 | 27 | interface StreamService { 28 | @GET("api/auth/create-token") 29 | suspend fun getAuthData( 30 | @Query("environment") environment: String, 31 | @Query("user_id") userId: String?, 32 | ): GetAuthDataResponse 33 | 34 | companion object { 35 | private const val BASE_URL = "https://pronto.getstream.io/" 36 | 37 | private val json = Json { ignoreUnknownKeys = true } 38 | 39 | private val retrofit = Retrofit.Builder() 40 | .baseUrl(BASE_URL) 41 | .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) 42 | .build() 43 | 44 | val instance = retrofit.create() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.di 18 | 19 | import android.content.Context 20 | import com.google.android.gms.auth.api.signin.GoogleSignIn 21 | import com.google.android.gms.auth.api.signin.GoogleSignInClient 22 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions 23 | import com.google.android.gms.common.api.Scope 24 | import dagger.Provides 25 | import dagger.hilt.InstallIn 26 | import dagger.hilt.android.qualifiers.ApplicationContext 27 | import dagger.hilt.components.SingletonComponent 28 | import io.getstream.android.video.chat.compose.R 29 | import io.getstream.android.video.chat.compose.data.repositories.GoogleAccountRepository 30 | import io.getstream.android.video.chat.compose.util.NetworkMonitor 31 | import io.getstream.video.android.datastore.delegate.StreamUserDataStore 32 | import javax.inject.Singleton 33 | 34 | @dagger.Module 35 | @InstallIn(SingletonComponent::class) 36 | object AppModule { 37 | 38 | @Provides 39 | @Singleton 40 | fun provideUserDataStore(): StreamUserDataStore { 41 | return StreamUserDataStore.instance() 42 | } 43 | 44 | @Provides 45 | @Singleton 46 | fun provideGoogleSignInClient( 47 | @ApplicationContext context: Context, 48 | ): GoogleSignInClient = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) 49 | .requestEmail() 50 | .requestIdToken(context.getString(R.string.default_web_client_id)) 51 | .requestScopes(Scope("https://www.googleapis.com/auth/directory.readonly")) 52 | .build() 53 | .let { gso -> GoogleSignIn.getClient(context, gso) } 54 | 55 | @Provides 56 | fun provideGoogleAccountRepository( 57 | @ApplicationContext context: Context, 58 | googleSignInClient: GoogleSignInClient, 59 | ) = GoogleAccountRepository(context, googleSignInClient) 60 | 61 | @Provides 62 | @Singleton 63 | fun provideNetworkMonitor(@ApplicationContext context: Context) = 64 | NetworkMonitor(context) 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/models/GoogleAccount.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.models 18 | 19 | data class GoogleAccount( 20 | val email: String?, 21 | val id: String?, 22 | val name: String?, 23 | val photoUrl: String?, 24 | val isFavorite: Boolean = false, 25 | ) 26 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/tooling/extensions/ContextExtensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.tooling.extensions 18 | 19 | import android.content.Context 20 | import android.widget.Toast 21 | import androidx.annotation.StringRes 22 | 23 | @JvmSynthetic 24 | internal fun Context.toast(@StringRes message: Int) { 25 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/tooling/extensions/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.tooling.extensions 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.platform.LocalDensity 21 | import androidx.compose.ui.unit.Dp 22 | 23 | @Composable 24 | fun Dp.toPx(): Float { 25 | val density = LocalDensity.current.density 26 | return this.value * density 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/tooling/ui/ExceptionTraceActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.tooling.ui 18 | 19 | import android.content.Context 20 | import android.content.Intent 21 | import android.os.Bundle 22 | import androidx.activity.ComponentActivity 23 | import androidx.activity.compose.setContent 24 | import androidx.core.os.bundleOf 25 | import io.getstream.video.android.compose.theme.VideoTheme 26 | 27 | internal class ExceptionTraceActivity : ComponentActivity() { 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | 32 | val throwable = intent.getStringExtra(EXTRA_EXCEPTION) 33 | val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) 34 | if (throwable == null || packageName == null) { 35 | finish() 36 | } 37 | 38 | setContent { 39 | VideoTheme { ExceptionTraceScreen(packageName = packageName!!, message = throwable!!) } 40 | } 41 | } 42 | 43 | internal companion object { 44 | private const val EXTRA_EXCEPTION = "EXTRA_EXCEPTION" 45 | private const val EXTRA_MESSAGE = "EXTRA_MESSAGE" 46 | private const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME" 47 | 48 | fun getIntent(context: Context, exception: String, message: String, packageName: String) = 49 | Intent(context, ExceptionTraceActivity::class.java).apply { 50 | putExtras( 51 | bundleOf( 52 | EXTRA_EXCEPTION to exception, 53 | EXTRA_MESSAGE to message, 54 | EXTRA_PACKAGE_NAME to packageName, 55 | ), 56 | ) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/tooling/ui/ExceptionTraceScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.tooling.ui 18 | 19 | import android.content.Context 20 | import android.content.Intent 21 | import androidx.compose.foundation.BorderStroke 22 | import androidx.compose.foundation.background 23 | import androidx.compose.foundation.border 24 | import androidx.compose.foundation.clickable 25 | import androidx.compose.foundation.layout.Box 26 | import androidx.compose.foundation.layout.Column 27 | import androidx.compose.foundation.layout.Spacer 28 | import androidx.compose.foundation.layout.fillMaxWidth 29 | import androidx.compose.foundation.layout.height 30 | import androidx.compose.foundation.layout.padding 31 | import androidx.compose.foundation.rememberScrollState 32 | import androidx.compose.foundation.shape.RoundedCornerShape 33 | import androidx.compose.foundation.verticalScroll 34 | import androidx.compose.material.Icon 35 | import androidx.compose.material.Text 36 | import androidx.compose.material.icons.Icons 37 | import androidx.compose.material.icons.filled.ContentCopy 38 | import androidx.compose.runtime.Composable 39 | import androidx.compose.ui.Alignment 40 | import androidx.compose.ui.Modifier 41 | import androidx.compose.ui.platform.ClipboardManager 42 | import androidx.compose.ui.platform.LocalClipboardManager 43 | import androidx.compose.ui.platform.LocalContext 44 | import androidx.compose.ui.res.stringResource 45 | import androidx.compose.ui.text.AnnotatedString 46 | import androidx.compose.ui.text.font.FontWeight 47 | import androidx.compose.ui.unit.dp 48 | import androidx.compose.ui.unit.sp 49 | import io.getstream.android.video.chat.compose.R 50 | import io.getstream.android.video.chat.compose.tooling.extensions.toast 51 | import io.getstream.video.android.compose.theme.VideoTheme 52 | import io.getstream.video.android.compose.ui.components.base.StreamButton 53 | 54 | @Composable 55 | internal fun ExceptionTraceScreen(packageName: String, message: String) { 56 | val scrollState = rememberScrollState() 57 | Column( 58 | modifier = 59 | Modifier 60 | .verticalScroll(scrollState) 61 | .background(VideoTheme.colors.baseSheetPrimary) 62 | .padding(16.dp), 63 | ) { 64 | val context: Context = LocalContext.current 65 | StreamButton( 66 | style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), 67 | text = stringResource(id = R.string.stream_video_tooling_restart_app), 68 | onClick = { 69 | val mainActivity = Class.forName(packageName) 70 | context.startActivity(Intent(context, mainActivity)) 71 | }, 72 | ) 73 | 74 | Box(modifier = Modifier.fillMaxWidth()) { 75 | Text( 76 | modifier = Modifier.align(Alignment.CenterStart), 77 | text = stringResource(id = R.string.stream_video_tooling_exception_log), 78 | color = VideoTheme.colors.basePrimary, 79 | fontWeight = FontWeight.Bold, 80 | fontSize = 16.sp, 81 | ) 82 | 83 | val clipboardManager: ClipboardManager = LocalClipboardManager.current 84 | Icon( 85 | modifier = 86 | Modifier 87 | .align(Alignment.CenterEnd) 88 | .clickable { 89 | clipboardManager.setText(AnnotatedString(message)) 90 | context.toast(R.string.stream_video_tooling_copy_into_clipboard) 91 | }, 92 | imageVector = Icons.Filled.ContentCopy, 93 | tint = VideoTheme.colors.basePrimary, 94 | contentDescription = null, 95 | ) 96 | } 97 | 98 | Spacer(modifier = Modifier.height(12.dp)) 99 | 100 | Text( 101 | modifier = 102 | Modifier 103 | .border( 104 | border = BorderStroke(2.dp, VideoTheme.colors.brandPrimary), 105 | shape = RoundedCornerShape(6.dp), 106 | ) 107 | .padding(12.dp), 108 | text = message, 109 | color = VideoTheme.colors.basePrimary, 110 | fontSize = 14.sp, 111 | ) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/tooling/util/StreamFlavors.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.tooling.util 18 | 19 | /** 20 | * Defined flavors. Used in code logic. 21 | */ 22 | internal object StreamFlavors { 23 | const val development = "development" 24 | const val production = "production" 25 | } 26 | 27 | public object StreamEnvironments { 28 | const val demo = "demo" 29 | const val pronto = "pronto" 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/DogfoodingNavHost.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui 18 | 19 | import androidx.compose.foundation.layout.fillMaxSize 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.navigation.NavHostController 24 | import androidx.navigation.NavType 25 | import androidx.navigation.compose.NavHost 26 | import androidx.navigation.compose.composable 27 | import androidx.navigation.compose.rememberNavController 28 | import androidx.navigation.navArgument 29 | import io.getstream.android.video.chat.compose.DirectCallActivity 30 | import io.getstream.android.video.chat.compose.ui.join.CallJoinScreen 31 | import io.getstream.android.video.chat.compose.ui.join.barcode.BarcodeScanner 32 | import io.getstream.android.video.chat.compose.ui.lobby.CallLobbyScreen 33 | import io.getstream.android.video.chat.compose.ui.login.LoginScreen 34 | import io.getstream.android.video.chat.compose.ui.outgoing.DirectCallJoinScreen 35 | 36 | @Composable 37 | fun AppNavHost( 38 | modifier: Modifier = Modifier, 39 | navController: NavHostController = rememberNavController(), 40 | startDestination: String = AppScreens.Login.route, 41 | ) { 42 | NavHost( 43 | modifier = modifier.fillMaxSize(), 44 | navController = navController, 45 | startDestination = startDestination, 46 | ) { 47 | composable(AppScreens.Login.route) { backStackEntry -> 48 | LoginScreen( 49 | autoLogIn = backStackEntry.arguments?.getString("auto_log_in")?.let { it.toBoolean() } ?: true, 50 | navigateToCallJoin = { 51 | navController.navigate(AppScreens.CallJoin.route) { 52 | popUpTo(AppScreens.Login.route) { inclusive = true } 53 | } 54 | }, 55 | ) 56 | } 57 | composable(AppScreens.CallJoin.route) { 58 | CallJoinScreen( 59 | navigateToCallLobby = { cid -> 60 | navController.navigate(AppScreens.CallLobby.routeWithArg(cid)) 61 | }, 62 | navigateUpToLogin = { autoLogIn -> 63 | navController.navigate(AppScreens.Login.routeWithArg(autoLogIn)) { 64 | popUpTo(AppScreens.CallJoin.route) { inclusive = true } 65 | } 66 | }, 67 | navigateToDirectCallJoin = { 68 | navController.navigate(AppScreens.DirectCallJoin.route) 69 | }, 70 | navigateToBarcodeScanner = { 71 | navController.navigate(AppScreens.BarcodeScanning.route) 72 | }, 73 | ) 74 | } 75 | composable( 76 | AppScreens.CallLobby.route, 77 | arguments = listOf(navArgument("cid") { type = NavType.StringType }), 78 | ) { 79 | CallLobbyScreen( 80 | onBack = { 81 | navController.popBackStack() 82 | }, 83 | ) 84 | } 85 | composable(AppScreens.DirectCallJoin.route) { 86 | val context = LocalContext.current 87 | DirectCallJoinScreen( 88 | navigateToDirectCall = { members -> 89 | context.startActivity( 90 | DirectCallActivity.createIntent( 91 | context, 92 | members = members.split(","), 93 | ), 94 | ) 95 | }, 96 | ) 97 | } 98 | composable(AppScreens.BarcodeScanning.route) { 99 | BarcodeScanner { 100 | navController.popBackStack() 101 | } 102 | } 103 | } 104 | } 105 | 106 | enum class AppScreens(val route: String) { 107 | Login("login/{auto_log_in}"), 108 | CallJoin("call_join"), 109 | CallLobby("call_lobby/{cid}"), 110 | DirectCallJoin("direct_call_join"), 111 | BarcodeScanning("barcode_scanning"), ; 112 | fun routeWithArg(argValue: Any): String = when (this) { 113 | Login -> this.route.replace("{auto_log_in}", argValue.toString()) 114 | CallLobby -> this.route.replace("{cid}", argValue.toString()) 115 | else -> this.route 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/call/AvailableDeviceMenu.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.call 18 | 19 | import android.widget.Toast 20 | import androidx.compose.foundation.clickable 21 | import androidx.compose.foundation.layout.PaddingValues 22 | import androidx.compose.foundation.layout.fillMaxWidth 23 | import androidx.compose.foundation.layout.padding 24 | import androidx.compose.foundation.layout.width 25 | import androidx.compose.foundation.lazy.LazyColumn 26 | import androidx.compose.foundation.lazy.items 27 | import androidx.compose.foundation.shape.RoundedCornerShape 28 | import androidx.compose.material.Card 29 | import androidx.compose.material.Text 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.runtime.LaunchedEffect 32 | import androidx.compose.runtime.getValue 33 | import androidx.compose.ui.Alignment 34 | import androidx.compose.ui.Modifier 35 | import androidx.compose.ui.platform.LocalContext 36 | import androidx.compose.ui.unit.IntOffset 37 | import androidx.compose.ui.unit.dp 38 | import androidx.compose.ui.window.Popup 39 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 40 | import io.getstream.video.android.compose.theme.VideoTheme 41 | import io.getstream.video.android.core.Call 42 | import kotlinx.coroutines.delay 43 | 44 | @Composable 45 | fun AvailableDeviceMenu( 46 | call: Call, 47 | onDismissed: () -> Unit, 48 | ) { 49 | val context = LocalContext.current 50 | val availableDevices by call.microphone.devices.collectAsStateWithLifecycle() 51 | 52 | LaunchedEffect(key1 = availableDevices) { 53 | delay(3000) 54 | if (availableDevices.isEmpty()) { 55 | onDismissed.invoke() 56 | } else { 57 | Toast.makeText(context, "There's no available devices", Toast.LENGTH_SHORT).show() 58 | } 59 | } 60 | 61 | Popup( 62 | alignment = Alignment.BottomStart, 63 | offset = IntOffset(30, -200), 64 | onDismissRequest = { onDismissed.invoke() }, 65 | ) { 66 | Card( 67 | modifier = Modifier.width(140.dp), 68 | shape = RoundedCornerShape(12.dp), 69 | contentColor = VideoTheme.colors.basePrimary, 70 | backgroundColor = VideoTheme.colors.baseSheetPrimary, 71 | elevation = 6.dp, 72 | ) { 73 | LazyColumn( 74 | modifier = Modifier.fillMaxWidth(), 75 | contentPadding = PaddingValues(12.dp), 76 | ) { 77 | items(items = availableDevices, key = { it.name }) { audioDevice -> 78 | Text( 79 | modifier = Modifier 80 | .padding(bottom = 12.dp) 81 | .clickable { 82 | call.microphone.select(audioDevice) 83 | onDismissed.invoke() 84 | Toast 85 | .makeText( 86 | context, 87 | "Switched to ${audioDevice.name}", 88 | Toast.LENGTH_SHORT, 89 | ) 90 | .show() 91 | }, 92 | text = audioDevice.name, 93 | color = VideoTheme.colors.basePrimary, 94 | ) 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/call/CallActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.call 18 | 19 | import android.content.Context 20 | import android.content.Intent 21 | import android.os.Bundle 22 | import android.util.Log 23 | import android.view.WindowManager 24 | import android.widget.Toast 25 | import androidx.activity.ComponentActivity 26 | import androidx.activity.compose.setContent 27 | import androidx.compose.runtime.LaunchedEffect 28 | import androidx.compose.runtime.collectAsState 29 | import androidx.compose.runtime.getValue 30 | import androidx.lifecycle.lifecycleScope 31 | import io.getstream.android.video.chat.compose.MainActivity 32 | import io.getstream.chat.android.client.ChatClient 33 | import io.getstream.chat.android.models.Filters 34 | import io.getstream.chat.android.models.querysort.QuerySortByField 35 | import io.getstream.result.Result 36 | import io.getstream.result.onSuccessSuspend 37 | import io.getstream.video.android.core.StreamVideo 38 | import io.getstream.video.android.core.notifications.NotificationHandler 39 | import io.getstream.video.android.model.StreamCallId 40 | import io.getstream.video.android.model.streamCallId 41 | import kotlinx.coroutines.launch 42 | 43 | class CallActivity : ComponentActivity() { 44 | 45 | override fun onCreate(savedInstanceState: Bundle?) { 46 | super.onCreate(savedInstanceState) 47 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 48 | 49 | // step 1 - get the StreamVideo instance and create a call 50 | val streamVideo = StreamVideo.instance() 51 | val cid = intent.streamCallId(EXTRA_CID) 52 | ?: throw IllegalArgumentException("call type and id is invalid!") 53 | 54 | // optional - check for already active call that can be utilized 55 | // This step is optional and can be skipped 56 | // val cal = streamVideo.call(type = cid.type, id = cid.id) 57 | val activeCall = streamVideo.state.activeCall.value 58 | val call = if (activeCall != null) { 59 | if (activeCall.id != cid.id) { 60 | Log.w("CallActivity", "A call with id: ${cid.cid} existed. Leaving.") 61 | // If the call id is different leave the previous call 62 | activeCall.leave() 63 | // Return a new call 64 | streamVideo.call(type = cid.type, id = cid.id) 65 | } else { 66 | // Call ID is the same, use the active call 67 | activeCall 68 | } 69 | } else { 70 | // There is no active call, create new call 71 | streamVideo.call(type = cid.type, id = cid.id) 72 | } 73 | 74 | // optional - call settings. We disable the mic if coming from QR code demo 75 | if (intent.getBooleanExtra(EXTRA_DISABLE_MIC_BOOLEAN, false)) { 76 | call.microphone.disable(true) 77 | } 78 | 79 | // step 2 - join a call 80 | lifecycleScope.launch { 81 | // If the call is new, join the call 82 | if (activeCall != call) { 83 | val result = call.join(create = true) 84 | 85 | // Unable to join. Device is offline or other usually connection issue. 86 | if (result is Result.Failure) { 87 | Log.e("CallActivity", "Call.join failed ${result.value}") 88 | Toast.makeText( 89 | this@CallActivity, 90 | "Failed to join call (${result.value.message})", 91 | Toast.LENGTH_SHORT, 92 | ).show() 93 | finish() 94 | } 95 | } 96 | } 97 | 98 | // step 3 - build a call screen 99 | setContent { 100 | CallScreen( 101 | call = call, 102 | showDebugOptions = io.getstream.android.video.chat.compose.BuildConfig.DEBUG, 103 | onCallDisconnected = { 104 | // call state changed to disconnected - we can leave the screen 105 | goBackToMainScreen() 106 | }, 107 | onUserLeaveCall = { 108 | call.leave() 109 | // we don't need to wait for the call state to change to disconnected, we can 110 | // leave immediately 111 | goBackToMainScreen() 112 | }, 113 | ) 114 | 115 | // step 4 (optional) - chat integration 116 | val user by ChatClient.instance().clientState.user.collectAsState(initial = null) 117 | LaunchedEffect(key1 = user) { 118 | if (user != null) { 119 | val channel = ChatClient.instance().channel("videocall", cid.id) 120 | channel.queryMembers( 121 | offset = 0, 122 | limit = 10, 123 | filter = Filters.neutral(), 124 | sort = QuerySortByField(), 125 | ).await().onSuccessSuspend { members -> 126 | if (members.isNotEmpty()) { 127 | channel.addMembers(listOf(user!!.id)).await() 128 | } else { 129 | channel.create(listOf(user!!.id), emptyMap()).await() 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | private fun goBackToMainScreen() { 138 | if (!isFinishing) { 139 | val intent = Intent(this@CallActivity, MainActivity::class.java).apply { 140 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 141 | } 142 | startActivity(intent) 143 | finish() 144 | } 145 | } 146 | 147 | companion object { 148 | const val EXTRA_CID: String = NotificationHandler.INTENT_EXTRA_CALL_CID 149 | const val EXTRA_DISABLE_MIC_BOOLEAN: String = "EXTRA_DISABLE_MIC" 150 | 151 | /** 152 | * @param callId the Call ID you want to join 153 | * @param disableMicOverride optional parameter if you want to override the users setting 154 | * and disable the microphone. 155 | */ 156 | @JvmStatic 157 | fun createIntent( 158 | context: Context, 159 | callId: StreamCallId, 160 | disableMicOverride: Boolean = false, 161 | ): Intent { 162 | return Intent(context, CallActivity::class.java).apply { 163 | putExtra(EXTRA_CID, callId) 164 | putExtra(EXTRA_DISABLE_MIC_BOOLEAN, disableMicOverride) 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/call/ChatDialog.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialApi::class) 18 | 19 | package io.getstream.android.video.chat.compose.ui.call 20 | 21 | import androidx.compose.foundation.background 22 | import androidx.compose.foundation.clickable 23 | import androidx.compose.foundation.layout.Column 24 | import androidx.compose.foundation.layout.fillMaxWidth 25 | import androidx.compose.foundation.layout.height 26 | import androidx.compose.foundation.layout.padding 27 | import androidx.compose.foundation.shape.RoundedCornerShape 28 | import androidx.compose.material.ExperimentalMaterialApi 29 | import androidx.compose.material.Icon 30 | import androidx.compose.material.ModalBottomSheetLayout 31 | import androidx.compose.material.ModalBottomSheetState 32 | import androidx.compose.runtime.Composable 33 | import androidx.compose.runtime.LaunchedEffect 34 | import androidx.compose.runtime.remember 35 | import androidx.compose.ui.Alignment 36 | import androidx.compose.ui.Modifier 37 | import androidx.compose.ui.platform.LocalContext 38 | import androidx.compose.ui.res.painterResource 39 | import androidx.compose.ui.unit.dp 40 | import androidx.lifecycle.viewmodel.compose.viewModel 41 | import io.getstream.chat.android.compose.ui.messages.MessagesScreen 42 | import io.getstream.chat.android.compose.ui.theme.ChatTheme 43 | import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel 44 | import io.getstream.chat.android.compose.viewmodel.messages.MessagesViewModelFactory 45 | import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState 46 | import io.getstream.video.android.compose.theme.VideoTheme 47 | import io.getstream.video.android.core.Call 48 | import java.time.Instant 49 | import java.util.Date 50 | 51 | @Composable 52 | internal fun ChatDialog( 53 | call: Call, 54 | state: ModalBottomSheetState, 55 | content: @Composable () -> Unit, 56 | updateUnreadCount: (Int) -> Unit, 57 | onNewMessages: (List) -> Unit, 58 | onDismissed: () -> Unit, 59 | ) { 60 | val context = LocalContext.current 61 | val viewModelFactory = remember { 62 | MessagesViewModelFactory( 63 | context = context, 64 | channelId = "videocall:${call.id}", 65 | ) 66 | } 67 | val listViewModel = viewModel(MessageListViewModel::class.java, factory = viewModelFactory) 68 | val unreadCount = listViewModel.currentMessagesState.unreadCount 69 | 70 | LaunchedEffect(key1 = unreadCount) { 71 | updateUnreadCount.invoke(unreadCount) 72 | } 73 | 74 | val messageItems = listViewModel.currentMessagesState.messageItems 75 | LaunchedEffect(key1 = messageItems) { 76 | onNewMessages.invoke( 77 | messageItems.filterIsInstance().take(3) 78 | .filter { 79 | it.message.createdAt?.after( 80 | Date.from( 81 | Instant.now().minusSeconds(10), 82 | ), 83 | ) == true 84 | } 85 | .map { messageItemState -> 86 | if (messageItemState.message.text.isNotEmpty()) { 87 | messageItemState 88 | } else { 89 | messageItemState.copy( 90 | message = messageItemState.message.copy( 91 | text = "[User shared an attachment]", 92 | ), 93 | ) 94 | } 95 | } 96 | .reversed(), 97 | ) 98 | } 99 | 100 | ChatTheme(isInDarkMode = true) { 101 | ModalBottomSheetLayout( 102 | modifier = Modifier.fillMaxWidth(), 103 | sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), 104 | sheetState = state, 105 | sheetBackgroundColor = VideoTheme.colors.baseSheetPrimary, 106 | sheetContent = { 107 | if (state.isVisible) { 108 | Column( 109 | modifier = Modifier 110 | .background(ChatTheme.colors.appBackground) 111 | .fillMaxWidth() 112 | .height(500.dp), 113 | ) { 114 | Icon( 115 | modifier = Modifier 116 | .align(Alignment.End) 117 | .padding(16.dp) 118 | .clickable { onDismissed.invoke() }, 119 | tint = ChatTheme.colors.textHighEmphasis, 120 | painter = painterResource( 121 | id = 122 | io.getstream.video.android.ui.common.R.drawable.stream_video_ic_close, 123 | ), 124 | contentDescription = null, 125 | ) 126 | 127 | MessagesScreen( 128 | showHeader = false, 129 | viewModelFactory = viewModelFactory, 130 | onBackPressed = { onDismissed.invoke() }, 131 | onHeaderTitleClick = { onDismissed.invoke() }, 132 | ) 133 | } 134 | } 135 | }, 136 | content = content, 137 | ) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/call/ChatOverly.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.call 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.Column 22 | import androidx.compose.foundation.layout.Spacer 23 | import androidx.compose.foundation.layout.fillMaxWidth 24 | import androidx.compose.foundation.layout.height 25 | import androidx.compose.foundation.layout.padding 26 | import androidx.compose.foundation.layout.width 27 | import androidx.compose.foundation.shape.RoundedCornerShape 28 | import androidx.compose.material.Text 29 | import androidx.compose.runtime.Composable 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.draw.alpha 32 | import androidx.compose.ui.draw.clip 33 | import androidx.compose.ui.graphics.Color 34 | import androidx.compose.ui.platform.LocalConfiguration 35 | import androidx.compose.ui.unit.dp 36 | import androidx.compose.ui.unit.sp 37 | import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState 38 | 39 | @Composable 40 | fun ChatOverly( 41 | modifier: Modifier = Modifier, 42 | messages: List, 43 | ) { 44 | val configuration = LocalConfiguration.current 45 | Column(modifier = modifier.width((configuration.screenWidthDp * 0.65f).dp)) { 46 | if (messages.isNotEmpty()) { 47 | Message( 48 | modifier = Modifier 49 | .clip(RoundedCornerShape(32.dp)) 50 | .alpha(0.15f), 51 | messageItemState = messages[0], 52 | ) 53 | } 54 | 55 | Spacer(modifier = Modifier.height(4.dp)) 56 | 57 | if (messages.size > 1) { 58 | Message( 59 | modifier = Modifier 60 | .clip(RoundedCornerShape(32.dp)) 61 | .alpha(0.3f), 62 | messageItemState = messages[1], 63 | ) 64 | } 65 | 66 | Spacer(modifier = Modifier.height(4.dp)) 67 | 68 | if (messages.size > 2) { 69 | Message( 70 | modifier = Modifier 71 | .clip(RoundedCornerShape(32.dp)) 72 | .alpha(0.45f), 73 | messageItemState = messages[2], 74 | ) 75 | } 76 | } 77 | } 78 | 79 | @Composable 80 | private fun Message( 81 | modifier: Modifier, 82 | messageItemState: MessageItemState, 83 | ) { 84 | Box( 85 | modifier = Modifier.fillMaxWidth(), 86 | ) { 87 | Box( 88 | modifier = modifier 89 | .matchParentSize() 90 | .background(Color.Black), 91 | ) 92 | 93 | Text( 94 | modifier = Modifier 95 | .fillMaxWidth() 96 | .padding(vertical = 8.dp, horizontal = 16.dp), 97 | text = messageItemState.message.text, 98 | color = Color.White, 99 | fontSize = 13.sp, 100 | ) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/call/CustomReactionContent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.call 18 | 19 | import androidx.compose.foundation.layout.BoxScope 20 | import androidx.compose.foundation.layout.BoxWithConstraints 21 | import androidx.compose.foundation.layout.fillMaxSize 22 | import androidx.compose.foundation.layout.padding 23 | import androidx.compose.material.Text 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.LaunchedEffect 26 | import androidx.compose.runtime.getValue 27 | import androidx.compose.runtime.mutableStateOf 28 | import androidx.compose.runtime.remember 29 | import androidx.compose.runtime.setValue 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.unit.sp 32 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 33 | import io.getstream.video.android.compose.theme.VideoTheme 34 | import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle 35 | import io.getstream.video.android.core.ParticipantState 36 | import io.getstream.video.android.core.model.Reaction 37 | import io.getstream.video.android.core.model.ReactionState 38 | import kotlinx.coroutines.delay 39 | 40 | @Composable 41 | fun BoxScope.CustomReactionContent( 42 | participant: ParticipantState, 43 | style: VideoRendererStyle, 44 | ) { 45 | val reactions by participant.reactions.collectAsStateWithLifecycle() 46 | val reaction = reactions.lastOrNull { it.createdAt + 3000 > System.currentTimeMillis() } 47 | var currentReaction: Reaction? by remember { mutableStateOf(null) } 48 | var reactionState: ReactionState by remember { mutableStateOf(ReactionState.Nothing) } 49 | 50 | LaunchedEffect(key1 = reaction) { 51 | if (reactionState == ReactionState.Nothing) { 52 | currentReaction?.let { participant.consumeReaction(it) } 53 | currentReaction = reaction 54 | 55 | // deliberately execute this instead of animation finish listener to remove animation on the screen. 56 | if (reaction != null) { 57 | reactionState = ReactionState.Running 58 | delay(style.reactionDuration * 2 - 50L) 59 | participant.consumeReaction(reaction) 60 | currentReaction = null 61 | reactionState = ReactionState.Nothing 62 | } 63 | } else { 64 | if (currentReaction != null) { 65 | participant.consumeReaction(currentReaction!!) 66 | reactionState = ReactionState.Nothing 67 | currentReaction = null 68 | delay(style.reactionDuration * 2 - 50L) 69 | } 70 | } 71 | } 72 | 73 | val emojiCode = currentReaction?.response?.emojiCode 74 | if (currentReaction != null && emojiCode != null) { 75 | var isEmojiVisible by remember { mutableStateOf(true) } 76 | val emojiMapper = VideoTheme.reactionMapper 77 | val emojiText = emojiMapper.map(emojiCode) 78 | 79 | LaunchedEffect(key1 = Unit) { 80 | delay(style.reactionDuration.toLong()) 81 | isEmojiVisible = false 82 | } 83 | 84 | if (isEmojiVisible) { 85 | BoxWithConstraints(modifier = Modifier.fillMaxSize()) { 86 | Text( 87 | text = emojiText, 88 | modifier = Modifier 89 | .padding(top = maxHeight * 0.10f) 90 | .align(style.reactionPosition), 91 | fontSize = VideoTheme.dimens.componentHeightM.value.sp, 92 | ) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/call/LandscapeControls.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.call 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.layout.Arrangement 21 | import androidx.compose.foundation.layout.Box 22 | import androidx.compose.foundation.layout.Column 23 | import androidx.compose.foundation.layout.Row 24 | import androidx.compose.foundation.layout.fillMaxWidth 25 | import androidx.compose.foundation.layout.padding 26 | import androidx.compose.foundation.layout.width 27 | import androidx.compose.material.icons.Icons 28 | import androidx.compose.material.icons.filled.CallEnd 29 | import androidx.compose.runtime.Composable 30 | import androidx.compose.runtime.getValue 31 | import androidx.compose.ui.Alignment 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.platform.LocalContext 34 | import androidx.compose.ui.tooling.preview.Preview 35 | import androidx.compose.ui.unit.IntOffset 36 | import androidx.compose.ui.unit.dp 37 | import androidx.compose.ui.window.Popup 38 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 39 | import io.getstream.android.video.chat.compose.tooling.extensions.toPx 40 | import io.getstream.video.android.compose.theme.VideoTheme 41 | import io.getstream.video.android.compose.ui.components.base.StreamButton 42 | import io.getstream.video.android.compose.ui.components.call.controls.actions.FlipCameraAction 43 | import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleCameraAction 44 | import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleMicrophoneAction 45 | import io.getstream.video.android.core.Call 46 | import io.getstream.video.android.core.mapper.ReactionMapper 47 | import io.getstream.video.android.mock.StreamPreviewDataUtils 48 | import io.getstream.video.android.mock.previewCall 49 | 50 | @Composable 51 | fun LandscapeControls(call: Call, onDismiss: () -> Unit) { 52 | val isCameraEnabled by call.camera.isEnabled.collectAsStateWithLifecycle() 53 | val isMicrophoneEnabled by call.microphone.isEnabled.collectAsStateWithLifecycle() 54 | val toggleCamera = { 55 | call.camera.setEnabled(!isCameraEnabled, true) 56 | } 57 | val toggleMicrophone = { 58 | call.microphone.setEnabled(!isMicrophoneEnabled, true) 59 | } 60 | val onClick = { 61 | call.leave() 62 | } 63 | 64 | Popup( 65 | onDismissRequest = onDismiss, 66 | alignment = Alignment.TopEnd, 67 | offset = IntOffset( 68 | 0, 69 | (VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingM).toPx().toInt(), 70 | ), 71 | ) { 72 | LandscapeControlsContent( 73 | isCameraEnabled = isCameraEnabled, 74 | isMicEnabled = isMicrophoneEnabled, 75 | call = call, 76 | camera = toggleCamera, 77 | mic = toggleMicrophone, 78 | onClick = onClick, 79 | ) { 80 | onDismiss() 81 | } 82 | } 83 | } 84 | 85 | @Composable 86 | fun LandscapeControlsContent( 87 | isCameraEnabled: Boolean, 88 | isMicEnabled: Boolean, 89 | call: Call, 90 | camera: () -> Unit, 91 | mic: () -> Unit, 92 | onClick: () -> Unit, 93 | onDismiss: () -> Unit, 94 | ) { 95 | Box( 96 | modifier = Modifier 97 | .background( 98 | color = VideoTheme.colors.baseSheetPrimary, 99 | shape = VideoTheme.shapes.dialog, 100 | ) 101 | .width(400.dp), 102 | ) { 103 | Column( 104 | modifier = Modifier 105 | .padding(12.dp), 106 | ) { 107 | ReactionsMenu(call = call, reactionMapper = ReactionMapper.defaultReactionMapper()) { 108 | onDismiss() 109 | } 110 | Row( 111 | modifier = Modifier.fillMaxWidth(), 112 | horizontalArrangement = Arrangement.SpaceBetween, 113 | ) { 114 | ToggleCameraAction(isCameraEnabled = isCameraEnabled) { 115 | camera() 116 | } 117 | ToggleMicrophoneAction(isMicrophoneEnabled = isMicEnabled) { 118 | mic() 119 | } 120 | FlipCameraAction { 121 | call.camera.flip() 122 | } 123 | 124 | StreamButton( 125 | style = VideoTheme.styles.buttonStyles.alertButtonStyle(), 126 | icon = Icons.Default.CallEnd, 127 | text = "Leave call", 128 | onClick = onClick, 129 | ) 130 | } 131 | } 132 | } 133 | } 134 | 135 | @Preview 136 | @Composable 137 | fun LandscapeControlsPreview() { 138 | StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) 139 | VideoTheme { 140 | LandscapeControlsContent( 141 | true, 142 | false, 143 | previewCall, 144 | {}, 145 | {}, 146 | {}, 147 | ) { 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/call/LayoutChooser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.call 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.layout.Column 21 | import androidx.compose.foundation.layout.width 22 | import androidx.compose.material.icons.Icons 23 | import androidx.compose.material.icons.filled.AutoAwesome 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.rememberUpdatedState 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.graphics.vector.ImageVector 28 | import androidx.compose.ui.platform.LocalContext 29 | import androidx.compose.ui.res.vectorResource 30 | import androidx.compose.ui.state.ToggleableState 31 | import androidx.compose.ui.tooling.preview.Preview 32 | import androidx.compose.ui.unit.IntOffset 33 | import androidx.compose.ui.unit.dp 34 | import androidx.compose.ui.window.Popup 35 | import io.getstream.android.video.chat.compose.R 36 | import io.getstream.android.video.chat.compose.tooling.extensions.toPx 37 | import io.getstream.video.android.compose.theme.VideoTheme 38 | import io.getstream.video.android.compose.ui.components.base.StreamToggleButton 39 | import io.getstream.video.android.compose.ui.components.call.renderer.LayoutType 40 | import io.getstream.video.android.mock.StreamPreviewDataUtils 41 | 42 | private data class LayoutChooserDataItem( 43 | val which: LayoutType, 44 | val text: String = "", 45 | ) 46 | 47 | private val layouts = arrayOf( 48 | LayoutChooserDataItem(LayoutType.DYNAMIC, "Dynamic"), 49 | LayoutChooserDataItem(LayoutType.SPOTLIGHT, "Spotlight"), 50 | LayoutChooserDataItem(LayoutType.GRID, "Grid"), 51 | ) 52 | 53 | /** 54 | * Reactions menu. The reaction menu is a dialog displaying the list of reactions found in 55 | * [DefaultReactionsMenuData]. 56 | * @param current 57 | * @param onDismiss on dismiss listener. 58 | */ 59 | @Composable 60 | internal fun LayoutChooser( 61 | current: LayoutType, 62 | onLayoutChoice: (LayoutType) -> Unit, 63 | onDismiss: () -> Unit, 64 | ) { 65 | Popup( 66 | offset = IntOffset( 67 | 0, 68 | (VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingS).toPx().toInt(), 69 | ), 70 | onDismissRequest = onDismiss, 71 | ) { 72 | Column( 73 | Modifier.background( 74 | color = VideoTheme.colors.baseSheetPrimary, 75 | shape = VideoTheme.shapes.sheet, 76 | ) 77 | .width(300.dp), 78 | ) { 79 | layouts.forEach { layout -> 80 | 81 | val state = ToggleableState(layout.which == current) 82 | val icon = when (layout.which) { 83 | LayoutType.DYNAMIC -> Icons.Default.AutoAwesome 84 | LayoutType.SPOTLIGHT -> ImageVector.vectorResource( 85 | R.drawable.ic_layout_spotlight, 86 | ) 87 | LayoutType.GRID -> ImageVector.vectorResource(R.drawable.ic_layout_grid) 88 | } 89 | StreamToggleButton( 90 | onText = layout.text, 91 | offText = layout.text, 92 | toggleState = rememberUpdatedState(newValue = state), 93 | onIcon = icon, 94 | onStyle = VideoTheme.styles.buttonStyles.toggleButtonStyleOn(), 95 | offStyle = VideoTheme.styles.buttonStyles.toggleButtonStyleOff(), 96 | ) { 97 | onLayoutChoice(layout.which) 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | @Preview 105 | @Composable 106 | private fun LayoutChooserPreview() { 107 | StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) 108 | VideoTheme { 109 | LayoutChooser( 110 | current = LayoutType.GRID, 111 | onLayoutChoice = {}, 112 | onDismiss = {}, 113 | ) 114 | } 115 | } 116 | 117 | @Preview 118 | @Composable 119 | private fun LayoutChooserPreview2() { 120 | StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) 121 | VideoTheme { 122 | LayoutChooser( 123 | current = LayoutType.SPOTLIGHT, 124 | onLayoutChoice = {}, 125 | onDismiss = {}, 126 | ) 127 | } 128 | } 129 | 130 | @Preview 131 | @Composable 132 | private fun LayoutChooserPreview3() { 133 | StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) 134 | VideoTheme { 135 | LayoutChooser( 136 | current = LayoutType.DYNAMIC, 137 | onLayoutChoice = {}, 138 | onDismiss = {}, 139 | ) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/call/ReactionsMenu.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:OptIn(ExperimentalLayoutApi::class) 18 | 19 | package io.getstream.android.video.chat.compose.ui.call 20 | 21 | import androidx.compose.foundation.layout.Arrangement 22 | import androidx.compose.foundation.layout.Column 23 | import androidx.compose.foundation.layout.ExperimentalLayoutApi 24 | import androidx.compose.foundation.layout.FlowRow 25 | import androidx.compose.foundation.layout.Row 26 | import androidx.compose.foundation.layout.fillMaxWidth 27 | import androidx.compose.foundation.layout.requiredHeight 28 | import androidx.compose.foundation.layout.requiredWidth 29 | import androidx.compose.runtime.Composable 30 | import androidx.compose.runtime.rememberCoroutineScope 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.platform.LocalContext 33 | import androidx.compose.ui.tooling.preview.Preview 34 | import io.getstream.video.android.compose.theme.VideoTheme 35 | import io.getstream.video.android.compose.ui.components.base.StreamButton 36 | import io.getstream.video.android.compose.ui.components.base.styling.StyleSize 37 | import io.getstream.video.android.core.Call 38 | import io.getstream.video.android.core.mapper.ReactionMapper 39 | import io.getstream.video.android.mock.StreamPreviewDataUtils 40 | import io.getstream.video.android.mock.previewCall 41 | import kotlinx.coroutines.CoroutineScope 42 | import kotlinx.coroutines.launch 43 | 44 | /** 45 | * Default reaction item data 46 | * 47 | * @param displayText the text visible on the screen. 48 | * @param emojiCode the code of the emoji e.g. ":like:" 49 | * */ 50 | private data class ReactionItemData(val displayText: String, val emojiCode: String) 51 | 52 | /** 53 | * Default defined reactions. 54 | * 55 | * There is one main reaction, and a list of other reactions. The main reaction is shown on top of the rest. 56 | */ 57 | private object DefaultReactionsMenuData { 58 | val mainReaction = ReactionItemData("Raise hand", ":raise-hand:") 59 | val defaultReactions = listOf( 60 | ReactionItemData("Fireworks", ":fireworks:"), 61 | ReactionItemData("Like", ":like:"), 62 | ReactionItemData("Dislike", ":dislike:"), 63 | ReactionItemData("Smile", ":smile:"), 64 | ReactionItemData("Heart", ":heart:"), 65 | ) 66 | } 67 | 68 | /** 69 | * Reactions menu. The reaction menu is a dialog displaying the list of reactions found in 70 | * [DefaultReactionsMenuData]. 71 | * 72 | * @param call the call object. 73 | * @param reactionMapper the mapper of reactions to map from emoji code into UTF see: [ReactionMapper] 74 | * @param onDismiss on dismiss listener. 75 | */ 76 | @Composable 77 | internal fun ReactionsMenu( 78 | call: Call, 79 | reactionMapper: ReactionMapper, 80 | onDismiss: () -> Unit, 81 | ) { 82 | val scope = rememberCoroutineScope() 83 | val onEmojiSelected: (emoji: String) -> Unit = { 84 | sendReaction(scope, call, it, onDismiss) 85 | } 86 | Column(Modifier.fillMaxWidth()) { 87 | FlowRow( 88 | modifier = Modifier.fillMaxWidth(), 89 | horizontalArrangement = Arrangement.SpaceBetween, 90 | maxItemsInEachRow = 5, 91 | verticalArrangement = Arrangement.Center, 92 | ) { 93 | DefaultReactionsMenuData.defaultReactions.forEach { 94 | ReactionItem( 95 | reactionMapper = reactionMapper, 96 | onEmojiSelected = onEmojiSelected, 97 | reaction = it, 98 | ) 99 | } 100 | } 101 | 102 | Row(horizontalArrangement = Arrangement.Center) { 103 | ReactionItem( 104 | showText = true, 105 | reactionMapper = reactionMapper, 106 | reaction = DefaultReactionsMenuData.mainReaction, 107 | onEmojiSelected = onEmojiSelected, 108 | ) 109 | } 110 | } 111 | } 112 | 113 | @Composable 114 | private fun ReactionItem( 115 | reactionMapper: ReactionMapper, 116 | reaction: ReactionItemData, 117 | showText: Boolean = false, 118 | onEmojiSelected: (emoji: String) -> Unit, 119 | ) { 120 | val mappedEmoji = reactionMapper.map(reaction.emojiCode) 121 | val text = if (showText) { 122 | "$mappedEmoji ${reaction.displayText}" 123 | } else { 124 | mappedEmoji 125 | } 126 | val modifier = if (showText) { 127 | Modifier.fillMaxWidth() 128 | } else { 129 | Modifier 130 | .requiredWidth(VideoTheme.dimens.componentHeightL) 131 | .requiredHeight(VideoTheme.dimens.componentHeightL) 132 | } 133 | StreamButton( 134 | modifier = modifier, 135 | style = VideoTheme.styles.buttonStyles.primaryIconButtonStyle(StyleSize.S), 136 | text = text, 137 | onClick = { onEmojiSelected(reaction.emojiCode) }, 138 | ) 139 | } 140 | 141 | private fun sendReaction(scope: CoroutineScope, call: Call, emoji: String, onDismiss: () -> Unit) { 142 | scope.launch { 143 | call.sendReaction("default", emoji) 144 | onDismiss() 145 | } 146 | } 147 | 148 | @Preview 149 | @Composable 150 | private fun ReactionMenuPreview() { 151 | VideoTheme { 152 | StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) 153 | ReactionsMenu( 154 | call = previewCall, 155 | reactionMapper = ReactionMapper.defaultReactionMapper(), 156 | onDismiss = { }, 157 | ) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/call/ShareCall.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.call 18 | 19 | import android.content.ClipData 20 | import android.content.ClipboardManager 21 | import android.content.Context 22 | import android.content.Intent 23 | import androidx.compose.foundation.background 24 | import androidx.compose.foundation.layout.Arrangement 25 | import androidx.compose.foundation.layout.Box 26 | import androidx.compose.foundation.layout.Column 27 | import androidx.compose.foundation.layout.Row 28 | import androidx.compose.foundation.layout.Spacer 29 | import androidx.compose.foundation.layout.fillMaxWidth 30 | import androidx.compose.foundation.layout.padding 31 | import androidx.compose.foundation.layout.size 32 | import androidx.compose.foundation.layout.width 33 | import androidx.compose.material.Icon 34 | import androidx.compose.material.IconButton 35 | import androidx.compose.material.Text 36 | import androidx.compose.material.icons.Icons 37 | import androidx.compose.material.icons.filled.CopyAll 38 | import androidx.compose.material.icons.filled.PersonAddAlt1 39 | import androidx.compose.runtime.Composable 40 | import androidx.compose.runtime.State 41 | import androidx.compose.ui.Alignment 42 | import androidx.compose.ui.Modifier 43 | import androidx.compose.ui.graphics.Color 44 | import androidx.compose.ui.text.TextStyle 45 | import androidx.compose.ui.text.font.FontWeight 46 | import androidx.compose.ui.text.style.TextOverflow 47 | import androidx.compose.ui.unit.dp 48 | import androidx.compose.ui.unit.sp 49 | import io.getstream.android.video.chat.compose.util.config.types.StreamEnvironment 50 | import io.getstream.video.android.compose.theme.VideoTheme 51 | import io.getstream.video.android.compose.ui.components.base.StreamButton 52 | import io.getstream.video.android.core.Call 53 | 54 | @Composable 55 | public fun ShareCallWithOthers( 56 | modifier: Modifier = Modifier, 57 | call: Call, 58 | clipboardManager: ClipboardManager?, 59 | env: State, 60 | context: Context, 61 | ) { 62 | ShareSettingsBox(modifier, call, clipboardManager) { 63 | val link = "${env.value?.sharelink}${call.id}" 64 | val sendIntent: Intent = Intent().apply { 65 | action = Intent.ACTION_SEND 66 | putExtra(Intent.EXTRA_TEXT, link) 67 | type = "text/plain" 68 | } 69 | val shareIntent = Intent.createChooser(sendIntent, null) 70 | context.startActivity(shareIntent) 71 | } 72 | } 73 | 74 | @Composable 75 | public fun ShareSettingsBox( 76 | modifier: Modifier = Modifier, 77 | call: Call, 78 | clipboardManager: ClipboardManager?, 79 | onShare: (String) -> Unit, 80 | ) { 81 | Box( 82 | modifier = modifier 83 | .background( 84 | color = VideoTheme.colors.baseSheetTertiary, 85 | shape = VideoTheme.shapes.dialog, 86 | ), 87 | ) { 88 | Column( 89 | modifier = Modifier 90 | .fillMaxWidth() 91 | .padding(VideoTheme.dimens.spacingL), 92 | horizontalAlignment = Alignment.CenterHorizontally, 93 | ) { 94 | StreamButton( 95 | modifier = Modifier.fillMaxWidth(), 96 | text = "Share link with others", 97 | icon = Icons.Default.PersonAddAlt1, 98 | style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), 99 | ) { 100 | onShare(call.id) 101 | } 102 | Spacer(modifier = Modifier.size(16.dp)) 103 | Text( 104 | text = "Or share this call ID with the \u2028others you want in the meeting", 105 | style = VideoTheme.typography.bodyM, 106 | ) 107 | Spacer(modifier = Modifier.size(16.dp)) 108 | Row( 109 | modifier = Modifier.fillMaxWidth(), 110 | horizontalArrangement = Arrangement.SpaceBetween, 111 | verticalAlignment = Alignment.CenterVertically, 112 | ) { 113 | Row { 114 | Text( 115 | text = "Call ID: ", 116 | style = TextStyle( 117 | fontSize = 16.sp, 118 | lineHeight = 20.sp, 119 | fontWeight = FontWeight(500), 120 | color = Color.White, 121 | ), 122 | ) 123 | Text( 124 | modifier = Modifier.width(200.dp), 125 | maxLines = 1, 126 | overflow = TextOverflow.Ellipsis, 127 | text = call.id, 128 | softWrap = false, 129 | style = TextStyle( 130 | fontSize = 16.sp, 131 | lineHeight = 16.sp, 132 | fontWeight = FontWeight.W400, 133 | color = VideoTheme.colors.brandCyan, 134 | ), 135 | ) 136 | } 137 | 138 | Spacer(modifier = Modifier.size(8.dp)) 139 | IconButton( 140 | modifier = Modifier.size(32.dp), 141 | onClick = { 142 | val clipData = ClipData.newPlainText("Call ID", call.id) 143 | clipboardManager?.setPrimaryClip(clipData) 144 | }, 145 | ) { 146 | Icon( 147 | tint = Color.White, 148 | imageVector = Icons.Default.CopyAll, 149 | contentDescription = "Copy", 150 | ) 151 | } 152 | } 153 | Spacer(modifier = Modifier.size(16.dp)) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/join/CallJoinViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.join 18 | 19 | import androidx.lifecycle.ViewModel 20 | import androidx.lifecycle.viewModelScope 21 | import com.google.android.gms.auth.api.signin.GoogleSignInClient 22 | import dagger.hilt.android.lifecycle.HiltViewModel 23 | import io.getstream.android.video.chat.compose.util.NetworkMonitor 24 | import io.getstream.android.video.chat.compose.util.StreamVideoInitHelper 25 | import io.getstream.chat.android.client.ChatClient 26 | import io.getstream.video.android.core.Call 27 | import io.getstream.video.android.core.StreamVideo 28 | import io.getstream.video.android.datastore.delegate.StreamUserDataStore 29 | import io.getstream.video.android.model.User 30 | import io.getstream.video.android.model.mapper.isValidCallId 31 | import io.getstream.video.android.model.mapper.toTypeAndId 32 | import kotlinx.coroutines.delay 33 | import kotlinx.coroutines.flow.Flow 34 | import kotlinx.coroutines.flow.MutableSharedFlow 35 | import kotlinx.coroutines.flow.SharedFlow 36 | import kotlinx.coroutines.flow.SharingStarted 37 | import kotlinx.coroutines.flow.flatMapLatest 38 | import kotlinx.coroutines.flow.flowOf 39 | import kotlinx.coroutines.flow.map 40 | import kotlinx.coroutines.flow.shareIn 41 | import kotlinx.coroutines.launch 42 | import java.util.UUID 43 | import javax.inject.Inject 44 | 45 | @HiltViewModel 46 | class CallJoinViewModel @Inject constructor( 47 | private val dataStore: StreamUserDataStore, 48 | private val googleSignInClient: GoogleSignInClient, 49 | networkMonitor: NetworkMonitor, 50 | ) : ViewModel() { 51 | val user: Flow = dataStore.user 52 | val isLoggedOut = dataStore.user.map { it == null } 53 | var autoLogInAfterLogOut = true 54 | val isNetworkAvailable = networkMonitor.isNetworkAvailable 55 | 56 | private val event: MutableSharedFlow = MutableSharedFlow() 57 | internal val uiState: SharedFlow = event 58 | .flatMapLatest { event -> 59 | when (event) { 60 | is CallJoinEvent.GoBackToLogin -> { 61 | flowOf(CallJoinUiState.GoBackToLogin) 62 | } 63 | 64 | is CallJoinEvent.JoinCall -> { 65 | val call = joinCall(event.callId) 66 | flowOf(CallJoinUiState.JoinCompleted(callId = call.cid)) 67 | } 68 | 69 | is CallJoinEvent.JoinCompleted -> flowOf( 70 | CallJoinUiState.JoinCompleted(event.callId), 71 | ) 72 | 73 | else -> flowOf(CallJoinUiState.Nothing) 74 | } 75 | } 76 | .shareIn(viewModelScope, SharingStarted.Lazily, 0) 77 | 78 | init { 79 | viewModelScope.launch { 80 | isNetworkAvailable.collect { isNetworkAvailable -> 81 | if (isNetworkAvailable && !StreamVideo.isInstalled) { 82 | StreamVideoInitHelper.loadSdk( 83 | dataStore = dataStore, 84 | ) 85 | } 86 | } 87 | } 88 | } 89 | 90 | fun handleUiEvent(event: CallJoinEvent) { 91 | viewModelScope.launch { this@CallJoinViewModel.event.emit(event) } 92 | } 93 | 94 | private fun joinCall(callId: String? = null): Call { 95 | val streamVideo = StreamVideo.instance() 96 | val newCallId = callId ?: "default:${UUID.randomUUID()}" 97 | val (type, id) = if (newCallId.isValidCallId()) { 98 | newCallId.toTypeAndId() 99 | } else { 100 | "default" to newCallId 101 | } 102 | return streamVideo.call(type = type, id = id) 103 | } 104 | 105 | fun logOut() { 106 | viewModelScope.launch { 107 | googleSignInClient.signOut() 108 | dataStore.clear() 109 | StreamVideo.instance().logOut() 110 | ChatClient.instance().disconnect(true).enqueue() 111 | delay(200) 112 | StreamVideo.removeClient() 113 | } 114 | } 115 | } 116 | 117 | sealed interface CallJoinUiState { 118 | object Nothing : CallJoinUiState 119 | 120 | data class JoinCompleted(val callId: String) : CallJoinUiState 121 | 122 | object GoBackToLogin : CallJoinUiState 123 | } 124 | 125 | sealed interface CallJoinEvent { 126 | object Nothing : CallJoinEvent 127 | 128 | data class JoinCall(val callId: String? = null) : CallJoinEvent 129 | 130 | data class JoinCompleted(val callId: String) : CallJoinEvent 131 | 132 | object GoBackToLogin : CallJoinEvent 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/login/GoogleSignIn.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.login 18 | 19 | import android.content.Intent 20 | import androidx.activity.ComponentActivity 21 | import androidx.activity.compose.ManagedActivityResultLauncher 22 | import androidx.activity.compose.rememberLauncherForActivityResult 23 | import androidx.activity.result.ActivityResult 24 | import androidx.activity.result.contract.ActivityResultContracts 25 | import androidx.compose.runtime.Composable 26 | import com.google.android.gms.auth.api.signin.GoogleSignIn 27 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount 28 | import com.google.android.gms.common.api.ApiException 29 | import com.google.android.gms.tasks.Task 30 | 31 | @Composable 32 | fun rememberRegisterForActivityResult( 33 | onSignInSuccess: (email: String) -> Unit, 34 | onSignInFailed: () -> Unit, 35 | ): ManagedActivityResultLauncher { 36 | return rememberLauncherForActivityResult( 37 | ActivityResultContracts.StartActivityForResult(), 38 | ) { result -> 39 | if (result.resultCode != ComponentActivity.RESULT_OK) { 40 | onSignInFailed.invoke() 41 | } 42 | 43 | val task: Task = GoogleSignIn.getSignedInAccountFromIntent(result.data) 44 | try { 45 | val account = task.getResult(ApiException::class.java) 46 | 47 | account?.email?.let { 48 | onSignInSuccess(it) 49 | } ?: onSignInFailed() 50 | } catch (e: ApiException) { 51 | // The ApiException status code indicates the detailed failure reason. 52 | // Please refer to the GoogleSignInStatusCodes class reference for more information. 53 | onSignInFailed() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/login/GoogleSignInLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.login 18 | 19 | import android.content.Intent 20 | import android.util.Log 21 | import androidx.activity.ComponentActivity 22 | import androidx.activity.compose.ManagedActivityResultLauncher 23 | import androidx.activity.compose.rememberLauncherForActivityResult 24 | import androidx.activity.result.ActivityResult 25 | import androidx.activity.result.contract.ActivityResultContracts 26 | import androidx.compose.runtime.Composable 27 | import com.google.android.gms.auth.api.signin.GoogleSignIn 28 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount 29 | import com.google.android.gms.common.api.ApiException 30 | import com.google.android.gms.tasks.Task 31 | 32 | @Composable 33 | fun rememberLauncherForGoogleSignInActivityResult( 34 | onSignInSuccess: (email: String) -> Unit, 35 | onSignInFailed: () -> Unit, 36 | ): ManagedActivityResultLauncher { 37 | return rememberLauncherForActivityResult( 38 | ActivityResultContracts.StartActivityForResult(), 39 | ) { result -> 40 | Log.d("Google Sign In", "Checking activity result") 41 | 42 | if (result.resultCode != ComponentActivity.RESULT_OK) { 43 | Log.d("Google Sign In", "Failed with result not OK: ${result.resultCode}") 44 | onSignInFailed() 45 | } else { 46 | val task: Task = GoogleSignIn.getSignedInAccountFromIntent( 47 | result.data, 48 | ) 49 | try { 50 | val account = task.getResult(ApiException::class.java) 51 | val email = account.email 52 | 53 | if (email != null) { 54 | Log.d("Google Sign In", "Successful: $email") 55 | onSignInSuccess(email) 56 | } else { 57 | Log.d("Google Sign In", "Failed with null email") 58 | onSignInFailed() 59 | } 60 | } catch (e: ApiException) { 61 | // The ApiException status code indicates the detailed failure reason. 62 | // Please refer to the GoogleSignInStatusCodes class reference for more information. 63 | Log.d("Google Sign In", "Failed with ApiException: ${e.statusCode}") 64 | onSignInFailed() 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/menu/VideoFiltersMenu.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.menu 18 | 19 | import androidx.annotation.DrawableRes 20 | import androidx.compose.foundation.horizontalScroll 21 | import androidx.compose.foundation.layout.Arrangement 22 | import androidx.compose.foundation.layout.Row 23 | import androidx.compose.foundation.layout.fillMaxWidth 24 | import androidx.compose.foundation.rememberScrollState 25 | import androidx.compose.material.icons.Icons 26 | import androidx.compose.material.icons.filled.AccountCircle 27 | import androidx.compose.material.icons.filled.BlurOn 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.runtime.rememberUpdatedState 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.graphics.vector.ImageVector 33 | import androidx.compose.ui.platform.LocalContext 34 | import androidx.compose.ui.state.ToggleableState 35 | import androidx.compose.ui.tooling.preview.Preview 36 | import io.getstream.android.video.chat.compose.R 37 | import io.getstream.video.android.compose.theme.VideoTheme 38 | import io.getstream.video.android.compose.ui.components.base.StreamDrawableToggleButton 39 | import io.getstream.video.android.compose.ui.components.base.StreamIconToggleButton 40 | import io.getstream.video.android.compose.ui.components.base.styling.ButtonStyles 41 | import io.getstream.video.android.mock.StreamPreviewDataUtils 42 | 43 | @Composable 44 | internal fun VideoFiltersMenu(selectedFilterIndex: Int = 0, onSelectFilter: (Int) -> Unit) { 45 | Row( 46 | modifier = Modifier 47 | .fillMaxWidth() 48 | .horizontalScroll(state = rememberScrollState()), 49 | horizontalArrangement = Arrangement.spacedBy(VideoTheme.dimens.spacingM), 50 | verticalAlignment = Alignment.CenterVertically, 51 | ) { 52 | availableVideoFilters.forEachIndexed { index, filter -> 53 | val toggleState = if (index == selectedFilterIndex) ToggleableState.On else ToggleableState.Off 54 | 55 | when (filter) { 56 | is VideoFilter.None -> BlurredBackgroundToggleItem( 57 | icon = Icons.Default.AccountCircle, 58 | toggleState = toggleState, 59 | onClick = { onSelectFilter(index) }, 60 | ) 61 | is VideoFilter.BlurredBackground -> BlurredBackgroundToggleItem( 62 | icon = Icons.Default.BlurOn, 63 | toggleState = toggleState, 64 | onClick = { onSelectFilter(index) }, 65 | ) 66 | is VideoFilter.VirtualBackground -> VirtualBackgroundToggleItem( 67 | drawable = filter.drawable, 68 | toggleState = toggleState, 69 | onClick = { onSelectFilter(index) }, 70 | ) 71 | } 72 | } 73 | } 74 | } 75 | 76 | val availableVideoFilters = listOf( 77 | VideoFilter.None, 78 | VideoFilter.BlurredBackground, 79 | VideoFilter.VirtualBackground(R.drawable.amsterdam1), 80 | VideoFilter.VirtualBackground(R.drawable.amsterdam2), 81 | VideoFilter.VirtualBackground(R.drawable.boulder1), 82 | VideoFilter.VirtualBackground(R.drawable.boulder2), 83 | VideoFilter.VirtualBackground(R.drawable.gradient1), 84 | ) 85 | 86 | sealed class VideoFilter { 87 | data object None : VideoFilter() 88 | data object BlurredBackground : VideoFilter() 89 | data class VirtualBackground(@DrawableRes val drawable: Int) : VideoFilter() 90 | } 91 | 92 | @Composable 93 | private fun BlurredBackgroundToggleItem( 94 | icon: ImageVector, 95 | toggleState: ToggleableState, 96 | onClick: () -> Unit = {}, 97 | ) { 98 | StreamIconToggleButton( 99 | toggleState = rememberUpdatedState(newValue = toggleState), 100 | onIcon = icon, 101 | onStyle = VideoTheme.styles.buttonStyles.primaryIconButtonStyle(), 102 | offStyle = VideoTheme.styles.buttonStyles.tertiaryIconButtonStyle(), 103 | onClick = { onClick() }, 104 | ) 105 | } 106 | 107 | @Composable 108 | private fun VirtualBackgroundToggleItem( 109 | @DrawableRes drawable: Int, 110 | toggleState: ToggleableState, 111 | onClick: () -> Unit = {}, 112 | ) { 113 | StreamDrawableToggleButton( 114 | toggleState = rememberUpdatedState(newValue = toggleState), 115 | onDrawable = drawable, 116 | onStyle = ButtonStyles.drawableToggleButtonStyleOn(), 117 | offStyle = ButtonStyles.drawableToggleButtonStyleOff(), 118 | onClick = { onClick() }, 119 | ) 120 | } 121 | 122 | @Preview(showBackground = true) 123 | @Composable 124 | private fun VideoFiltersMenuPreview() { 125 | VideoTheme { 126 | StreamPreviewDataUtils.initializeStreamVideo(LocalContext.current) 127 | VideoFiltersMenu(selectedFilterIndex = 0, onSelectFilter = {}) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/menu/base/MenuTypes.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.menu.base 18 | 19 | import androidx.compose.ui.graphics.vector.ImageVector 20 | 21 | /** 22 | * Parent class on all menu items. 23 | * 24 | * @param title - title of the item, used to display in the menu, or a subtitle to the sub menu. 25 | * @param icon - the icon to be shown with the item. 26 | * @param highlight - if the icon should be highlighted or not (usually tinted with primary color) 27 | */ 28 | abstract class MenuItem( 29 | val title: String, 30 | val icon: ImageVector, 31 | val highlight: Boolean = false, 32 | ) 33 | 34 | /** 35 | * Same as [MenuItem] but additionally has an action associated with it. 36 | * 37 | * @param action - the action that will execute when the item is clicked. 38 | */ 39 | class ActionMenuItem( 40 | title: String, 41 | icon: ImageVector, 42 | highlight: Boolean = false, 43 | val action: () -> Unit, 44 | ) : MenuItem(title, icon, highlight) 45 | 46 | /** 47 | * Unlike the [ActionMenuItem] the [SubMenuItem] contains a list of [MenuItem] that create a new submenu. 48 | * Clicking a [SubMenuItem] will show the [items]. 49 | * 50 | * @param items - the items will be shown in the menu. 51 | */ 52 | open class SubMenuItem(title: String, icon: ImageVector, val items: List) : 53 | MenuItem(title, icon) 54 | 55 | /** 56 | * Similar to the [SubMenuItem] the [DynamicSubMenuItem] contains an [itemsLoader] function to load the items. 57 | * The [DynamicMenu] knows how to invoke this function to dynamically load the items while showing a progress indicator. 58 | * 59 | * @param itemsLoader the items provider function. 60 | */ 61 | class DynamicSubMenuItem( 62 | title: String, 63 | icon: ImageVector, 64 | val itemsLoader: suspend () -> List, 65 | ) : SubMenuItem(title, icon, emptyList()) 66 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/ui/outgoing/DirectCallJoinViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.ui.outgoing 18 | 19 | import androidx.lifecycle.ViewModel 20 | import androidx.lifecycle.viewModelScope 21 | import dagger.hilt.android.lifecycle.HiltViewModel 22 | import io.getstream.android.video.chat.compose.data.repositories.GoogleAccountRepository 23 | import io.getstream.android.video.chat.compose.models.GoogleAccount 24 | import io.getstream.video.android.datastore.delegate.StreamUserDataStore 25 | import io.getstream.video.android.model.User 26 | import kotlinx.coroutines.flow.MutableStateFlow 27 | import kotlinx.coroutines.flow.asStateFlow 28 | import kotlinx.coroutines.flow.firstOrNull 29 | import kotlinx.coroutines.flow.update 30 | import kotlinx.coroutines.launch 31 | import javax.inject.Inject 32 | 33 | @HiltViewModel 34 | class DirectCallJoinViewModel @Inject constructor( 35 | private val userDataStore: StreamUserDataStore, 36 | private val googleAccountRepository: GoogleAccountRepository, 37 | ) : ViewModel() { 38 | private val _uiState = MutableStateFlow(DirectCallUiState()) 39 | val uiState = _uiState.asStateFlow() 40 | 41 | init { 42 | viewModelScope.launch { 43 | _uiState.update { it.copy(currentUser = userDataStore.user.firstOrNull()) } 44 | } 45 | } 46 | 47 | fun getGoogleAccounts() { 48 | _uiState.update { it.copy(isLoading = true) } 49 | 50 | viewModelScope.launch { 51 | _uiState.update { 52 | it.copy( 53 | isLoading = false, 54 | googleAccounts = googleAccountRepository.getAllAccounts()?.map { user -> 55 | GoogleAccountUiState( 56 | isSelected = false, 57 | account = user, 58 | ) 59 | }, 60 | ) 61 | } 62 | } 63 | } 64 | 65 | fun toggleGoogleAccountSelection(selectedIndex: Int) { 66 | _uiState.update { 67 | it.copy( 68 | googleAccounts = it.googleAccounts?.mapIndexed { index, accountUiState -> 69 | if (index == selectedIndex) { 70 | GoogleAccountUiState( 71 | isSelected = !accountUiState.isSelected, 72 | account = accountUiState.account, 73 | ) 74 | } else { 75 | accountUiState 76 | } 77 | }, 78 | ) 79 | } 80 | } 81 | } 82 | 83 | data class DirectCallUiState( 84 | val isLoading: Boolean = false, 85 | val currentUser: User? = null, 86 | val googleAccounts: List? = emptyList(), 87 | ) 88 | 89 | data class GoogleAccountUiState( 90 | val isSelected: Boolean = false, 91 | val account: GoogleAccount, 92 | ) 93 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/util/FeedbackSender.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.util 18 | 19 | import kotlinx.coroutines.CoroutineScope 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.MainScope 22 | import kotlinx.coroutines.launch 23 | import okhttp3.MultipartBody 24 | import okhttp3.OkHttpClient 25 | import okhttp3.Request 26 | import okhttp3.Response 27 | import java.net.URL 28 | 29 | /** 30 | * A simple http post sender for the feedback request. 31 | */ 32 | class FeedbackSender { 33 | private val client = OkHttpClient() 34 | 35 | fun isValidEmail(email: String): Boolean { 36 | val emailRegex = "^[A-Za-z](.*)([@]{1})(.{1,})(\\.)(.{1,})".toRegex() 37 | return email.matches(emailRegex) 38 | } 39 | 40 | fun sendFeedback(email: String, message: String, callId: String, coroutineScope: CoroutineScope = MainScope(), onFinished: (isError: Boolean) -> Unit) { 41 | coroutineScope.launch(Dispatchers.IO) { 42 | val error = try { 43 | val response = sendFeedbackInternal(email, message, callId) 44 | when (response.code) { 45 | 204 -> false 46 | else -> true 47 | } 48 | } catch (e: Exception) { 49 | true 50 | } 51 | onFinished(error) 52 | } 53 | } 54 | 55 | private fun sendFeedbackInternal(email: String, message: String, callId: String): Response { 56 | val url = URL("https://getstream.io/api/crm/video_feedback/") 57 | val formData = MultipartBody.Builder().apply { 58 | addFormDataPart("email", email) 59 | addFormDataPart("message", message) 60 | addFormDataPart("page_url", "https://www.getstream.io?meeting=true&id=$callId") 61 | setType(MultipartBody.FORM) 62 | } 63 | val request = Request.Builder() 64 | .url(url) 65 | .post(formData.build()) 66 | .header("User-Agent", "StreamDemoApp-Android/1.0.0") 67 | .header("Connection", "keep-alive") 68 | .header("Accept", "application/json") 69 | .header("Content-Type", "application/json") 70 | .build() 71 | 72 | return client.newCall(request).execute() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/util/LockOrientation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.util 18 | 19 | import android.app.Activity 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.DisposableEffect 22 | import androidx.compose.ui.platform.LocalContext 23 | 24 | @Composable 25 | fun LockScreenOrientation(orientation: Int) { 26 | val context = LocalContext.current 27 | DisposableEffect(Unit) { 28 | val activity = context as? Activity ?: return@DisposableEffect onDispose {} 29 | val originalOrientation = activity.requestedOrientation 30 | activity.requestedOrientation = orientation 31 | onDispose { 32 | // restore original orientation when view disappears 33 | activity.requestedOrientation = originalOrientation 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/util/NetworkMonitor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.util 18 | 19 | import android.content.Context 20 | import android.net.ConnectivityManager 21 | import android.net.Network 22 | import android.net.NetworkCapabilities 23 | import io.getstream.log.taggedLogger 24 | import kotlinx.coroutines.flow.MutableStateFlow 25 | import kotlinx.coroutines.flow.asStateFlow 26 | 27 | class NetworkMonitor(context: Context) { 28 | private val connectivityManager = context.getSystemService( 29 | Context.CONNECTIVITY_SERVICE, 30 | ) as ConnectivityManager 31 | private val logger by taggedLogger("NetworkMonitor") 32 | 33 | private val _isNetworkAvailable = MutableStateFlow(true) 34 | val isNetworkAvailable = _isNetworkAvailable.asStateFlow() 35 | 36 | init { 37 | // Check initial network state 38 | _isNetworkAvailable.value = connectivityManager.getNetworkCapabilities( 39 | connectivityManager.activeNetwork, 40 | )?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true 41 | 42 | connectivityManager.registerDefaultNetworkCallback(getNetworkCallback()) 43 | } 44 | 45 | private fun getNetworkCallback() = object : ConnectivityManager.NetworkCallback() { 46 | override fun onAvailable(network: Network) { 47 | super.onAvailable(network) 48 | _isNetworkAvailable.value = true 49 | logger.i { "Network available" } 50 | } 51 | 52 | override fun onLost(network: Network) { 53 | super.onLost(network) 54 | _isNetworkAvailable.value = false 55 | logger.i { "Network lost" } 56 | } 57 | 58 | override fun onUnavailable() { 59 | super.onUnavailable() 60 | _isNetworkAvailable.value = false 61 | logger.i { "Network unavailable" } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/util/UserHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.util 18 | 19 | import java.util.Locale 20 | 21 | object UserHelper { 22 | fun generateRandomString(length: Int = 8, upperCaseOnly: Boolean = false): String { 23 | val allowedChars: List = ('A'..'Z') + ('0'..'9') + if (!upperCaseOnly) { 24 | ('a'..'z') 25 | } else { 26 | emptyList() 27 | } 28 | 29 | return (1..length) 30 | .map { allowedChars.random() } 31 | .joinToString("") 32 | } 33 | 34 | fun getUserIdFromEmail(email: String) = email 35 | .replace(" ", "") 36 | .replace(".", "") 37 | .replace("@", "") 38 | 39 | fun getFullNameFromEmail(email: String) = email 40 | .split("@") 41 | .first() 42 | .let { 43 | val parts = it.split(".") 44 | val firstName = parts[0].capitalize(Locale.getDefault()) 45 | val lastName = parts.getOrNull(1)?.capitalize(Locale.getDefault())?.let { lastName -> " $lastName" } ?: "" 46 | 47 | "$firstName$lastName" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/util/config/AppConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.util.config 18 | 19 | import android.content.Context 20 | import android.content.Context.MODE_PRIVATE 21 | import android.content.SharedPreferences 22 | import android.net.Uri 23 | import androidx.core.content.edit 24 | import com.squareup.moshi.JsonAdapter 25 | import com.squareup.moshi.Moshi 26 | import com.squareup.moshi.adapter 27 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 28 | import io.getstream.android.video.chat.compose.util.config.types.StreamEnvironment 29 | import io.getstream.log.taggedLogger 30 | import kotlinx.coroutines.CoroutineScope 31 | import kotlinx.coroutines.DelicateCoroutinesApi 32 | import kotlinx.coroutines.GlobalScope 33 | import kotlinx.coroutines.flow.MutableStateFlow 34 | import kotlinx.coroutines.launch 35 | 36 | /** 37 | * Main entry point for remote / local configuration 38 | */ 39 | @OptIn(ExperimentalStdlibApi::class) 40 | object AppConfig { 41 | // Constants 42 | private val logger by taggedLogger("RemoteConfig") 43 | private const val SHARED_PREF_NAME = "stream_demo_app" 44 | private const val SELECTED_ENV = "selected_env_v2" 45 | 46 | // Data 47 | private lateinit var environment: StreamEnvironment 48 | private lateinit var prefs: SharedPreferences 49 | 50 | // State of config values 51 | val currentEnvironment = MutableStateFlow(null) 52 | val availableEnvironments = listOf( 53 | StreamEnvironment( 54 | env = "pronto", 55 | aliases = listOf("stream-calls-dogfood"), 56 | displayName = "Pronto", 57 | sharelink = "https://pronto.getstream.io/join/", 58 | ), 59 | StreamEnvironment( 60 | env = "demo", 61 | aliases = listOf(""), 62 | displayName = "Demo", 63 | sharelink = "https://getstream.io/video/demos/join/", 64 | ), 65 | StreamEnvironment( 66 | env = "staging", 67 | aliases = emptyList(), 68 | displayName = "Staging", 69 | sharelink = "https://staging.getstream.io/join/", 70 | ), 71 | ) 72 | 73 | // Utilities 74 | private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() 75 | 76 | // API 77 | /** 78 | * Setup the remote configuration. 79 | * Will automatically put config into [AppConfig.config] 80 | * 81 | * @param context an android context. 82 | * @param coroutineScope the scope used to run [onLoaded] 83 | */ 84 | @OptIn(DelicateCoroutinesApi::class) 85 | fun load( 86 | context: Context, 87 | coroutineScope: CoroutineScope = GlobalScope, 88 | onLoaded: suspend () -> Unit = {}, 89 | ) { 90 | // Load prefs 91 | prefs = context.getSharedPreferences(SHARED_PREF_NAME, MODE_PRIVATE) 92 | try { 93 | val jsonAdapter: JsonAdapter = moshi.adapter() 94 | val selectedEnvData = prefs.getString(SELECTED_ENV, null) 95 | val selectedEnvironment = selectedEnvData?.let { 96 | jsonAdapter.fromJson(it) 97 | } 98 | val which = selectedEnvironment ?: availableEnvironments[0] 99 | selectEnv(which) 100 | currentEnvironment.value = which 101 | coroutineScope.launch { 102 | onLoaded() 103 | } 104 | } catch (e: Exception) { 105 | logger.e(e) { "Failed to parse remote config. Deeplinks not working!" } 106 | } 107 | } 108 | 109 | /** 110 | * Select environment. Must be one of [StreamRemoteConfig.environments]. 111 | * 112 | * @param which environment to select 113 | */ 114 | fun selectEnv(which: StreamEnvironment) { 115 | val jsonAdapter: JsonAdapter = moshi.adapter() 116 | // Select default environment from config if none is in prefs 117 | environment = which 118 | // Update selected env 119 | prefs.edit(commit = true) { 120 | putString(SELECTED_ENV, jsonAdapter.toJson(environment)) 121 | } 122 | currentEnvironment.value = environment 123 | } 124 | 125 | fun List.fromUri(env: Uri): StreamEnvironment? { 126 | val environmentName = env.extractEnvironment() 127 | return environmentName?.let { name -> 128 | firstOrNull { streamEnv -> 129 | streamEnv.env == name || streamEnv.aliases.contains(name) 130 | } 131 | } 132 | } 133 | 134 | private fun Uri?.extractEnvironment(): String? { 135 | // Extract the host from the Uri 136 | val host = this?.host ?: return null 137 | // Split the host by "." and return the first part 138 | val parts = host.split(".") 139 | // 0 | 1 | 2 140 | // | getstream | io 141 | // pronto | getstream | io 142 | // stream-call-dogfood | vercel | app 143 | return if (parts.size > 2) parts[0] else "" 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/util/config/types/StreamEnvironment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.util.config.types 18 | 19 | import androidx.annotation.Keep 20 | import com.squareup.moshi.Json 21 | 22 | @Keep 23 | data class StreamEnvironment( 24 | @Json(name = "env") var env: String, 25 | @Json(name = "aliases") var aliases: List = emptyList(), 26 | @Json(name = "displayName") var displayName: String, 27 | @Json(name = "sharelink") var sharelink: String? = null, 28 | ) 29 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/util/filters/SampleAudioFilter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.util.filters 18 | 19 | import java.nio.ByteBuffer 20 | import java.nio.ByteOrder 21 | 22 | object SampleAudioFilter { 23 | 24 | fun toRoboticVoice(audioBuffer: ByteBuffer, numChannels: Int, pitchShiftFactor: Float) { 25 | require(pitchShiftFactor > 0) { "Pitch shift factor must be greater than 0." } 26 | 27 | val inputBuffer = audioBuffer.duplicate() 28 | inputBuffer.order( 29 | ByteOrder.LITTLE_ENDIAN, 30 | ) // Set byte order for correct handling of PCM data 31 | 32 | val numSamples = inputBuffer.remaining() / 2 // Assuming 16-bit PCM audio 33 | 34 | val outputBuffer = ByteBuffer.allocate(inputBuffer.capacity()) 35 | outputBuffer.order(ByteOrder.LITTLE_ENDIAN) 36 | 37 | for (channel in 0 until numChannels) { 38 | val channelBuffer = ShortArray(numSamples) 39 | inputBuffer.asShortBuffer().get(channelBuffer) 40 | 41 | for (i in 0 until numSamples) { 42 | val originalIndex = (i * pitchShiftFactor).toInt() 43 | 44 | if (originalIndex >= 0 && originalIndex < numSamples) { 45 | outputBuffer.putShort(channelBuffer[originalIndex]) 46 | } else { 47 | // Fill with silence if the index is out of bounds 48 | outputBuffer.putShort(0) 49 | } 50 | } 51 | } 52 | 53 | outputBuffer.flip() 54 | audioBuffer.clear() 55 | audioBuffer.put(outputBuffer) 56 | audioBuffer.flip() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/getstream/android/video/chat/compose/util/filters/SampleVideoFilter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. 3 | * 4 | * Licensed under the Stream License; 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://github.com/GetStream/stream-video-android/blob/main/LICENSE 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.android.video.chat.compose.util.filters 18 | 19 | import android.graphics.Bitmap 20 | import android.graphics.Canvas 21 | import android.graphics.ColorMatrix 22 | import android.graphics.ColorMatrixColorFilter 23 | import android.graphics.Paint 24 | 25 | object SampleVideoFilter { 26 | 27 | fun toGrayscale(bmpOriginal: Bitmap) { 28 | val c = Canvas(bmpOriginal) 29 | val paint = Paint() 30 | val cm = ColorMatrix() 31 | cm.setSaturation(0f) 32 | val f = ColorMatrixColorFilter(cm) 33 | paint.colorFilter = f 34 | c.drawBitmap(bmpOriginal, 0f, 0f, paint) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/amsterdam1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/drawable/amsterdam1.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/amsterdam2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/drawable/amsterdam2.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/boulder1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/drawable/boulder1.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/boulder2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/drawable/boulder2.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/feedback_artwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/drawable/feedback_artwork.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/google_button_logo.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 28 | 31 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/drawable/gradient1.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_blur_off.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_blur_on.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_default_avatar.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 26 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 27 | 30 | 33 | 37 | 40 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_layout_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 28 | 31 | 34 | 37 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_layout_spotlight.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_scan_qr.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stream_video_meeting_logo.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 26 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 46 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/stream_calls_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/drawable/stream_calls_logo.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 23 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/android-video-chat/b043317ac0f32b544ea27a8897522f88c49ac990/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | #FFBB86FC 19 | #FF6200EE 20 | #FF3700B3 21 | #FF03DAC5 22 | #FF018786 23 | #FF000000 24 | #FFFFFFFF 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | getstream.io 19 | getstream.io 20 | /video/demos 21 | Join video call 22 | Your login token is expired. Please re-login. 23 | Please sign in with your Google \nStream account. 24 | Google Sign In 25 | Sign In with Email 26 | Random User Sign In 27 | Having problems logging in?  28 | Contact us 29 | Sign out 30 | Start a new call, join a meeting by \nentering the call ID or by scanning \na QR code. 31 | Join Call 32 | Scan QR meeting code 33 | You are about to join a call. %d more people are in the call. 34 | Don\'t have a Call ID? 35 | Call ID 36 | Start a New Call 37 | Start Call 38 | Call ID Number 39 | Stream Video 40 | Try out a video call in this demo powered by Stream\'s video SDK 41 | Cannot load user list.\nPlease try again or re-login into the app. 42 | Direct Call 43 | Select users and tap the call button below 44 | Enter your e-mail address 45 | Google sign in not finalized, please try again 46 | Are you sure you want to sign out? 47 | Cancel 48 | To continue into the call\nScan the QR Code 49 | Scan QR Code 50 | Restart App 51 | Error Log 52 | Log messages copied to clipboard! 53 | App has been updated 54 | Please consider installing the update.\nIt contains important features or bug fixes. 55 | App update failed. Try again later 56 | You are offline. Check your internet connection. 57 | 58 | %s is typing 59 | %s and %d more are typing 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.diffplug.gradle.spotless.SpotlessExtension 2 | import com.diffplug.spotless.kotlin.KotlinConstants 3 | 4 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 5 | plugins { 6 | alias(libs.plugins.android.application) apply false 7 | alias(libs.plugins.kotlin.android) apply false 8 | alias(libs.plugins.kotlin.serialization) apply false 9 | alias(libs.plugins.ksp) apply false 10 | alias(libs.plugins.compose.compiler) apply false 11 | alias(libs.plugins.dokka) apply false 12 | alias(libs.plugins.spotless) apply false 13 | alias(libs.plugins.google.gms) apply false 14 | alias(libs.plugins.firebase.crashlytics) apply false 15 | alias(libs.plugins.hilt) apply false 16 | alias(libs.plugins.baseline.profile) apply false 17 | } 18 | 19 | subprojects { 20 | apply(plugin = rootProject.libs.plugins.spotless.get().pluginId) 21 | 22 | extensions.configure { 23 | kotlin { 24 | target("**/*.kt") 25 | targetExclude( 26 | "**/build/**/*.kt", // Build directory 27 | "**/org/openapitools/client/**/*.kt" // OpenAPI generated code 28 | ) 29 | ktlint() 30 | .editorConfigOverride( 31 | mapOf( 32 | "ktlint_standard_max-line-length" to "disabled" 33 | ) 34 | ) 35 | trimTrailingWhitespace() 36 | endWithNewline() 37 | licenseHeaderFile(rootProject.file("$rootDir/spotless/copyright.kt")) 38 | } 39 | format("openapiGenerated") { 40 | target("**/*.kt") 41 | targetExclude("**/build/**/*.kt") 42 | trimTrailingWhitespace() 43 | endWithNewline() 44 | licenseHeaderFile( 45 | rootProject.file("$rootDir/spotless/copyright.kt"), 46 | KotlinConstants.LICENSE_HEADER_DELIMITER 47 | ) 48 | } 49 | format("kts") { 50 | target("**/*.kts") 51 | targetExclude("**/build/**/*.kts") 52 | // Look for the first line that doesn't have a block comment (assumed to be the license) 53 | licenseHeaderFile( 54 | rootProject.file("spotless/copyright.kts"), 55 | "(^(?![\\/ ]\\*).*$)" 56 | ) 57 | } 58 | format("xml") { 59 | target("**/*.xml") 60 | targetExclude("**/build/**/*.xml") 61 | // Look for the first XML tag that isn't a comment ( 17 | --------------------------------------------------------------------------------