├── .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