├── settings.gradle
├── chat-demo-android
├── lint.xml
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values-v11
│ │ │ │ └── styles.xml
│ │ │ ├── values-v14
│ │ │ │ └── styles.xml
│ │ │ ├── values
│ │ │ │ ├── styles.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ └── strings.xml
│ │ │ ├── drawable-mdpi
│ │ │ │ ├── avatar1.png
│ │ │ │ ├── avatar2.png
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_menu_stop.png
│ │ │ │ ├── ic_notification.png
│ │ │ │ ├── ic_action_settings.png
│ │ │ │ ├── reachability_online.png
│ │ │ │ ├── ic_action_content_add.png
│ │ │ │ ├── ic_menu_info_details.png
│ │ │ │ ├── reachability_disabled.png
│ │ │ │ ├── reachability_offline.png
│ │ │ │ └── reachability_notifiable.png
│ │ │ ├── drawable-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_menu_stop.png
│ │ │ │ ├── ic_action_settings.png
│ │ │ │ ├── ic_action_content_add.png
│ │ │ │ └── ic_menu_info_details.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ ├── bubble_a.9.png
│ │ │ │ ├── bubble_b.9.png
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_menu_stop.png
│ │ │ │ ├── ic_action_settings.png
│ │ │ │ ├── ic_menu_info_details.png
│ │ │ │ └── ic_action_content_add.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_menu_stop.png
│ │ │ │ ├── ic_action_settings.png
│ │ │ │ ├── ic_action_content_add.png
│ │ │ │ └── ic_menu_info_details.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_action_content_add.png
│ │ │ ├── layout
│ │ │ │ ├── member_list.xml
│ │ │ │ ├── member_item_layout.xml
│ │ │ │ ├── activity_channel.xml
│ │ │ │ ├── activity_user_info.xml
│ │ │ │ ├── activity_message.xml
│ │ │ │ ├── channel_item_layout.xml
│ │ │ │ ├── activity_login.xml
│ │ │ │ └── message_item_layout.xml
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ └── menu
│ │ │ │ ├── login.xml
│ │ │ │ ├── message.xml
│ │ │ │ └── channel.xml
│ │ ├── assets
│ │ │ └── fonts
│ │ │ │ ├── Dosis-Light.ttf
│ │ │ │ ├── Dosis-Regular.ttf
│ │ │ │ ├── OpenSans-Light.ttf
│ │ │ │ ├── Raleway-Light.ttf
│ │ │ │ ├── Raleway-Thin.ttf
│ │ │ │ ├── Dosis-ExtraLight.ttf
│ │ │ │ ├── OpenSans-Regular.ttf
│ │ │ │ ├── AlegreyaSans-Light.ttf
│ │ │ │ ├── AlegreyaSans-Thin.ttf
│ │ │ │ ├── PT_Sans-Web-Regular.ttf
│ │ │ │ └── AlegreyaSans-Regular.ttf
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── twilio
│ │ │ │ └── chat
│ │ │ │ └── demo
│ │ │ │ ├── models
│ │ │ │ └── Media.kt
│ │ │ │ ├── FCMPreferences.kt
│ │ │ │ ├── Constants.kt
│ │ │ │ ├── utils
│ │ │ │ ├── ChatClientExtensions.kt
│ │ │ │ └── callbacks.kt
│ │ │ │ ├── views
│ │ │ │ ├── MessageTextView.kt
│ │ │ │ ├── MemberViewHolder.kt
│ │ │ │ ├── ChannelViewHolder.kt
│ │ │ │ └── MessageViewHolder.kt
│ │ │ │ ├── services
│ │ │ │ ├── FCMInstanceIDService.kt
│ │ │ │ ├── RegistrationIntentService.kt
│ │ │ │ ├── FCMListenerService.kt
│ │ │ │ └── MediaService.kt
│ │ │ │ ├── HttpHelper.kt
│ │ │ │ ├── TwilioApplication.kt
│ │ │ │ ├── ChannelModel.kt
│ │ │ │ ├── activities
│ │ │ │ ├── LoginActivity.kt
│ │ │ │ ├── UserActivity.kt
│ │ │ │ ├── ChannelActivity.kt
│ │ │ │ └── MessageActivity.kt
│ │ │ │ └── BasicChatClient.kt
│ │ └── AndroidManifest.xml
│ └── release
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── ic_launcher-web.png
├── project.properties
├── proguard-rules.pro
├── proguard-project.txt
└── build.gradle
├── SimulateCrashMenu.png
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── REPORT_BUGS.md
├── LICENSE
├── .github
├── ISSUE_TEMPLATE.md
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .clang-format
├── gradlew.bat
├── README.md
└── gradlew
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':chat-demo-android'
2 |
--------------------------------------------------------------------------------
/chat-demo-android/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/values-v11/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/values-v14/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/SimulateCrashMenu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/SimulateCrashMenu.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4096m
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/chat-demo-android/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/ic_launcher-web.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 | - #FFCC0000
3 |
4 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/Dosis-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/Dosis-Light.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/Dosis-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/Dosis-Regular.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/OpenSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/OpenSans-Light.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/Raleway-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/Raleway-Light.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/Raleway-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/Raleway-Thin.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/avatar1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/avatar1.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/avatar2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/avatar2.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/Dosis-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/Dosis-ExtraLight.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xhdpi/bubble_a.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xhdpi/bubble_a.9.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xhdpi/bubble_b.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xhdpi/bubble_b.9.png
--------------------------------------------------------------------------------
/chat-demo-android/src/release/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Chat Quickstart
4 |
5 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/AlegreyaSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/AlegreyaSans-Light.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/AlegreyaSans-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/AlegreyaSans-Thin.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/PT_Sans-Web-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/PT_Sans-Web-Regular.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-hdpi/ic_menu_stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-hdpi/ic_menu_stop.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/ic_menu_stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/ic_menu_stop.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xhdpi/ic_menu_stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xhdpi/ic_menu_stop.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xxhdpi/ic_menu_stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xxhdpi/ic_menu_stop.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/assets/fonts/AlegreyaSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/assets/fonts/AlegreyaSans-Regular.ttf
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/ic_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/ic_notification.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-hdpi/ic_action_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-hdpi/ic_action_settings.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/ic_action_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/ic_action_settings.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/reachability_online.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/reachability_online.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xhdpi/ic_action_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xhdpi/ic_action_settings.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-hdpi/ic_action_content_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-hdpi/ic_action_content_add.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-hdpi/ic_menu_info_details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-hdpi/ic_menu_info_details.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/ic_action_content_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/ic_action_content_add.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/ic_menu_info_details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/ic_menu_info_details.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/reachability_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/reachability_disabled.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/reachability_offline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/reachability_offline.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xhdpi/ic_menu_info_details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xhdpi/ic_menu_info_details.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xxhdpi/ic_action_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xxhdpi/ic_action_settings.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-mdpi/reachability_notifiable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-mdpi/reachability_notifiable.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xhdpi/ic_action_content_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xhdpi/ic_action_content_add.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xxhdpi/ic_action_content_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xxhdpi/ic_action_content_add.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xxhdpi/ic_menu_info_details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xxhdpi/ic_menu_info_details.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/drawable-xxxhdpi/ic_action_content_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/twilio-chat-demo-android/HEAD/chat-demo-android/src/main/res/drawable-xxxhdpi/ic_action_content_add.png
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/models/Media.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.models
2 |
3 | import java.io.InputStream
4 |
5 | data class Media(val name: String, val type: String, val stream: InputStream)
6 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/FCMPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo
2 |
3 | object FCMPreferences {
4 | val SENT_TOKEN_TO_SERVER = "sentTokenToServer"
5 | val REGISTRATION_COMPLETE = "registrationComplete"
6 | }
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 16dp
5 | 16dp
6 |
7 |
8 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/layout/member_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo
2 |
3 | interface Constants {
4 | companion object {
5 | /** Key into an Intent's extras data that points to a [Channel] object. */
6 | val EXTRA_CHANNEL = "com.twilio.chat.Channel"
7 | /** Key into an Intent's extras data that contains Channel SID. */
8 | val EXTRA_CHANNEL_SID = "C_SID"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 | 64dp
9 |
10 |
11 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/menu/login.xml:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/menu/message.xml:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/utils/ChatClientExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.utils
2 |
3 | import com.twilio.chat.ChatClient
4 |
5 | /** Extension function to simulate native crash */
6 | fun ChatClient.simulateCrash(where: Where) {
7 | val method = ChatClient::class.java.getDeclaredMethod("simulateCrash", Int::class.java)
8 | method.isAccessible = true
9 | method.invoke(this, where.value)
10 | }
11 |
12 | enum class Where(val value: Int) {
13 | CHAT_CLIENT_CPP(1),
14 | TM_CLIENT_CPP(2)
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Built application files
4 | *.apk
5 | *.ap_
6 |
7 | # Files for the Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 |
15 | # Gradle files
16 | .gradle/
17 | build/
18 | output/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 | .idea/
32 | *.iml
33 |
34 | # Android Studio captures folder
35 | captures/
36 |
--------------------------------------------------------------------------------
/chat-demo-android/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-23
15 | android.library=false
16 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/views/MessageTextView.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.views
2 |
3 | import android.content.Context
4 | import android.graphics.Typeface
5 | import android.util.AttributeSet
6 | import android.widget.TextView
7 |
8 | class MessageTextView(context: Context, attrs: AttributeSet) : TextView(context, attrs) {
9 |
10 | init {
11 | var font: String? = attrs.getAttributeValue(androidNS, "fontFamily")
12 | if (font == null) {
13 | font = "AlegreyaSans-Light"
14 | }
15 | this.typeface = Typeface.createFromAsset(context.assets, "fonts/$font.ttf")
16 | }
17 |
18 | companion object {
19 | private val androidNS = "http://schemas.android.com/apk/res/android"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/layout/member_item_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/services/FCMInstanceIDService.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.services
2 |
3 | import com.google.firebase.messaging.FirebaseMessagingService
4 | import org.jetbrains.anko.AnkoLogger
5 | import org.jetbrains.anko.debug
6 | import org.jetbrains.anko.startService
7 |
8 | class FCMInstanceIDService : FirebaseMessagingService(), AnkoLogger {
9 |
10 | /**
11 | * Called if InstanceID token is updated. This may occur if the security of
12 | * the previous token had been compromised. This call is initiated by the
13 | * InstanceID provider.
14 | */
15 | override fun onNewToken(token: String) {
16 | debug { "onNewToken" }
17 |
18 | // Fetch updated Instance ID token and notify our app's server of any changes.
19 | startService()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/layout/activity_channel.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/views/MemberViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.views
2 |
3 | import android.content.Context
4 | import com.twilio.chat.demo.R
5 | import com.twilio.chat.Member
6 | import android.view.ViewGroup
7 | import android.widget.TextView
8 | import kotterknife.bindView
9 | import eu.inloop.simplerecycleradapter.SettableViewHolder
10 |
11 | class MemberViewHolder : SettableViewHolder {
12 | private val memberIdentity: TextView by bindView(R.id.identity)
13 | // private val memberSid: TextView by bindView(R.id.member_sid)
14 |
15 | constructor(context: Context, parent: ViewGroup)
16 | : super(context, R.layout.member_item_layout, parent)
17 | {}
18 |
19 | override fun setData(member: Member) {
20 | memberIdentity.text = member.identity
21 | // memberSid.text = member.sid
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/REPORT_BUGS.md:
--------------------------------------------------------------------------------
1 | 1. Enable debug log
2 |
3 | ```
4 | // Before creating a new ChatClient with ChatClient.create() add this line:
5 | ChatClient.setLogLevel(android.util.Log.VERBOSE);
6 | ...
7 |
8 | ChatClient.create(....
9 | ```
10 |
11 | 2. Run your application and reproduce the problem
12 |
13 | 3. Capture entire device log using
14 |
15 | ```
16 | adb logcat -d > my_error_log
17 | ```
18 |
19 | 4. Send my_error_log together with the problem description to Twilio
20 |
21 | 5. YES, we need the entire log to reconstruct order of events on the client side.
22 |
23 | 6. NO, only sending the lines with
24 | ```
25 | Native thread exiting without having called DetachCurrentThread (maybe it's going to use a pthread_key_create destructor?): Thread[28,tid=22833,Native,Thread*=0xeec28600,peer=0x132c4a40,"EventThread - 2 - 22833"]
26 | ```
27 | will not help - those are harmless and accounted for.
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Twilio
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/chat-demo-android/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | #==============================================
2 | # Proguard rules for use with IP Messaging SDK
3 | #==============================================
4 |
5 | -keep class com.twilio.chat.** { *; }
6 | -keepattributes InnerClasses
7 | #-keep interface com.twilio.chat.** { *; }
8 | #-keep enum com.twilio.chat.** { *; }
9 |
10 | ## Keep native methods
11 |
12 | -keepclasseswithmembernames class com.twilio.chat.** {
13 | native ;
14 | }
15 |
16 | ## Keep callbacks from native
17 | # ?
18 |
19 | #======================================
20 | # Local demo application configuration
21 | #======================================
22 |
23 | -keepclassmembers class **.R$* {
24 | public static ;
25 | }
26 |
27 | ## EasyAdapter
28 |
29 | -dontwarn uk.co.ribot.easyadapter.**
30 | -keepattributes *Annotation*
31 | -keepclassmembers class * extends uk.co.ribot.easyadapter.ItemViewHolder {
32 | public (...);
33 | }
34 |
35 | ## Google libraries
36 |
37 | -dontwarn android.support.**
38 | -keep class com.google.ads.** { *; }
39 | -keep class com.google.android.gms.** { *; }
40 | -keep class com.google.firebase.** { *; }
41 | -keepattributes Signature
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 | > Before filing an issue please check that the issue is not already addressed by the following:
3 | > * [Github Issues](https://github.com/twilio/twilio-chat-demo-android/issues)
4 | > * [Changelog](https://www.twilio.com/docs/api/chat/changelogs/android)
5 |
6 | ### Description
7 |
8 | [Description of the issue]
9 |
10 | ### Steps to Reproduce
11 |
12 | 1. [Step one]
13 | 2. [Step two]
14 | 3. [Insert as many steps as needed]
15 |
16 | #### Code (optional)
17 |
18 | ```java
19 | // Code that help reproduce the issue
20 | ```
21 |
22 | #### Expected Behavior
23 |
24 | [What you expect to happen]
25 |
26 | #### Actual Behavior
27 |
28 | [What actually happens]
29 |
30 | #### Reproduces how Often
31 |
32 | [What percentage of the time does it reproduce?]
33 |
34 | #### Logs
35 |
36 | Please collect logs as described [here](https://github.com/twilio/twilio-chat-demo-android/blob/master/REPORT_BUGS.md). Full unedited logs help reproduce and fix issues faster.
37 |
38 | ```
39 | // Log output when issue occurs
40 | ```
41 |
42 | Or attach it as a file.
43 |
44 | ### Versions
45 |
46 | All relevant version information for issue.
47 |
48 | #### Chat Android SDK
49 |
50 | [eg. 2.0.8]
51 |
52 | #### Android API
53 |
54 | [eg. 18]
55 |
56 | #### Android Device
57 |
58 | [eg. Nexus 5]
59 |
--------------------------------------------------------------------------------
/chat-demo-android/proguard-project.txt:
--------------------------------------------------------------------------------
1 | # To enable ProGuard in your project, edit project.properties
2 | # to define the proguard.config property as described in that file.
3 | #
4 | # Add project specific ProGuard rules here.
5 | # By default, the flags in this file are appended to flags specified
6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt
7 | # You can edit the include path and order by changing the ProGuard
8 | # include property in project.properties.
9 | #
10 | # For more details, see
11 | # http://developer.android.com/guide/developing/tools/proguard.html
12 |
13 | # Add any project specific keep options here:
14 |
15 | # If your project uses WebView with JS, uncomment the following
16 | # and specify the fully qualified class name to the JavaScript interface
17 | # class:
18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
19 | # public *;
20 | #}
21 | -keep class * extends java.util.ListResourceBundle {
22 | protected Object[][] getContents();
23 | }
24 |
25 | -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {
26 | public static final *** NULL;
27 | }
28 |
29 | -keepnames @com.google.android.gms.common.annotation.KeepName class *
30 | -keepclassmembernames class * {
31 | @com.google.android.gms.common.annotation.KeepName *;
32 | }
33 |
34 | -keepnames class * implements android.os.Parcelable {
35 | public static final ** CREATOR;
36 | }
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/utils/callbacks.kt:
--------------------------------------------------------------------------------
1 | import com.twilio.chat.CallbackListener
2 | import com.twilio.chat.ErrorInfo
3 | import com.twilio.chat.StatusListener
4 | import com.twilio.chat.demo.TwilioApplication
5 |
6 | typealias SuccessStatus = () -> Unit
7 | typealias SuccessCallback = (T) -> Unit
8 | typealias ErrorCallback = (error: ErrorInfo) -> Unit
9 |
10 | class ChatCallbackListener(val fail: ErrorCallback = {},
11 | val success: SuccessCallback = {}) : CallbackListener() {
12 |
13 | override fun onSuccess(p0: T) = success(p0)
14 |
15 | override fun onError(err: ErrorInfo) {
16 | TwilioApplication.instance.showError(err)
17 | fail(err)
18 | }
19 | }
20 |
21 | open class ChatStatusListener(val fail: ErrorCallback = {},
22 | val success: SuccessStatus = {}) : StatusListener() {
23 |
24 | override fun onSuccess() = success()
25 |
26 | override fun onError(err: ErrorInfo) {
27 | TwilioApplication.instance.showError(err)
28 | fail(err)
29 | }
30 | }
31 |
32 |
33 | /**
34 | * Status listener that shows a toast with operation results.
35 | */
36 | class ToastStatusListener(val okText: String, val errorText: String, fail: ErrorCallback = {},
37 | success: SuccessStatus = {}) : ChatStatusListener(fail, success) {
38 |
39 | override fun onSuccess() {
40 | TwilioApplication.instance.showToast(okText)
41 | success()
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/HttpHelper.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo
2 |
3 | import java.net.HttpURLConnection
4 | import java.net.URL
5 | import org.json.JSONObject
6 | import org.json.JSONException
7 | import org.jetbrains.anko.*
8 |
9 | object HttpHelper : AnkoLogger {
10 | @Throws(Exception::class)
11 | fun httpGet(url: String): String {
12 | val urlObj = URL(url)
13 |
14 | val conn = urlObj.openConnection() as HttpURLConnection
15 |
16 | conn.connectTimeout = 45000
17 | conn.readTimeout = 30000
18 | conn.doInput = true
19 |
20 | val responseCode = conn.responseCode
21 |
22 | if (responseCode == HttpURLConnection.HTTP_OK) {
23 | // Try to get access token from "token" field in the JSON format response
24 | // If response cannot be parsed as JSON, use it as-is.
25 |
26 | var accessToken = conn.inputStream.reader().readText()
27 | conn.inputStream.close()
28 | conn.disconnect()
29 |
30 | try {
31 | JSONObject(accessToken).apply {
32 | accessToken = this.getString("token")
33 | }
34 | } catch (xcp: JSONException) {
35 | // Do nothing
36 | }
37 |
38 | info { "Received Token: ${accessToken}" }
39 |
40 | return accessToken
41 | } else {
42 | conn.disconnect()
43 | throw Exception("Got error code $responseCode from server")
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/.clang-format:
--------------------------------------------------------------------------------
1 | ---
2 | BasedOnStyle: Mozilla
3 | IndentWidth: 4
4 | TabWidth: 4
5 | ContinuationIndentWidth: 4
6 | ConstructorInitializerIndentWidth: 4
7 | UseTab: Never
8 | ColumnLimit: 100
9 | ---
10 | Language: Java
11 | AllowShortBlocksOnASingleLine: false
12 | AlignEscapedNewlinesLeft: true
13 | AlignAfterOpenBracket: true
14 | AlignConsecutiveAssignments: false
15 | AlignConsecutiveDeclarations: true
16 | AllowAllParametersOfDeclarationOnNextLine: true
17 | AllowShortFunctionsOnASingleLine: false
18 | AllowShortIfStatementsOnASingleLine: false
19 | AllowShortCaseLabelsOnASingleLine: true
20 | AllowShortLoopsOnASingleLine: false
21 | AlwaysBreakAfterDefinitionReturnType: TopLevel
22 | AlwaysBreakBeforeMultilineStrings: true
23 | AlwaysBreakTemplateDeclarations: true
24 | AlignOperands: true
25 | AlignTrailingComments: true
26 | BinPackArguments: false
27 | BinPackParameters: false
28 | BreakAfterJavaFieldAnnotations: true
29 | BreakBeforeBinaryOperators: NonAssignment
30 | BreakBeforeBraces: Linux
31 | BreakBeforeTernaryOperators: false
32 | ConstructorInitializerAllOnOneLineOrOnePerLine: true
33 | IndentCaseLabels: true
34 | IndentWrappedFunctionNames: false
35 | KeepEmptyLinesAtTheStartOfBlocks: true
36 | MaxEmptyLinesToKeep: 1
37 | NamespaceIndentation: Inner
38 | ReflowComments: true
39 | SortIncludes: true
40 | SpaceAfterCStyleCast: false
41 | SpacesInCStyleCastParentheses: false
42 | SpaceBeforeParens: ControlStatements
43 | SpaceInEmptyParentheses: false
44 | SpacesInAngles: false
45 | SpacesInContainerLiterals: true
46 | SpacesInParentheses: false
47 | SpacesInSquareBrackets: false
48 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/layout/activity_user_info.xml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
18 |
24 |
30 |
31 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/TwilioApplication.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo
2 |
3 | import android.app.Application
4 | import android.os.Handler
5 | import android.os.Looper
6 | import android.view.Gravity
7 | import android.widget.Toast
8 | import com.google.firebase.FirebaseApp
9 | import com.twilio.chat.ErrorInfo
10 | import org.jetbrains.anko.*
11 |
12 | class TwilioApplication : Application(), AnkoLogger {
13 | lateinit var basicClient: BasicChatClient
14 | private set
15 |
16 | override fun onCreate() {
17 | super.onCreate()
18 | FirebaseApp.initializeApp(this)
19 | instance = this
20 | basicClient = BasicChatClient(applicationContext)
21 | }
22 |
23 | @JvmOverloads fun showToast(text: String, duration: Int = Toast.LENGTH_SHORT) {
24 | debug { text }
25 | Handler(Looper.getMainLooper()).post {
26 | val toast = Toast.makeText(applicationContext, text, duration)
27 | toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0)
28 | toast.show()
29 | }
30 | }
31 |
32 | fun showError(error: ErrorInfo) {
33 | showError("Something went wrong", error)
34 | }
35 |
36 | fun showError(message: String, error: ErrorInfo) {
37 | showToast(formatted(message, error), Toast.LENGTH_LONG)
38 | logErrorInfo(message, error)
39 | }
40 |
41 | fun logErrorInfo(message: String, error: ErrorInfo) {
42 | error { formatted(message, error) }
43 | }
44 |
45 | private fun formatted(message: String, error: ErrorInfo): String {
46 | return String.format("%s. %s", message, error.toString())
47 | }
48 |
49 | companion object {
50 | lateinit var instance: TwilioApplication
51 | private set
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | Before filing an issue please check that the issue is not already addressed by the following:
11 | * [Github Issues](https://github.com/twilio/twilio-chat-demo-android/issues)
12 | * [Changelog](https://www.twilio.com/docs/api/chat/changelogs/android)
13 |
14 | Remove the above lines from the template! Fill in ALL fields in the following:
15 |
16 | ### Description
17 |
18 | [Description of the issue]
19 |
20 | ### Steps to Reproduce
21 |
22 | 1. [Step one]
23 | 2. [Step two]
24 | 3. [Insert as many steps as needed]
25 |
26 | #### Code (optional)
27 |
28 | ```java
29 | // Code that help reproduce the issue
30 | ```
31 |
32 | #### Expected Behavior
33 |
34 | [What you expect to happen]
35 |
36 | #### Actual Behavior
37 |
38 | [What actually happens]
39 |
40 | #### Reproduces how Often
41 |
42 | [What percentage of the time does it reproduce?]
43 |
44 | #### Logs
45 |
46 | Please collect logs as described [here](https://github.com/twilio/twilio-chat-demo-android/blob/master/REPORT_BUGS.md). Full unedited logs help reproduce and fix issues faster.
47 |
48 | ```
49 | // Log output when issue occurs
50 | ```
51 |
52 | Or attach it as a file.
53 |
54 | #### Logs
55 |
56 | Yes, if you didn't collect the logs as described above, please collect the logs. Do not file the issue if you don't have enough information, this means having some logs, too.
57 |
58 | #### Logs
59 | #### Logs
60 | #### Logs
61 | #### Logs
62 | #### Logs
63 | #### Logs
64 |
65 | Yes, seriously. Please send logs!
66 |
67 | ### Versions
68 |
69 | All relevant version information for issue, fill in below:
70 |
71 | #### Chat Android SDK
72 |
73 | Which SDK version did you use? [eg. 2.0.8]
74 |
75 | #### Android API
76 |
77 | On which Android API version or versions does it happen? [eg. 18]
78 |
79 | #### Android Device
80 |
81 | Device or devices on which the problem happens [eg. Nexus 5]
82 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/layout/activity_message.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
17 |
18 |
26 |
27 |
33 |
34 |
43 |
44 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/services/RegistrationIntentService.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.services
2 |
3 | import android.app.IntentService
4 | import android.content.Intent
5 | import android.preference.PreferenceManager
6 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
7 | import com.google.firebase.iid.FirebaseInstanceId
8 | import com.twilio.chat.demo.FCMPreferences
9 | import com.twilio.chat.demo.TwilioApplication
10 | import org.jetbrains.anko.*
11 |
12 | /**
13 | * Registration intent handles receiving and updating the FCM token lifecycle events.
14 | */
15 | class RegistrationIntentService : IntentService("RegistrationIntentService"), AnkoLogger {
16 | init {
17 | info { "Started" }
18 | }
19 |
20 | override fun onHandleIntent(intent: Intent?) {
21 | info { "onHandleIntent" }
22 | val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
23 |
24 | try {
25 | val token = FirebaseInstanceId.getInstance().token
26 | info { "FCM Registration Token: ${token!!}" }
27 |
28 | /**
29 | * Persist registration to Twilio servers.
30 | */
31 | TwilioApplication.instance.basicClient.setFCMToken(token!!)
32 |
33 | // You should store a boolean that indicates whether the generated token has been
34 | // sent to your server. If the boolean is false, send the token to your server,
35 | // otherwise your server should have already received the token.
36 | sharedPreferences.edit().putBoolean(FCMPreferences.SENT_TOKEN_TO_SERVER, true).apply()
37 | } catch (e: Exception) {
38 | error { "Failed to complete token refresh: $e" }
39 | // If an exception happens while fetching the new token or updating our registration
40 | // data, this ensures that we'll attempt the update at a later time.
41 | sharedPreferences.edit().putBoolean(FCMPreferences.SENT_TOKEN_TO_SERVER, false).apply()
42 | }
43 |
44 | // Notify UI that registration has completed, so the progress indicator can be hidden.
45 | val registrationComplete = Intent(FCMPreferences.REGISTRATION_COMPLETE)
46 | LocalBroadcastManager.getInstance(this).sendBroadcast(registrationComplete)
47 | }
48 |
49 | companion object {
50 | private val TOPICS = arrayOf("global")
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/layout/channel_item_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
22 |
23 |
30 |
31 |
38 |
39 |
46 |
47 |
54 |
55 |
62 |
63 |
70 |
71 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Chat Debug
4 | Client Name
5 | Log In
6 | Logout Client
7 | Logging in. Please wait...
8 | Invite Member
9 | Channel Invite
10 | You got a Channel Invite. Join it?
11 | Join
12 | Decline
13 | Conversation
14 | Hello world!
15 | Settings
16 | ChannelActivity
17 | MessageActivity
18 | Enter a friendly name
19 | Enter a topic
20 | Update message
21 | Channel Friendly Name
22 | Invite member
23 | Add member
24 | Search Channel by Unique Name
25 | Remove member
26 | Type message
27 | OK
28 | Options
29 | Create public channel
30 | Create private channel
31 | Reg Id:
32 | FCM
33 | Pin certificate bundle
34 | Unregister FCM Token
35 | Create Pub Ch with Options
36 | Create Pri Ch with Options
37 | Search channel by unique name
38 | Update token
39 | TTL (sec)
40 | About
41 | User info activity
42 | Simulate crash
43 | in ChannelActivity.kt
44 | in ChatClient.cpp
45 | in TMClient.cpp
46 | Update message attributes
47 | User avatar
48 | Create
49 | Cancel
50 | Update
51 | Change Topic
52 | Add
53 | Select an option
54 | Create channel
55 | Find channel
56 | Search
57 |
58 | - us1
59 | - ie1
60 |
61 |
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # This repository is deprecated
2 |
3 | The new version of the Chat Demo Application is available in [twilio-chat-demo-kotlin](https://github.com/twilio/twilio-chat-demo-kotlin). It is an example of better app architecture and demonstrates best practices in implementing Twilio-based Chat.
4 |
5 | # Chat Demo Application Overview
6 |
7 | ## This is Kotlin version of the Demo app
8 |
9 | For the Java version you can look [here](https://github.com/twilio/twilio-chat-demo-android/tree/java).
10 |
11 | This demo app SDK version: 
12 |
13 | Latest available SDK version: [  ](https://bintray.com/twilio/releases/chat-android/_latestVersion)
14 |
15 | ## Getting Started
16 |
17 | Welcome to the Chat Demo application. This application demonstrates a basic chat client with the ability to create and join channels, invite other members into the channels and exchange messages.
18 |
19 | What you'll minimally need to get started:
20 |
21 | - A clone of this repository
22 | - [A way to create a Chat Service Instance and generate client tokens](https://www.twilio.com/docs/api/chat/guides/identity)
23 | - Google Play Services library : [Follow the instructions here](https://developers.google.com/android/guides/setup)
24 |
25 | ## Building
26 |
27 | ### Add google-services.json
28 |
29 | [Generate google-services.json](https://support.google.com/firebase/answer/7015592?hl=en) file and place it under `chat-demo-android/`.
30 |
31 | ### Set the value of `ACCESS_TOKEN_SERVICE_URL`
32 |
33 | Set the value of `ACCESS_TOKEN_SERVICE_URL` in `gradle.properties` file to point to a valid Access-Token server.
34 |
35 | Create that file if it doesn't exist with the following contents:
36 |
37 | ```
38 | ACCESS_TOKEN_SERVICE_URL=http://example.com/get-token/
39 | ```
40 |
41 | NOTE: no need for quotes around the URL, they will be added automatically.
42 |
43 | You can also pass this parameter to gradle during build without need to create a properties file, as follows:
44 |
45 | ```
46 | ./gradlew assembleDebug -PACCESS_TOKEN_SERVICE_URL=http://example.com/get-token/
47 | ```
48 |
49 | ### Optionally setup Firebase Crashlytics
50 |
51 | If you want to see crashes reported to crashlytics:
52 | 1. [Set up Crashlytics in the Firebase console](https://firebase.google.com/docs/crashlytics/get-started?platform=android#setup-console)
53 |
54 | 2. In order to see native crashes symbolicated upload symbols into the Firebase console:
55 | ```
56 | ./gradlew chat-demo-android:assembleBUILD_VARIANT
57 | ./gradlew chat-demo-android:uploadCrashlyticsSymbolFileBUILD_VARIANT
58 | ```
59 | for example to upload symbols for `debug` build type run:
60 | ```
61 | ./gradlew chat-demo-android:assembleDebug
62 | ./gradlew chat-demo-android:uploadCrashlyticsSymbolFileDebug
63 | ```
64 |
65 | [Read more](https://firebase.google.com/docs/crashlytics/upgrade-sdk?platform=android#optional_step_set_up_ndk_crash_reporting) about Android NDK crash reports.
66 |
67 | 3. Login into `chat-demo-android` application and navigate to `Menu -> Options -> Simulate crash` in order to check that crashes coming into Firebase console.
68 |
69 | ### Build
70 |
71 | Run `./gradlew assembleDebug` to fetch Twilio SDK files and build application.
72 |
73 | ### Android Studio
74 |
75 | You can import this project into Android Studio and then build as you would ordinarily. The token server setup is still important.
76 |
77 | ### Debug
78 |
79 | Build in debug configuration, this will enable verbose logging.
80 |
81 | ## License
82 |
83 | MIT
84 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
39 |
40 |
41 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
63 |
66 |
67 |
68 |
69 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
81 |
82 |
83 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/views/ChannelViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.views
2 |
3 | import android.content.Context
4 | import com.twilio.chat.demo.ChannelModel
5 | import com.twilio.chat.Channel.ChannelStatus
6 | import com.twilio.chat.Channel.ChannelType
7 | import com.twilio.chat.Channel.NotificationLevel
8 | import com.twilio.chat.CallbackListener
9 | import android.graphics.Color
10 | import android.util.Log
11 | import android.view.ViewGroup
12 | import android.widget.TextView
13 | import com.twilio.chat.Message
14 | import com.twilio.chat.demo.R
15 | import eu.inloop.simplerecycleradapter.SettableViewHolder
16 | import kotterknife.bindView
17 | import org.jetbrains.anko.*
18 |
19 | class ChannelViewHolder : SettableViewHolder, AnkoLogger {
20 | val friendlyName: TextView by bindView(R.id.channel_friendly_name)
21 | val channelSid: TextView by bindView(R.id.channel_sid)
22 | val updatedDate: TextView by bindView(R.id.channel_updated_date)
23 | val createdDate: TextView by bindView(R.id.channel_created_date)
24 | val usersCount: TextView by bindView(R.id.channel_users_count)
25 | val totalMessagesCount: TextView by bindView(R.id.channel_total_messages_count)
26 | val unconsumedMessagesCount: TextView by bindView(R.id.channel_unconsumed_messages_count)
27 | val lastMessageDate: TextView by bindView(R.id.channel_last_message_date)
28 | val pushesLevel: TextView by bindView(R.id.channel_pushes_level)
29 |
30 | constructor(context: Context, parent: ViewGroup)
31 | : super(context, R.layout.channel_item_layout, parent)
32 | {}
33 |
34 | override fun setData(channel: ChannelModel) {
35 | warn { "setData for ${channel.friendlyName} sid|${channel.sid}|" }
36 | friendlyName.text = channel.friendlyName
37 | channelSid.text = channel.sid
38 |
39 | updatedDate.text = if (channel.dateUpdatedAsDate != null)
40 | channel.dateUpdatedAsDate!!.toString()
41 | else
42 | ""
43 |
44 | createdDate.text = if (channel.dateCreatedAsDate != null)
45 | channel.dateCreatedAsDate!!.toString()
46 | else
47 | ""
48 |
49 | pushesLevel.text = if (channel.notificationLevel == NotificationLevel.MUTED)
50 | "Pushes: Muted"
51 | else
52 | "Pushes: Default"
53 |
54 | channel.getUnconsumedMessagesCount(object : CallbackListener() {
55 | override fun onSuccess(value: Long?) {
56 | Log.d("ChannelViewHolder", "getUnconsumedMessagesCount callback")
57 | unconsumedMessagesCount.text = "Unread ${value ?: "all"}"
58 | }
59 | })
60 |
61 | channel.getMessagesCount(object : CallbackListener() {
62 | override fun onSuccess(value: Long) {
63 | Log.d("ChannelViewHolder", "getMessagesCount callback")
64 | totalMessagesCount.text = "Messages $value"
65 | }
66 | })
67 |
68 | channel.getMembersCount(object : CallbackListener() {
69 | override fun onSuccess(value: Long) {
70 | Log.d("ChannelViewHolder", "getMembersCount callback")
71 | usersCount.text = "Members $value"
72 | }
73 | })
74 |
75 | val lastmsg = channel.lastMessageDate;
76 | if (lastmsg != null) {
77 | lastMessageDate.text = lastmsg.toString()
78 | }
79 |
80 | itemView.setBackgroundColor(
81 | if (channel.status == ChannelStatus.JOINED) {
82 | if (channel.type == ChannelType.PRIVATE)
83 | Color.BLUE
84 | else
85 | Color.WHITE
86 | } else {
87 | if (channel.status == ChannelStatus.INVITED)
88 | Color.YELLOW
89 | else
90 | Color.GRAY
91 | }
92 | )
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/menu/channel.xml:
--------------------------------------------------------------------------------
1 |
93 |
--------------------------------------------------------------------------------
/chat-demo-android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'kotlin-android-extensions'
5 | apply plugin: 'com.google.gms.google-services'
6 | apply plugin: 'com.google.firebase.crashlytics'
7 |
8 | def ACCESS_TOKEN_SERVICE_URL = project.hasProperty('ACCESS_TOKEN_SERVICE_URL') ? "\""+project.getProperty('ACCESS_TOKEN_SERVICE_URL')+"\"" : "\"http://example.com/token\""
9 |
10 | dependencies {
11 | implementation "androidx.appcompat:appcompat:$androidXVersion"
12 | implementation 'com.google.android.material:material:1.2.1'
13 |
14 | // FCM
15 | // Firebase now requires the app gradle file to explicitly list
16 | // `com.google.firebase:firebase-core` as a dependency for Firebase services to work as expected.
17 | implementation 'com.google.firebase:firebase-core:17.5.1'
18 | implementation 'com.google.firebase:firebase-iid:20.3.0'
19 | implementation 'com.google.firebase:firebase-messaging:20.3.0'
20 |
21 | // Crashlytics
22 | implementation 'com.google.firebase:firebase-crashlytics:17.2.2'
23 | implementation 'com.google.firebase:firebase-crashlytics-ndk:17.2.2'
24 |
25 | implementation "com.jakewharton:kotterknife:0.1.0-SNAPSHOT"
26 | implementation "eu.inloop:simplerecycleradapter:0.3.4"
27 |
28 | implementation 'com.twilio:chat-android-with-symbols:6.1.1'
29 |
30 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
31 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion"
32 | implementation "org.jetbrains.anko:anko:0.10.5"
33 |
34 | }
35 |
36 | android {
37 | compileSdkVersion androidCompileSdkVersion
38 |
39 | defaultConfig {
40 | applicationId "com.twilio.chat.demo"
41 | minSdkVersion androidMinSdkVersion
42 | targetSdkVersion androidCompileSdkVersion
43 | buildToolsVersion androidBuildToolsVersion
44 |
45 | versionCode 26
46 | versionName "1.0"
47 |
48 | buildConfigField "String", "ACCESS_TOKEN_SERVICE_URL", ACCESS_TOKEN_SERVICE_URL
49 | }
50 |
51 | lintOptions {
52 | abortOnError false
53 | }
54 |
55 | buildTypes {
56 | release {
57 | shrinkResources true
58 | minifyEnabled true
59 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
60 |
61 | firebaseCrashlytics {
62 | nativeSymbolUploadEnabled true
63 |
64 | println "release: unstrippedNativeLibsDir \"build/intermediates/merged_native_libs/release/out/lib\""
65 | unstrippedNativeLibsDir "build/intermediates/merged_native_libs/release/out/lib"
66 |
67 | println "release: strippedNativeLibsDir \"build/intermediates/stripped_native_libs/release/out/lib\""
68 | strippedNativeLibsDir "build/intermediates/stripped_native_libs/release/out/lib"
69 | }
70 | }
71 | debug {
72 | applicationIdSuffix ".debug"
73 | versionNameSuffix "-debug"
74 |
75 | firebaseCrashlytics {
76 | nativeSymbolUploadEnabled true
77 |
78 | println "debug: unstrippedNativeLibsDir \"build/intermediates/merged_native_libs/debug/out/lib\""
79 | unstrippedNativeLibsDir "build/intermediates/merged_native_libs/debug/out/lib"
80 |
81 | println "debug: strippedNativeLibsDir \"build/intermediates/stripped_native_libs/debug/out/lib\""
82 | strippedNativeLibsDir "build/intermediates/stripped_native_libs/debug/out/lib"
83 | }
84 | }
85 | }
86 |
87 | compileOptions {
88 | sourceCompatibility JavaVersion.VERSION_1_8
89 | targetCompatibility JavaVersion.VERSION_1_8
90 | }
91 |
92 | kotlinOptions {
93 | jvmTarget = "1.8"
94 | useIR = true
95 | }
96 |
97 | dexOptions {
98 | javaMaxHeapSize "4g"
99 | }
100 |
101 | sourceSets {
102 | main.java.srcDirs += 'src/main/kotlin'
103 | }
104 | }
105 |
106 | // Keep an eye on SDK deprecations
107 | tasks.withType(JavaCompile) {
108 | options.compilerArgs << "-Xlint:deprecation" << "-Xdiags:verbose"
109 | }
110 |
111 | if (hasProperty("googleServicesJson")) {
112 | copy {
113 | from(googleServicesJson)
114 | into(projectDir)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/services/FCMListenerService.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.services
2 |
3 | import android.app.Notification
4 | import android.app.NotificationManager
5 | import android.app.PendingIntent
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.graphics.Color
9 | import android.net.Uri
10 | import androidx.core.app.NotificationCompat
11 | import com.google.firebase.messaging.FirebaseMessagingService
12 | import com.google.firebase.messaging.RemoteMessage
13 | import com.twilio.chat.NotificationPayload
14 | import com.twilio.chat.demo.Constants
15 | import com.twilio.chat.demo.R
16 | import com.twilio.chat.demo.TwilioApplication
17 | import com.twilio.chat.demo.activities.MessageActivity
18 | import org.jetbrains.anko.*
19 |
20 | class FCMListenerService : FirebaseMessagingService(), AnkoLogger {
21 | override fun onMessageReceived(remoteMessage: RemoteMessage) {
22 | // If the application is in the foreground handle both data and notification messages here.
23 | // Also if you intend on generating your own notifications as a result of a received FCM
24 | // message, here is where that should be initiated. See sendNotification method below.
25 | if (remoteMessage == null) return;
26 |
27 | debug { "onMessageReceived for FCM from: ${remoteMessage.from}" }
28 |
29 | // Check if message contains a data payload.
30 | if (remoteMessage.data.isNotEmpty()) {
31 | debug { "Data Message Body: ${remoteMessage.data}" }
32 |
33 | val payload = NotificationPayload(remoteMessage.data)
34 |
35 | val client = TwilioApplication.instance.basicClient.chatClient
36 | client?.handleNotification(payload)
37 |
38 | val type = payload.type
39 |
40 | if (type == NotificationPayload.Type.UNKNOWN) return // Ignore everything we don't support
41 |
42 | var title = "Twilio Notification"
43 |
44 | if (type == NotificationPayload.Type.NEW_MESSAGE)
45 | title = "Twilio: New Message"
46 | if (type == NotificationPayload.Type.ADDED_TO_CHANNEL)
47 | title = "Twilio: Added to Channel"
48 | if (type == NotificationPayload.Type.INVITED_TO_CHANNEL)
49 | title = "Twilio: Invited to Channel"
50 | if (type == NotificationPayload.Type.REMOVED_FROM_CHANNEL)
51 | title = "Twilio: Removed from Channel"
52 |
53 | // Set up action Intent
54 | val intent = Intent(this, MessageActivity::class.java)
55 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
56 |
57 | val cSid = payload.channelSid
58 | if (!"".contentEquals(cSid)) {
59 | intent.putExtra(Constants.EXTRA_CHANNEL_SID, cSid)
60 | }
61 |
62 | val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)
63 |
64 | val notification = NotificationCompat.Builder(this, /*NotificationChannel.DEFAULT_CHANNEL_ID*/"miscellaneous")
65 | .setSmallIcon(R.drawable.ic_notification)
66 | .setContentTitle(title)
67 | .setContentText(payload.body)
68 | .setAutoCancel(true)
69 | .setContentIntent(pendingIntent)
70 | .setColor(Color.rgb(214, 10, 37))
71 | .build()
72 |
73 | val soundFileName = payload.sound
74 | if (resources.getIdentifier(soundFileName, "raw", packageName) != 0) {
75 | val sound = Uri.parse("android.resource://$packageName/raw/$soundFileName")
76 | notification.defaults = notification.defaults and Notification.DEFAULT_SOUND.inv()
77 | notification.sound = sound
78 | debug { "Playing specified sound $soundFileName" }
79 | } else {
80 | notification.defaults = notification.defaults or Notification.DEFAULT_SOUND
81 | debug { "Playing default sound" }
82 | }
83 |
84 | val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
85 |
86 | notificationManager.notify(0, notification)
87 | }
88 |
89 | // Check if message contains a notification payload.
90 | if (remoteMessage.notification != null) {
91 | //debug { "Notification Message Body: ${remoteMessage.notification.body}" }
92 | error { "We do not parse notification body - leave it to system" }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/layout/activity_login.xml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
18 |
19 |
29 |
30 |
36 |
37 |
46 |
47 |
53 |
54 |
61 |
62 |
68 |
69 |
76 |
77 |
78 |
79 |
80 |
81 |
89 |
90 |
99 |
100 |
107 |
108 |
109 |
110 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/res/layout/message_item_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
21 |
22 |
28 |
29 |
40 |
41 |
48 |
49 |
50 |
51 |
62 |
63 |
72 |
73 |
85 |
86 |
87 |
88 |
95 |
102 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/ChannelModel.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo
2 |
3 | import com.twilio.chat.*
4 | import java.util.Date
5 | import com.twilio.chat.Channel.ChannelStatus
6 | import com.twilio.chat.Channel.ChannelType
7 | import com.twilio.chat.Channel.NotificationLevel
8 |
9 | class ChannelModel {
10 | private var channel: Channel? = null
11 | private var channelDescriptor: ChannelDescriptor? = null
12 |
13 | constructor(channel_: Channel) {
14 | channel = channel_
15 | }
16 |
17 | constructor(channel_: ChannelDescriptor) {
18 | channelDescriptor = channel_
19 | }
20 |
21 | val friendlyName: String
22 | get() {
23 | if (channel != null) return channel!!.friendlyName
24 | if (channelDescriptor != null) return channelDescriptor!!.friendlyName
25 | throw IllegalStateException("Invalid state")
26 | }
27 |
28 | val sid: String
29 | get() {
30 | if (channel != null) return channel!!.sid
31 | if (channelDescriptor != null) return channelDescriptor!!.sid
32 | throw IllegalStateException("Invalid state")
33 | }
34 |
35 | val dateUpdatedAsDate: Date?
36 | get() {
37 | if (channel != null) return channel!!.dateUpdatedAsDate
38 | if (channelDescriptor != null) return channelDescriptor!!.dateUpdated
39 | throw IllegalStateException("Invalid state")
40 | }
41 |
42 | val dateCreatedAsDate: Date?
43 | get() {
44 | if (channel != null) return channel!!.dateCreatedAsDate
45 | if (channelDescriptor != null) return channelDescriptor!!.dateCreated
46 | throw IllegalStateException("Invalid state")
47 | }
48 |
49 | val status: ChannelStatus
50 | get() {
51 | if (channel != null) return channel!!.status
52 | if (channelDescriptor != null) return channelDescriptor!!.status
53 | throw IllegalStateException("Invalid state")
54 | }
55 |
56 | val lastMessageDate: Date?
57 | get() {
58 | if (channel != null) return channel!!.lastMessageDate
59 | if (channelDescriptor != null) return null
60 | throw IllegalStateException("Invalid state")
61 | }
62 |
63 | val notificationLevel: NotificationLevel
64 | get() {
65 | if (channel != null) return channel!!.notificationLevel
66 | if (channelDescriptor != null) return NotificationLevel.DEFAULT
67 | throw IllegalStateException("Invalid state")
68 | }
69 |
70 | val lastMessageIndex: Long?
71 | get() {
72 | if (channel != null) return channel!!.lastMessageIndex
73 | if (channelDescriptor != null) return null
74 | throw IllegalStateException("Invalid state")
75 | }
76 |
77 | fun getUnconsumedMessagesCount(listener: CallbackListener) {
78 | if (channel != null) {
79 | channel!!.getUnconsumedMessagesCount(listener)
80 | return
81 | }
82 | if (channelDescriptor != null) {
83 | listener.onSuccess(channelDescriptor!!.unconsumedMessagesCount)
84 | return
85 | }
86 | listener.onError(ErrorInfo(-10001, "No channel in model"))
87 | }
88 |
89 | fun getMessagesCount(listener: CallbackListener) {
90 | if (channel != null) {
91 | channel!!.getMessagesCount(listener)
92 | return
93 | }
94 | if (channelDescriptor != null) {
95 | listener.onSuccess(channelDescriptor!!.messagesCount)
96 | return
97 | }
98 | listener.onError(ErrorInfo(-10002, "No channel in model"))
99 | }
100 |
101 | fun getMembersCount(listener: CallbackListener) {
102 | if (channel != null) {
103 | channel!!.getMembersCount(listener)
104 | return
105 | }
106 | if (channelDescriptor != null) {
107 | listener.onSuccess(channelDescriptor!!.membersCount)
108 | return
109 | }
110 | listener.onError(ErrorInfo(-10003, "No channel in model"))
111 | }
112 |
113 | fun join(listener: StatusListener) {
114 | if (channel != null) {
115 | channel!!.join(listener)
116 | return
117 | }
118 | if (channelDescriptor != null) {
119 | channelDescriptor!!.getChannel(object : CallbackListener() {
120 | override fun onSuccess(chan: Channel) {
121 | chan.join(listener)
122 | }
123 |
124 | override fun onError(err: ErrorInfo?) {
125 | listener.onError(err)
126 | }
127 | })
128 | return
129 | }
130 | listener.onError(ErrorInfo(-10004, "No channel in model"))
131 | }
132 |
133 | fun getChannel(listener: CallbackListener) {
134 | if (channel != null) {
135 | listener.onSuccess(channel)
136 | return
137 | }
138 | if (channelDescriptor != null) {
139 | channelDescriptor!!.getChannel(listener)
140 | return
141 | }
142 | listener.onError(ErrorInfo(-10005, "No channel in model"))
143 | }
144 |
145 | val type: Channel.ChannelType
146 | get() {
147 | if (channel != null) return channel!!.type
148 | if (channelDescriptor != null) return ChannelType.PUBLIC
149 | throw IllegalStateException("Invalid state")
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/views/MessageViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.views
2 |
3 | import android.content.Context
4 | import com.twilio.chat.CallbackListener
5 | import com.twilio.chat.Member
6 | import com.twilio.chat.User
7 | import com.twilio.chat.demo.R
8 | import android.graphics.BitmapFactory
9 | import android.graphics.Color
10 | import android.net.Uri
11 | import android.util.Base64
12 | import android.view.View
13 | import android.view.ViewGroup
14 | import android.widget.ImageView
15 | import android.widget.LinearLayout
16 | import android.widget.RelativeLayout
17 | import android.widget.TextView
18 | import kotterknife.bindView
19 | import com.twilio.chat.demo.TwilioApplication
20 | import com.twilio.chat.demo.activities.MessageActivity
21 | import eu.inloop.simplerecycleradapter.SettableViewHolder
22 | import java.io.File
23 |
24 | class MessageViewHolder(val context: Context, parent: ViewGroup)
25 | : SettableViewHolder(context, R.layout.message_item_layout, parent)
26 | {
27 | val avatarView: ImageView by bindView(R.id.avatar)
28 | val reachabilityView: ImageView by bindView(R.id.reachability)
29 | val body: TextView by bindView(R.id.body)
30 | val mediaView: ImageView by bindView(R.id.mediaPreview)
31 | val author: TextView by bindView(R.id.author)
32 | val date: TextView by bindView(R.id.date)
33 | val identities: RelativeLayout by bindView(R.id.consumptionHorizonIdentities)
34 | val lines: LinearLayout by bindView(R.id.consumptionHorizonLines)
35 |
36 | override fun setData(message: MessageActivity.MessageItem) {
37 | val msg = message.message
38 |
39 | author.text = msg.author
40 | body.text = msg.messageBody
41 | date.text = msg.dateCreated
42 |
43 | identities.removeAllViews()
44 | lines.removeAllViews()
45 |
46 | for (member in message.members.membersList) {
47 | if (msg.author.contentEquals(member.identity)) {
48 | fillUserAvatar(avatarView, member)
49 | fillUserReachability(reachabilityView, member)
50 | }
51 |
52 | if (member.lastConsumedMessageIndex != null && member.lastConsumedMessageIndex == message.message.messageIndex) {
53 | drawConsumptionHorizon(member)
54 | }
55 | }
56 |
57 | if (msg.hasMedia()) {
58 | body.visibility = View.GONE
59 | mediaView.visibility = View.VISIBLE
60 | mediaView.setImageURI(Uri.fromFile(File(context.cacheDir, msg.media.sid)))
61 | }
62 | }
63 |
64 | fun toggleDateVisibility() {
65 | date.visibility = if (date.visibility == View.GONE) View.VISIBLE else View.GONE
66 | }
67 |
68 | private fun drawConsumptionHorizon(member: Member) {
69 | val ident = member.identity
70 | val color = getMemberRgb(ident)
71 |
72 | val identity = TextView(itemView.context)
73 | identity.text = ident
74 | identity.textSize = 8f
75 | identity.setTextColor(color)
76 |
77 | // Layout
78 | val params = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT)
79 | val cc = identities.childCount
80 | if (cc > 0) {
81 | params.addRule(RelativeLayout.RIGHT_OF, identities.getChildAt(cc - 1).id)
82 | }
83 | identity.layoutParams = params
84 |
85 | val line = View(itemView.context)
86 | line.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5)
87 | line.setBackgroundColor(color)
88 |
89 | identities.addView(identity)
90 | lines.addView(line)
91 | }
92 |
93 | private fun fillUserAvatar(avatarView: ImageView, member: Member) {
94 | TwilioApplication.instance.basicClient.chatClient?.users?.getAndSubscribeUser(member.identity, object : CallbackListener() {
95 | override fun onSuccess(user: User) {
96 | val attributes = user.attributes
97 | val avatar = attributes.jsonObject?.opt("avatar") as String?
98 | if (avatar != null) {
99 | val data = Base64.decode(avatar, Base64.NO_WRAP)
100 | val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
101 | avatarView.setImageBitmap(bitmap)
102 | } else {
103 | avatarView.setImageResource(R.drawable.avatar2)
104 | }
105 | }
106 | })
107 | }
108 |
109 | private fun fillUserReachability(reachabilityView: ImageView, member: Member) {
110 | if (!TwilioApplication.instance.basicClient.chatClient?.isReachabilityEnabled!!) {
111 | reachabilityView.setImageResource(R.drawable.reachability_disabled)
112 | } else {
113 | member.getAndSubscribeUser(object : CallbackListener() {
114 | override fun onSuccess(user: User) {
115 | if (user.isOnline) {
116 | reachabilityView.setImageResource(R.drawable.reachability_online)
117 | } else if (user.isNotifiable) {
118 | reachabilityView.setImageResource(R.drawable.reachability_notifiable)
119 | } else {
120 | reachabilityView.setImageResource(R.drawable.reachability_offline)
121 | }
122 | }
123 | })
124 | }
125 | }
126 |
127 | fun getMemberRgb(identity: String): Int {
128 | return HORIZON_COLORS[Math.abs(identity.hashCode()) % HORIZON_COLORS.size]
129 | }
130 |
131 | companion object {
132 | private val HORIZON_COLORS = intArrayOf(Color.GRAY, Color.RED, Color.BLUE, Color.GREEN, Color.MAGENTA)
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/services/MediaService.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.services
2 |
3 | import ChatCallbackListener
4 | import ChatStatusListener
5 | import android.app.IntentService
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.net.Uri
9 | import android.provider.OpenableColumns
10 | import com.twilio.chat.Channel
11 | import com.twilio.chat.Message
12 | import com.twilio.chat.ProgressListener
13 | import com.twilio.chat.demo.TwilioApplication
14 | import com.twilio.chat.demo.models.Media
15 | import kotlinx.coroutines.CompletableDeferred
16 | import kotlinx.coroutines.GlobalScope
17 | import kotlinx.coroutines.cancel
18 | import kotlinx.coroutines.launch
19 | import kotlinx.coroutines.newSingleThreadContext
20 | import org.jetbrains.anko.*
21 | import java.io.File
22 | import java.io.FileOutputStream
23 | import java.util.concurrent.CancellationException
24 |
25 | class MediaService : IntentService(MediaService::class.java.simpleName), AnkoLogger {
26 |
27 | companion object {
28 | val EXTRA_MEDIA_URI = "com.twilio.demo.chat.media_uri"
29 | val EXTRA_CHANNEL = "com.twilio.demo.chat.media_channel"
30 | val EXTRA_MESSAGE_INDEX = "com.twilio.demo.chat.message_index"
31 |
32 | val EXTRA_ACTION = "com.twilio.demo.chat.media.action"
33 | val EXTRA_ACTION_UPLOAD = "com.twilio.demo.chat.media.action_upload"
34 | val EXTRA_ACTION_DOWNLOAD = "com.twilio.demo.chat.media.action_download"
35 | }
36 |
37 | private val coroutineContext = newSingleThreadContext(MediaService::class.java.simpleName)
38 |
39 | override fun onHandleIntent(intent: Intent?) {
40 | val action = intent?.getStringExtra(EXTRA_ACTION)
41 |
42 | when (action) {
43 | EXTRA_ACTION_UPLOAD -> upload(intent)
44 | EXTRA_ACTION_DOWNLOAD -> download(intent)
45 | }
46 | }
47 |
48 | private fun upload(intent: Intent) {
49 | val uriString = intent.getStringExtra(EXTRA_MEDIA_URI) ?: throw NullPointerException("Media URI not provided")
50 | val channel = intent.getParcelableExtra(EXTRA_CHANNEL) ?: throw NullPointerException("Channel is not provided")
51 |
52 | GlobalScope.launch(coroutineContext) {
53 | val deferred = CompletableDeferred()
54 |
55 | val uri = Uri.parse(uriString)
56 | val cursor = contentResolver.query(uri, null, null, null, null)!!
57 |
58 | try {
59 | if (cursor.moveToFirst()) {
60 | val name = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
61 | val type = contentResolver.getType(uri)!!
62 | val stream = contentResolver.openInputStream(uri)!!
63 |
64 | val media = Media(name, type, stream)
65 |
66 | val options = Message.options()
67 | .withMediaFileName(media.name)
68 | .withMedia(media.stream, media.type)
69 | .withMediaProgressListener(object : ProgressListener() {
70 | override fun onStarted() = debug { "Start media upload" }
71 | override fun onProgress(bytes: Long) = debug { "Media upload progress - bytes done: ${bytes}" }
72 | override fun onCompleted(mediaSid: String) = debug { "Media upload completed" }
73 | })
74 |
75 | TwilioApplication.instance.basicClient.chatClient?.channels?.getChannel(channel.sid, ChatCallbackListener {
76 | it.messages.sendMessage(options, ChatCallbackListener {
77 | debug { "Media message sent - sid: ${it.sid}, type: ${it.type}" }
78 | deferred.complete(Unit)
79 | })
80 | })
81 |
82 | }
83 | } catch (e: Exception) {
84 | error { "Failed to upload media -> error: ${e.message}" }
85 | deferred.completeExceptionally(e)
86 | } finally {
87 | cursor.close()
88 | }
89 |
90 | deferred.await()
91 | }
92 | }
93 |
94 | private fun download(intent: Intent) {
95 | val channel = intent.getParcelableExtra(EXTRA_CHANNEL) ?: throw NullPointerException("Channel is not provided")
96 | val messageIndex = intent.getLongExtra(EXTRA_MESSAGE_INDEX, -1L)
97 |
98 | GlobalScope.launch(coroutineContext) {
99 | val deferred = CompletableDeferred()
100 |
101 | channel.messages.getMessageByIndex(messageIndex, ChatCallbackListener { message ->
102 | val media = message.media ?: return@ChatCallbackListener
103 |
104 | debug { "Media received - sid: ${media.sid}, name: ${media.fileName}, type: ${media.type}, size: ${media.size}" }
105 |
106 | try {
107 | val outStream = FileOutputStream(File(cacheDir, media.sid))
108 |
109 | media.download(outStream, ChatStatusListener { debug { "Download completed" } }, object : ProgressListener() {
110 | override fun onStarted() = debug { "Start media download" }
111 | override fun onProgress(bytes: Long) = debug { "Media download progress - bytes done: ${bytes}" }
112 | override fun onCompleted(mediaSid: String) {
113 | debug { "Media download completed" }
114 | deferred.complete(mediaSid)
115 | }
116 | })
117 |
118 | } catch (e: Exception) {
119 | error { "Failed to download media - error: ${e.message}" }
120 | deferred.cancel(CancellationException(e.message))
121 | }
122 | })
123 |
124 | deferred.await()
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/activities/LoginActivity.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.activities
2 |
3 | import com.google.android.gms.common.ConnectionResult
4 | import com.google.android.gms.common.GoogleApiAvailability
5 | import com.twilio.chat.ChatClient
6 | import com.twilio.chat.demo.BasicChatClient.LoginListener
7 | import com.twilio.chat.demo.R
8 | import com.twilio.chat.demo.BuildConfig
9 | import android.net.Uri
10 | import android.app.Activity
11 | import android.os.Bundle
12 | import android.view.Menu
13 | import android.view.MenuItem
14 | import android.widget.ProgressBar
15 | import android.widget.Toast
16 | import android.preference.PreferenceManager
17 | import android.view.View
18 | import android.widget.ArrayAdapter
19 | import com.twilio.chat.demo.TwilioApplication
20 | import com.twilio.chat.demo.services.RegistrationIntentService
21 | import kotlinx.android.synthetic.main.activity_login.*
22 | import org.jetbrains.anko.*
23 |
24 | class LoginActivity : Activity(), LoginListener, AnkoLogger {
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | super.onCreate(savedInstanceState)
27 | setContentView(R.layout.activity_login)
28 |
29 | login.setOnClickListener {
30 | val userName = clientNameTextBox.text.toString()
31 | val certPinningChosen = certPinning.isChecked
32 | val realm = realmSelect.selectedItem as String
33 | val ttl = tokenTtlTextBox.text.toString()
34 |
35 | val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
36 | sharedPreferences.edit()
37 | .putString("userName", userName)
38 | .putBoolean("pinCerts", certPinningChosen)
39 | .putString("realm", realm)
40 | .putString("ttl", ttl)
41 | .apply()
42 |
43 | val url = Uri.parse(BuildConfig.ACCESS_TOKEN_SERVICE_URL)
44 | .buildUpon()
45 | .appendQueryParameter("identity", userName)
46 | .appendQueryParameter("realm", realm)
47 | .appendQueryParameter("ttl", ttl)
48 | .build()
49 | .toString()
50 | debug { "url string : $url" }
51 |
52 | TwilioApplication.instance.basicClient.login(userName, certPinningChosen, realm, url, this@LoginActivity)
53 | }
54 |
55 | if (checkPlayServices()) {
56 | fcmAvailable.isChecked = true
57 | // Start IntentService to register this application with GCM.
58 | startService()
59 | }
60 | }
61 |
62 | override fun onResume() {
63 | super.onResume()
64 |
65 | val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
66 |
67 | val userName = sharedPreferences.getString("userName", DEFAULT_CLIENT_NAME)
68 | val certPin = sharedPreferences.getBoolean("pinCerts", true)
69 | val realm = sharedPreferences.getString("realm", DEFAULT_REALM)
70 | val ttl = sharedPreferences.getString("ttl", DEFAULT_TTL)
71 |
72 | clientNameTextBox.setText(userName)
73 | certPinning.isChecked = certPin
74 | realmSelect.setSelection((realmSelect.adapter as ArrayAdapter).getPosition(realm))
75 | tokenTtlTextBox.setText(ttl)
76 |
77 | // Make sure no chatclient is created
78 | if (TwilioApplication.instance.basicClient.chatClient != null) {
79 | TwilioApplication.instance.basicClient.shutdown()
80 | }
81 | }
82 |
83 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
84 | // Inflate the menu; this adds items to the action bar if it is present.
85 | menuInflater.inflate(R.menu.login, menu)
86 | return true
87 | }
88 |
89 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
90 | if (item.itemId == R.id.action_about) {
91 | showAboutDialog()
92 | }
93 | return super.onOptionsItemSelected(item)
94 | }
95 |
96 | private fun showAboutDialog() {
97 | alert("Version: ${ChatClient.getSdkVersion()}", "About") {
98 | positiveButton("OK") { dialog -> dialog.cancel() }
99 | }.show()
100 | }
101 |
102 | fun setLoginProgressVisible(enable: Boolean) {
103 | if (enable) {
104 | loginInputsLayout.visibility = View.GONE
105 | loginProgressLayout.visibility = View.VISIBLE
106 | } else {
107 | loginInputsLayout.visibility = View.VISIBLE
108 | loginProgressLayout.visibility = View.GONE
109 | }
110 | }
111 |
112 | override fun onLoginStarted() {
113 | debug { "Log in started" }
114 | setLoginProgressVisible(true)
115 | }
116 |
117 | override fun onLoginFinished() {
118 | setLoginProgressVisible(false)
119 | startActivity()
120 | }
121 |
122 | override fun onLoginError(errorMessage: String) {
123 | setLoginProgressVisible(false)
124 | TwilioApplication.instance.showToast("Error logging in : " + errorMessage, Toast.LENGTH_LONG)
125 | }
126 |
127 | override fun onLogoutFinished() {
128 | setLoginProgressVisible(false)
129 | TwilioApplication.instance.showToast("Log out finished")
130 | }
131 |
132 | /**
133 | * Check the device to make sure it has the Google Play Services APK. If
134 | * it doesn't, display a dialog that allows users to download the APK from
135 | * the Google Play Store or enable it in the device's system settings.
136 | */
137 | private fun checkPlayServices(): Boolean {
138 | val apiAvailability = GoogleApiAvailability.getInstance()
139 | val resultCode = apiAvailability.isGooglePlayServicesAvailable(this)
140 | if (resultCode != ConnectionResult.SUCCESS) {
141 | if (apiAvailability.isUserResolvableError(resultCode)) {
142 | apiAvailability.getErrorDialog(this, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST)
143 | .show()
144 | } else {
145 | info { "This device is not supported." }
146 | finish()
147 | }
148 | return false
149 | }
150 | return true
151 | }
152 |
153 | companion object {
154 | private val DEFAULT_CLIENT_NAME = "TestUser"
155 | private val DEFAULT_REALM = "us1"
156 | private val DEFAULT_TTL = "3000"
157 | private val PLAY_SERVICES_RESOLUTION_REQUEST = 9000
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/activities/UserActivity.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.activities
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.graphics.Bitmap
6 | import android.graphics.BitmapFactory
7 | import android.os.Bundle
8 | import android.provider.MediaStore
9 | import android.util.Base64
10 | import android.view.Menu
11 | import com.twilio.chat.Channel
12 | import com.twilio.chat.ErrorInfo
13 | import com.twilio.chat.ChatClientListener
14 | import com.twilio.chat.ChatClient
15 | import com.twilio.chat.User
16 | import com.twilio.chat.demo.R
17 | import ToastStatusListener
18 | import com.twilio.chat.Attributes
19 | import com.twilio.chat.demo.TwilioApplication
20 | import org.json.JSONException
21 | import org.json.JSONObject
22 | import java.io.ByteArrayOutputStream
23 | import kotlinx.android.synthetic.main.activity_user_info.*
24 |
25 | class UserActivity : Activity() {
26 | internal var client: ChatClient? = null
27 | internal var bitmap: Bitmap? = null
28 |
29 | private val chatClientListener = object : ChatClientListener {
30 | override fun onChannelAdded(channel: Channel) {}
31 |
32 | override fun onChannelUpdated(channel: Channel, reason: Channel.UpdateReason) {}
33 |
34 | override fun onChannelDeleted(channel: Channel) {}
35 |
36 | override fun onChannelInvited(channel: Channel) {}
37 |
38 | override fun onChannelJoined(channel: Channel) {}
39 |
40 | override fun onError(error: ErrorInfo) {
41 | TwilioApplication.instance.showError("Error listening for userInfoChange", error)
42 | }
43 |
44 | override fun onChannelSynchronizationChange(channel: Channel) {}
45 |
46 | override fun onUserUpdated(user: User, reason: User.UpdateReason) {
47 | if (reason == User.UpdateReason.ATTRIBUTES) {
48 | fillUserAvatar()
49 | }
50 | TwilioApplication.instance.showToast("Update successful for user attributes")
51 | }
52 |
53 | override fun onUserSubscribed(user: User) {}
54 |
55 | override fun onUserUnsubscribed(user: User) {}
56 |
57 | override fun onClientSynchronization(synchronizationStatus: ChatClient.SynchronizationStatus) {}
58 |
59 | override fun onNewMessageNotification(channelSid: String?, messageSid: String?, messageIndex: Long) {}
60 | override fun onAddedToChannelNotification(channelSid: String?) {}
61 | override fun onInvitedToChannelNotification(channelSid: String?) {}
62 | override fun onRemovedFromChannelNotification(channelSid: String?) {}
63 |
64 | override fun onNotificationSubscribed() {}
65 |
66 | override fun onNotificationFailed(errorInfo: ErrorInfo) {}
67 |
68 | override fun onConnectionStateChange(connectionState: ChatClient.ConnectionState) {}
69 |
70 | override fun onTokenExpired() {
71 | TwilioApplication.instance.basicClient.onTokenExpired()
72 | }
73 |
74 | override fun onTokenAboutToExpire() {
75 | TwilioApplication.instance.basicClient.onTokenAboutToExpire()
76 | }
77 | }
78 |
79 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
80 | menuInflater.inflate(R.menu.channel, menu)
81 | return true
82 | }
83 |
84 | public override fun onResume() {
85 | super.onResume()
86 | client?.addListener(chatClientListener)
87 | }
88 |
89 | override fun onPause() {
90 | client?.removeListener(chatClientListener)
91 | super.onPause()
92 | }
93 |
94 | override fun onCreate(savedInstanceState: Bundle?) {
95 | super.onCreate(savedInstanceState)
96 | setContentView(R.layout.activity_user_info)
97 |
98 | client = TwilioApplication.instance.basicClient.chatClient
99 |
100 | val user = client?.users?.myUser
101 |
102 | if (user == null) return
103 |
104 | user_friendly_name.setText(user.friendlyName)
105 |
106 | user_info_save.setOnClickListener {
107 | if (user.friendlyName != user_friendly_name.text.toString()) {
108 | user.setFriendlyName(
109 | user_friendly_name.text.toString(), ToastStatusListener(
110 | "Update successful for user friendlyName",
111 | "Update failed for user friendlyName"))
112 | }
113 | if (bitmap != null) {
114 | val attributes = JSONObject()
115 | try {
116 | attributes.put("avatar", getBase64FromBitmap(bitmap!!))
117 | } catch (ignored: JSONException) {
118 | // whatever?
119 | }
120 |
121 | user.setAttributes(Attributes(attributes), ToastStatusListener(
122 | "Update successful for user attributes",
123 | "Update failed for user attributes") {
124 | fillUserAvatar()
125 | })
126 | }
127 | }
128 |
129 | fillUserAvatar()
130 | avatar.setOnClickListener { dispatchTakePictureIntent() }
131 | }
132 |
133 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
134 | if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == Activity.RESULT_OK) {
135 | val extras = data.extras!!
136 | val imageBitmap = extras.get("data") as Bitmap
137 | bitmap = getResizedBitmap(imageBitmap, 96)
138 | avatar.setImageBitmap(bitmap)
139 | }
140 | }
141 |
142 | fun getBase64FromBitmap(bitmap: Bitmap): String {
143 | val stream = ByteArrayOutputStream()
144 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
145 | val string = Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP)
146 | return string
147 | }
148 |
149 | private fun dispatchTakePictureIntent() {
150 | val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
151 | if (takePictureIntent.resolveActivity(packageManager) != null) {
152 | startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
153 | }
154 | }
155 |
156 | private fun fillUserAvatar() {
157 | val user = client?.users?.myUser
158 | val attributes = user?.attributes
159 | val ava = attributes?.jsonObject?.opt("avatar") as String?
160 | if (ava != null) {
161 | val data = Base64.decode(ava, Base64.NO_WRAP)
162 | bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
163 | avatar.setImageBitmap(bitmap)
164 | }
165 | }
166 |
167 | fun getResizedBitmap(image: Bitmap, minSize: Int): Bitmap {
168 | var width = image.width
169 | var height = image.height
170 |
171 | val bitmapRatio = width.toFloat() / height.toFloat()
172 | if (bitmapRatio <= 1) {
173 | width = minSize
174 | height = (width / bitmapRatio).toInt()
175 | } else {
176 | height = minSize
177 | width = (height * bitmapRatio).toInt()
178 | }
179 |
180 | return Bitmap.createScaledBitmap(image, width, height, true)
181 | }
182 |
183 | companion object {
184 | internal val REQUEST_IMAGE_CAPTURE = 1
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/BasicChatClient.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo
2 |
3 | import ToastStatusListener
4 | import android.content.Context
5 | import android.os.AsyncTask
6 | import android.os.Handler
7 | import com.twilio.chat.CallbackListener
8 | import com.twilio.chat.Channel
9 | import com.twilio.chat.ChatClient
10 | import com.twilio.chat.ChatClientListener
11 | import com.twilio.chat.ErrorInfo
12 | import com.twilio.chat.User
13 | import com.twilio.chat.internal.HandlerUtil
14 | import org.jetbrains.anko.AnkoLogger
15 | import org.jetbrains.anko.debug
16 | import org.jetbrains.anko.warn
17 | import java.util.*
18 |
19 | class BasicChatClient(private val context: Context)
20 | : CallbackListener()
21 | , ChatClientListener
22 | , AnkoLogger
23 | {
24 | private var accessToken: String? = null
25 | private var fcmToken: String? = null
26 |
27 | var chatClient: ChatClient? = null
28 | private set
29 |
30 | private var loginListener: LoginListener? = null
31 | private var loginListenerHandler: Handler? = null
32 |
33 | private var urlString: String? = null
34 | private var username: String? = null
35 | private var pinCerts: Boolean = true
36 | private var realm: String? = null
37 |
38 | init {
39 | if (BuildConfig.DEBUG) {
40 | warn { "Enabling DEBUG logging" }
41 | ChatClient.setLogLevel(ChatClient.LogLevel.VERBOSE)
42 | }
43 | }
44 |
45 | interface LoginListener {
46 | fun onLoginStarted()
47 | fun onLoginFinished()
48 | fun onLoginError(errorMessage: String)
49 | fun onLogoutFinished()
50 | }
51 |
52 | private fun notifyLoginStarted() { // Called before getting access token
53 | loginListenerHandler!!.post {
54 | if (loginListener != null) {
55 | loginListener!!.onLoginStarted()
56 | }
57 | }
58 | }
59 | private fun notifyLoginFinished() { // Called after successful creation of ChatClient
60 | loginListenerHandler!!.post {
61 | if (loginListener != null) {
62 | loginListener!!.onLoginFinished()
63 | }
64 | }
65 | }
66 | private fun notifyLoginError(errorMessage: String) {
67 | loginListenerHandler!!.post {
68 | if (loginListener != null) {
69 | loginListener!!.onLoginError(errorMessage)
70 | }
71 | }
72 | }
73 | private fun notifyLogoutFinished() {
74 | loginListenerHandler!!.post {
75 | if (loginListener != null) {
76 | loginListener!!.onLogoutFinished()
77 | }
78 | }
79 | }
80 |
81 | fun setFCMToken(fcmToken: String) {
82 | warn { "setFCMToken $fcmToken" }
83 | this.fcmToken = fcmToken
84 | if (chatClient != null) {
85 | setupFcmToken()
86 | }
87 | }
88 |
89 | fun login(username: String, pinCerts: Boolean, realm: String, url: String, listener: LoginListener) {
90 | /*
91 | if (username == this.username
92 | && pinCerts == this.pinCerts
93 | && realm == this.realm
94 | && url == this.urlString
95 | && listener === loginListener
96 | && chatClient != null) {
97 | onSuccess(chatClient!!)
98 | return
99 | }
100 | */
101 | assert(chatClient == null) { "ChatClient object is to be created on login, should be null before login" }
102 |
103 | this.username = username
104 | this.pinCerts = pinCerts
105 | this.realm = realm
106 | urlString = url
107 |
108 | loginListenerHandler = HandlerUtil.setupListenerHandler()
109 | loginListener = listener
110 |
111 | getAccessToken()
112 | }
113 |
114 | fun updateToken() {
115 | getAccessToken()
116 | }
117 |
118 | private fun setupFcmToken() {
119 | chatClient!!.registerFCMToken(ChatClient.FCMToken(fcmToken),
120 | ToastStatusListener(
121 | "Firebase Messaging registration successful",
122 | "Firebase Messaging registration not successful"))
123 | }
124 |
125 | fun unregisterFcmToken() {
126 | chatClient!!.unregisterFCMToken(ChatClient.FCMToken(fcmToken),
127 | ToastStatusListener(
128 | "Firebase Messaging unregistration successful",
129 | "Firebase Messaging unregistration not successful"))
130 | }
131 |
132 | private fun createClient() {
133 | assert(chatClient == null)
134 |
135 | val props = ChatClient.Properties.Builder()
136 | .setRegion(realm)
137 | .setDeferCertificateTrustToPlatform(!pinCerts)
138 | .createProperties()
139 |
140 | ChatClient.create(context.applicationContext,
141 | accessToken!!,
142 | props,
143 | this)
144 | }
145 |
146 | fun shutdown() {
147 | chatClient!!.shutdown()
148 | chatClient = null // Client no longer usable after shutdown()
149 | notifyLogoutFinished()
150 | }
151 |
152 | // Client created, remember the reference and set up UI
153 | override fun onSuccess(client: ChatClient) {
154 | debug { "Received completely initialized ChatClient" }
155 | chatClient = client
156 |
157 | if (fcmToken != null) {
158 | setupFcmToken()
159 | }
160 |
161 | notifyLoginFinished()
162 | }
163 |
164 | // Client not created, fail
165 | override fun onError(errorInfo: ErrorInfo?) {
166 | TwilioApplication.instance.logErrorInfo("Login error", errorInfo!!)
167 | chatClient = null
168 |
169 | notifyLoginError(errorInfo.toString())
170 | }
171 |
172 | // Token expiration events
173 |
174 | override fun onTokenAboutToExpire() {
175 | if (chatClient != null) {
176 | TwilioApplication.instance.showToast("Token will expire in 3 minutes. Getting new token.")
177 | getAccessToken()
178 | }
179 | }
180 |
181 | override fun onTokenExpired() {
182 | accessToken = null
183 | if (chatClient != null) {
184 | TwilioApplication.instance.showToast("Token expired. Getting new token.")
185 | getAccessToken()
186 | }
187 | }
188 |
189 | private fun getAccessToken() {
190 | GetAccessTokenAsyncTask().execute(urlString)
191 | }
192 |
193 | /**
194 | * Modify this method if you need to provide more information to your Access Token Service.
195 | */
196 | //TODO coroutines
197 | private inner class GetAccessTokenAsyncTask : AsyncTask>() {
198 | override fun onPreExecute() {
199 | super.onPreExecute()
200 | if (chatClient == null) {
201 | notifyLoginStarted()
202 | }
203 | }
204 |
205 | override fun doInBackground(vararg params: String): Optional {
206 | var result: Optional = Optional.empty();
207 | try {
208 | result = Optional.of(HttpHelper.httpGet(params[0]))
209 | } catch (e: Exception) {
210 | System.err.println("getAccessToken() error:")
211 | e.printStackTrace()
212 | notifyLoginError(e.message.orEmpty())
213 | }
214 |
215 | return result
216 | }
217 |
218 | override fun onPostExecute(result: Optional) {
219 | if (!result.isPresent) return
220 |
221 | accessToken = result.get();
222 |
223 | super.onPostExecute(result)
224 |
225 | applyAccessToken()
226 | }
227 |
228 | private fun applyAccessToken() {
229 | if (chatClient == null) {
230 | // Create client with accessToken
231 | createClient()
232 | } else {
233 | // Client already exists, so set accessToken to it
234 | chatClient!!.updateToken(accessToken, ToastStatusListener(
235 | "Client Update Token was successfull",
236 | "Client Update Token failed"))
237 | }
238 | }
239 | }
240 |
241 | override fun onChannelAdded(p0: Channel?) {}
242 | override fun onChannelDeleted(p0: Channel?) {}
243 | override fun onChannelInvited(p0: Channel?) {}
244 | override fun onChannelJoined(p0: Channel?) {}
245 | override fun onChannelSynchronizationChange(p0: Channel?) {}
246 | override fun onChannelUpdated(p0: Channel?, p1: Channel.UpdateReason?) {}
247 | override fun onClientSynchronization(p0: ChatClient.SynchronizationStatus?) {}
248 | override fun onConnectionStateChange(p0: ChatClient.ConnectionState?) {}
249 | override fun onRemovedFromChannelNotification(p0: String?) {}
250 | override fun onUserSubscribed(p0: User?) {}
251 | override fun onUserUnsubscribed(p0: User?) {}
252 | override fun onUserUpdated(p0: User?, p1: User.UpdateReason?) {}
253 | override fun onAddedToChannelNotification(p0: String?) {}
254 | override fun onInvitedToChannelNotification(p0: String?) {}
255 | override fun onNewMessageNotification(p0: String?, p1: String?, p2: Long) {}
256 | override fun onNotificationFailed(p0: ErrorInfo?) {}
257 | override fun onNotificationSubscribed() {}
258 | }
259 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/activities/ChannelActivity.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.activities
2 |
3 | import java.util.Comparator
4 | import java.util.HashMap
5 | import java.util.Random
6 | import com.twilio.chat.Channel
7 | import com.twilio.chat.Channel.ChannelType
8 | import com.twilio.chat.ChannelDescriptor
9 | import com.twilio.chat.CallbackListener
10 | import com.twilio.chat.ChatClientListener
11 | import com.twilio.chat.ChatClient
12 | import com.twilio.chat.ErrorInfo
13 | import com.twilio.chat.User
14 | import com.twilio.chat.Paginator
15 | import android.app.Activity
16 | import android.content.Intent
17 | import android.os.Bundle
18 | import android.os.Handler
19 | import android.view.*
20 | import com.twilio.chat.demo.*
21 | import com.twilio.chat.demo.views.ChannelViewHolder
22 | import eu.inloop.simplerecycleradapter.ItemClickListener
23 | import eu.inloop.simplerecycleradapter.SettableViewHolder
24 | import eu.inloop.simplerecycleradapter.SimpleRecyclerAdapter
25 | import kotlinx.android.synthetic.main.activity_channel.*
26 | import org.jetbrains.anko.*
27 | import org.json.JSONObject
28 | import org.json.JSONException
29 | import ChatCallbackListener
30 | import ToastStatusListener
31 | import androidx.recyclerview.widget.LinearLayoutManager
32 | import com.twilio.chat.Attributes
33 | import com.twilio.chat.demo.utils.Where.*
34 | import com.twilio.chat.demo.utils.simulateCrash
35 |
36 | class ChannelActivity : Activity(), ChatClientListener, AnkoLogger {
37 | private lateinit var basicClient: BasicChatClient
38 | private val channels = HashMap()
39 | private lateinit var adapter: SimpleRecyclerAdapter
40 |
41 | override fun onCreate(savedInstanceState: Bundle?) {
42 | super.onCreate(savedInstanceState)
43 | setContentView(R.layout.activity_channel)
44 |
45 | basicClient = TwilioApplication.instance.basicClient
46 | basicClient.chatClient?.addListener(this@ChannelActivity)
47 | setupListView()
48 | }
49 |
50 | override fun onResume() {
51 | super.onResume()
52 | getChannels()
53 | }
54 |
55 | override fun onDestroy() {
56 | basicClient.chatClient?.removeListener(this@ChannelActivity)
57 | super.onDestroy()
58 | }
59 |
60 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
61 | menuInflater.inflate(R.menu.channel, menu)
62 | return true
63 | }
64 |
65 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
66 | when (item.itemId) {
67 | R.id.action_create_public -> showCreateChannelDialog(ChannelType.PUBLIC)
68 | R.id.action_create_private -> showCreateChannelDialog(ChannelType.PRIVATE)
69 | R.id.action_create_public_withoptions -> createChannelWithType(ChannelType.PUBLIC)
70 | R.id.action_create_private_withoptions -> createChannelWithType(ChannelType.PRIVATE)
71 | R.id.action_search_by_unique_name -> showSearchChannelDialog()
72 | R.id.action_user_info -> startActivity(Intent(applicationContext, UserActivity::class.java))
73 | R.id.action_update_token -> basicClient.updateToken()
74 | R.id.action_logout -> {
75 | basicClient.shutdown()
76 | finish()
77 | }
78 | R.id.action_unregistercm -> basicClient.unregisterFcmToken()
79 | R.id.action_crash_in_java -> throw RuntimeException("Simulated crash in ChannelActivity.kt")
80 | R.id.action_crash_in_chat_client -> basicClient.chatClient!!.simulateCrash(CHAT_CLIENT_CPP)
81 | R.id.action_crash_in_tm_client -> basicClient.chatClient!!.simulateCrash(TM_CLIENT_CPP)
82 | }
83 | return super.onOptionsItemSelected(item)
84 | }
85 |
86 | private fun createChannelWithType(type: ChannelType) {
87 | val rand = Random()
88 | val value = rand.nextInt(50)
89 |
90 | val attrs = JSONObject()
91 | try {
92 | attrs.put("topic", "testing channel creation with options ${value}")
93 | } catch (xcp: JSONException) {
94 | error { "JSON exception: $xcp" }
95 | }
96 |
97 | val typ = if (type == ChannelType.PRIVATE) "Priv" else "Pub"
98 |
99 | val builder = basicClient.chatClient?.channels?.channelBuilder()
100 |
101 | builder?.withFriendlyName("${typ}_TestChannelF_${value}")
102 | ?.withUniqueName("${typ}_TestChannelU_${value}")
103 | ?.withType(type)
104 | ?.withAttributes(Attributes(attrs))
105 | ?.build(object : CallbackListener() {
106 | override fun onSuccess(newChannel: Channel) {
107 | debug { "Successfully created a channel with options." }
108 | channels.put(newChannel.sid, ChannelModel(newChannel))
109 | refreshChannelList()
110 | }
111 |
112 | override fun onError(errorInfo: ErrorInfo?) {
113 | error { "Error creating a channel" }
114 | }
115 | })
116 | }
117 |
118 | private fun showCreateChannelDialog(type: ChannelType) {
119 | alert(R.string.title_add_channel) {
120 | customView {
121 | verticalLayout {
122 | textView {
123 | text = "Enter ${type} name"
124 | padding = dip(10)
125 | }.lparams(width = matchParent)
126 | val channel_name = editText { padding = dip(10) }.lparams(width = matchParent)
127 | positiveButton(R.string.create) {
128 | val channelName = channel_name.text.toString()
129 | debug { "Creating channel with friendly Name|$channelName|" }
130 | basicClient.chatClient?.channels?.createChannel(channelName, type, ChatCallbackListener() {
131 | debug { "Channel created with sid|${it.sid}| and type ${it.type}" }
132 | channels.put(it.sid, ChannelModel(it))
133 | refreshChannelList()
134 | })
135 | }
136 | negativeButton(R.string.cancel) {}
137 | }
138 | }
139 | }.show()
140 | }
141 |
142 | private fun showSearchChannelDialog() {
143 | alert(R.string.title_find_channel) {
144 | customView {
145 | verticalLayout {
146 | textView {
147 | text = "Enter unique channel name"
148 | padding = dip(10)
149 | }.lparams(width = matchParent)
150 | val channel_name = editText { padding = dip(10) }.lparams(width = matchParent)
151 | positiveButton(R.string.search) {
152 | val channelSid = channel_name.text.toString()
153 | debug { "Searching for ${channelSid}" }
154 | basicClient.chatClient?.channels?.getChannel(channelSid, ChatCallbackListener() {
155 | if (it != null) {
156 | TwilioApplication.instance.showToast("${it.sid}: ${it.friendlyName}")
157 | } else {
158 | TwilioApplication.instance.showToast("Channel not found.")
159 | }
160 | })
161 | }
162 | negativeButton(R.string.cancel) {}
163 | }
164 | }
165 | }.show()
166 | }
167 |
168 | private fun setupListView() {
169 | adapter = SimpleRecyclerAdapter(
170 | ItemClickListener { channel: ChannelModel, _, _ ->
171 | if (channel.status == Channel.ChannelStatus.JOINED) {
172 | Handler().postDelayed({
173 | channel.getChannel(ChatCallbackListener() {
174 | startActivity(
175 | Constants.EXTRA_CHANNEL to it,
176 | Constants.EXTRA_CHANNEL_SID to it.sid
177 | )
178 | })
179 | }, 0)
180 | return@ItemClickListener
181 | }
182 | alert(R.string.select_action) {
183 | positiveButton("Join") { dialog ->
184 | channel.join(
185 | ToastStatusListener("Successfully joined channel",
186 | "Failed to join channel") {
187 | refreshChannelList()
188 | })
189 | dialog.cancel()
190 | }
191 | negativeButton(R.string.cancel) {}
192 | }.show()
193 | },
194 | object : SimpleRecyclerAdapter.CreateViewHolder() {
195 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettableViewHolder {
196 | return ChannelViewHolder(this@ChannelActivity, parent);
197 | }
198 | })
199 |
200 | channel_list.adapter = adapter
201 | channel_list.layoutManager = LinearLayoutManager(this).apply {
202 | orientation = LinearLayoutManager.VERTICAL
203 | }
204 | }
205 |
206 | private fun refreshChannelList() {
207 | adapter.clear()
208 | adapter.addItems(channels.values.sortedWith(CustomChannelComparator()))
209 | adapter.notifyDataSetChanged()
210 | }
211 |
212 | private fun getChannelsPage(paginator: Paginator) {
213 | for (cd in paginator.items) {
214 | error { "Adding channel descriptor for sid|${cd.sid}| friendlyName ${cd.friendlyName}" }
215 | channels.put(cd.sid, ChannelModel(cd))
216 | }
217 | refreshChannelList()
218 |
219 | if (paginator.hasNextPage()) {
220 | paginator.requestNextPage(object : CallbackListener>() {
221 | override fun onSuccess(channelDescriptorPaginator: Paginator) {
222 | getChannelsPage(channelDescriptorPaginator)
223 | }
224 | })
225 | } else {
226 | // Get subscribed channels last - so their status will overwrite whatever we received
227 | // from public list. Ugly workaround for now.
228 | val chans = basicClient.chatClient?.channels?.subscribedChannels
229 | if (chans != null) {
230 | for (channel in chans) {
231 | channels.put(channel.sid, ChannelModel(channel))
232 | }
233 | }
234 | refreshChannelList()
235 | }
236 | }
237 |
238 | // Initialize channels with channel list
239 | private fun getChannels() {
240 | channels.clear()
241 |
242 | basicClient.chatClient?.channels?.getPublicChannelsList(object : CallbackListener>() {
243 | override fun onSuccess(channelDescriptorPaginator: Paginator) {
244 | getChannelsPage(channelDescriptorPaginator)
245 | }
246 | })
247 |
248 | basicClient.chatClient?.channels?.getUserChannelsList(object : CallbackListener>() {
249 | override fun onSuccess(channelDescriptorPaginator: Paginator) {
250 | getChannelsPage(channelDescriptorPaginator)
251 | }
252 | })
253 | }
254 |
255 | private fun showIncomingInvite(channel: Channel) {
256 | alert(R.string.channel_invite_message, R.string.channel_invite) {
257 | customView {
258 | verticalLayout {
259 | textView {
260 | text = "You are invited to channel ${channel.friendlyName} |${channel.sid}|"
261 | padding = dip(10)
262 | }.lparams(width = matchParent)
263 | positiveButton(R.string.join) {
264 | channel.join(ToastStatusListener(
265 | "Successfully joined channel",
266 | "Failed to join channel") {
267 | channels.put(channel.sid, ChannelModel(channel))
268 | refreshChannelList()
269 | })
270 | }
271 | negativeButton(R.string.decline) {
272 | channel.declineInvitation(ToastStatusListener(
273 | "Successfully declined channel invite",
274 | "Failed to decline channel invite"))
275 | }
276 | }
277 | }
278 | }.show()
279 | }
280 |
281 | private inner class CustomChannelComparator : Comparator {
282 | override fun compare(lhs: ChannelModel, rhs: ChannelModel): Int {
283 | return lhs.friendlyName.compareTo(rhs.friendlyName)
284 | }
285 | }
286 |
287 | //=============================================================
288 | // ChatClientListener
289 | //=============================================================
290 |
291 | override fun onChannelJoined(channel: Channel) {
292 | debug { "Received onChannelJoined callback for channel |${channel.friendlyName}|" }
293 | channels.put(channel.sid, ChannelModel(channel))
294 | refreshChannelList()
295 | }
296 |
297 | override fun onChannelAdded(channel: Channel) {
298 | debug { "Received onChannelAdded callback for channel |${channel.friendlyName}|" }
299 | channels.put(channel.sid, ChannelModel(channel))
300 | refreshChannelList()
301 | }
302 |
303 | override fun onChannelUpdated(channel: Channel, reason: Channel.UpdateReason) {
304 | debug { "Received onChannelUpdated callback for channel |${channel.friendlyName}| with reason ${reason}" }
305 | channels.put(channel.sid, ChannelModel(channel))
306 | refreshChannelList()
307 | }
308 |
309 | override fun onChannelDeleted(channel: Channel) {
310 | debug { "Received onChannelDeleted callback for channel |${channel.friendlyName}|" }
311 | channels.remove(channel.sid)
312 | refreshChannelList()
313 | }
314 |
315 | override fun onChannelInvited(channel: Channel) {
316 | channels.put(channel.sid, ChannelModel(channel))
317 | refreshChannelList()
318 | showIncomingInvite(channel)
319 | }
320 |
321 | override fun onChannelSynchronizationChange(channel: Channel) {
322 | error { "Received onChannelSynchronizationChange callback for channel |${channel.friendlyName}| with new status ${channel.status}" }
323 | refreshChannelList()
324 | }
325 |
326 | override fun onClientSynchronization(status: ChatClient.SynchronizationStatus) {
327 | error { "Received onClientSynchronization callback ${status}" }
328 | }
329 |
330 | override fun onUserUpdated(user: User, reason: User.UpdateReason) {
331 | error { "Received onUserUpdated callback for ${reason}" }
332 | }
333 |
334 | override fun onUserSubscribed(user: User) {
335 | error { "Received onUserSubscribed callback" }
336 | }
337 |
338 | override fun onUserUnsubscribed(user: User) {
339 | error { "Received onUserUnsubscribed callback" }
340 | }
341 |
342 | override fun onNewMessageNotification(channelSid: String?, messageSid: String?, messageIndex: Long) {
343 | TwilioApplication.instance.showToast("Received onNewMessage push notification")
344 | }
345 |
346 | override fun onAddedToChannelNotification(channelSid: String?) {
347 | TwilioApplication.instance.showToast("Received onAddedToChannel push notification")
348 | }
349 |
350 | override fun onInvitedToChannelNotification(channelSid: String?) {
351 | TwilioApplication.instance.showToast("Received onInvitedToChannel push notification")
352 | }
353 |
354 | override fun onRemovedFromChannelNotification(channelSid: String?) {
355 | TwilioApplication.instance.showToast("Received onRemovedFromChannel push notification")
356 | }
357 |
358 | override fun onNotificationSubscribed() {
359 | TwilioApplication.instance.showToast("Subscribed to push notifications")
360 | }
361 |
362 | override fun onNotificationFailed(errorInfo: ErrorInfo) {
363 | TwilioApplication.instance.showError("Failed to subscribe to push notifications", errorInfo)
364 | }
365 |
366 | override fun onError(errorInfo: ErrorInfo) {
367 | TwilioApplication.instance.showError("Received error", errorInfo)
368 | }
369 |
370 | override fun onConnectionStateChange(connectionState: ChatClient.ConnectionState) {
371 | TwilioApplication.instance.showToast("Transport state changed to ${connectionState}")
372 | }
373 |
374 | override fun onTokenExpired() {
375 | basicClient.onTokenExpired()
376 | }
377 |
378 | override fun onTokenAboutToExpire() {
379 | basicClient.onTokenAboutToExpire()
380 | }
381 |
382 | companion object {
383 | private val CHANNEL_OPTIONS = arrayOf("Join")
384 | private val JOIN = 0
385 | }
386 | }
387 |
--------------------------------------------------------------------------------
/chat-demo-android/src/main/kotlin/com/twilio/chat/demo/activities/MessageActivity.kt:
--------------------------------------------------------------------------------
1 | package com.twilio.chat.demo.activities
2 |
3 | import java.util.ArrayList
4 | import java.util.Comparator
5 | import com.twilio.chat.Channel
6 | import com.twilio.chat.Channel.ChannelType
7 | import com.twilio.chat.ChannelListener
8 | import com.twilio.chat.CallbackListener
9 | import com.twilio.chat.Member
10 | import com.twilio.chat.Members
11 | import com.twilio.chat.Message
12 | import com.twilio.chat.Paginator
13 | import com.twilio.chat.User
14 | import com.twilio.chat.UserDescriptor
15 | import android.app.Activity
16 | import android.content.Intent
17 | import android.graphics.Color
18 | import android.os.Bundle
19 | import android.text.Editable
20 | import android.text.TextWatcher
21 | import android.view.*
22 | import android.view.inputmethod.EditorInfo
23 | import android.widget.*
24 | import com.twilio.chat.demo.Constants
25 | import com.twilio.chat.demo.R
26 | import com.twilio.chat.demo.TwilioApplication
27 | import com.twilio.chat.demo.services.MediaService
28 | import com.twilio.chat.demo.views.MemberViewHolder
29 | import com.twilio.chat.demo.views.MessageViewHolder
30 | import eu.inloop.simplerecycleradapter.ItemClickListener
31 | import eu.inloop.simplerecycleradapter.ItemLongClickListener
32 | import eu.inloop.simplerecycleradapter.SettableViewHolder
33 | import eu.inloop.simplerecycleradapter.SimpleRecyclerAdapter
34 | import org.json.JSONException
35 | import org.json.JSONObject
36 | import kotlinx.android.synthetic.main.activity_message.*
37 | import org.jetbrains.anko.*
38 | import org.jetbrains.anko.custom.ankoView
39 | import ChatStatusListener
40 | import ChatCallbackListener
41 | import ToastStatusListener
42 | import android.os.Parcelable
43 | import androidx.recyclerview.widget.LinearLayoutManager
44 | import androidx.recyclerview.widget.RecyclerView
45 | import com.twilio.chat.Attributes
46 |
47 | // RecyclerView Anko
48 | fun ViewManager.recyclerView() = recyclerView(theme = 0) {}
49 |
50 | inline fun ViewManager.recyclerView(init: RecyclerView.() -> Unit): RecyclerView {
51 | return ankoView({ RecyclerView(it) }, theme = 0, init = init)
52 | }
53 |
54 | fun ViewManager.recyclerView(theme: Int = 0) = recyclerView(theme) {}
55 |
56 | inline fun ViewManager.recyclerView(theme: Int = 0, init: RecyclerView.() -> Unit): RecyclerView {
57 | return ankoView({ RecyclerView(it) }, theme, init)
58 | }
59 | // End RecyclerView Anko
60 |
61 | class MessageActivity : Activity(), ChannelListener, AnkoLogger {
62 | private lateinit var adapter: SimpleRecyclerAdapter
63 | private var channel: Channel? = null
64 |
65 | private val messageItemList = ArrayList()
66 | private lateinit var identity: String
67 |
68 | override fun onCreate(savedInstanceState: Bundle?) {
69 | super.onCreate(savedInstanceState)
70 | setContentView(R.layout.activity_message)
71 | createUI()
72 | }
73 |
74 | override fun onDestroy() {
75 | channel?.removeListener(this@MessageActivity);
76 | super.onDestroy();
77 | }
78 |
79 | override fun onResume() {
80 | super.onResume()
81 | if (intent != null) {
82 | channel = intent.getParcelableExtra(Constants.EXTRA_CHANNEL)
83 | if (channel != null) {
84 | setupListView(channel!!)
85 | }
86 | }
87 | }
88 |
89 | private fun createUI() {
90 | if (intent != null) {
91 | val basicClient = TwilioApplication.instance.basicClient
92 | identity = basicClient.chatClient!!.myIdentity
93 | val channelSid = intent.getStringExtra(Constants.EXTRA_CHANNEL_SID)
94 | val channelsObject = basicClient.chatClient!!.channels
95 | channelsObject.getChannel(channelSid, ChatCallbackListener() {
96 | channel = it
97 | channel!!.addListener(this@MessageActivity)
98 | this@MessageActivity.title = (if (channel!!.type == ChannelType.PUBLIC) "PUB " else "PRIV ") + channel!!.friendlyName
99 |
100 | setupListView(channel!!)
101 |
102 | // message_list_view.transcriptMode = ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL
103 | // message_list_view.isStackFromBottom = true
104 | // adapter.registerDataSetObserver(object : DataSetObserver() {
105 | // override fun onChanged() {
106 | // super.onChanged()
107 | // message_list_view.setSelection(adapter.count - 1)
108 | // }
109 | // })
110 | setupInput()
111 | })
112 | }
113 | }
114 |
115 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
116 | menuInflater.inflate(R.menu.message, menu)
117 | return true
118 | }
119 |
120 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
121 | when (item.itemId) {
122 | R.id.action_settings -> showChannelSettingsDialog()
123 | }
124 | return super.onOptionsItemSelected(item)
125 | }
126 |
127 | private fun showChannelSettingsDialog() {
128 | selector("Select an option", EDIT_OPTIONS) { _, which ->
129 | when (which) {
130 | NAME_CHANGE -> showChangeNameDialog()
131 | TOPIC_CHANGE -> showChangeTopicDialog()
132 | LIST_MEMBERS -> {
133 | val users = TwilioApplication.instance.basicClient.chatClient!!.users
134 | // Members.getMembersList() way
135 | val members = channel!!.members.membersList
136 | val name = StringBuffer()
137 | for (i in members.indices) {
138 | name.append(members[i].identity)
139 | if (i + 1 < members.size) {
140 | name.append(", ")
141 | }
142 | members[i].getUserDescriptor(ChatCallbackListener() {
143 | debug { "Got user descriptor from member: ${it.identity}" }
144 | })
145 | members[i].getAndSubscribeUser(ChatCallbackListener() {
146 | debug { "Got subscribed user from member: ${it.identity}" }
147 | })
148 | }
149 | TwilioApplication.instance.showToast(name.toString(), Toast.LENGTH_LONG)
150 | // Users.getSubscribedUsers() everybody we subscribed to at the moment
151 | val userList = users.subscribedUsers
152 | val name2 = StringBuffer()
153 | for (i in userList.indices) {
154 | name2.append(userList[i].identity)
155 | if (i + 1 < userList.size) {
156 | name2.append(", ")
157 | }
158 | }
159 | TwilioApplication.instance.showToast("Subscribed users: ${name2.toString()}", Toast.LENGTH_LONG)
160 |
161 | // Get user descriptor via identity
162 | users.getUserDescriptor(channel!!.members.membersList[0].identity, ChatCallbackListener() {
163 | TwilioApplication.instance.showToast("Random user descriptor: ${it.friendlyName}/${it.identity}", Toast.LENGTH_SHORT)
164 | })
165 |
166 | // Users.getChannelUserDescriptors() way - paginated
167 | users.getChannelUserDescriptors(channel!!.sid,
168 | object : CallbackListener>() {
169 | override fun onSuccess(userDescriptorPaginator: Paginator) {
170 | getUsersPage(userDescriptorPaginator)
171 | }
172 | })
173 |
174 | // Channel.getMemberByIdentity() for finding the user in all channels
175 | val members2 = TwilioApplication.instance.basicClient.chatClient!!.channels.getMembersByIdentity(channel!!.members.membersList[0].identity)
176 | val name3 = StringBuffer()
177 | for (i in members2.indices) {
178 | name3.append(members2[i].identity + " in " + members2[i].channel.friendlyName)
179 | if (i + 1 < members2.size) {
180 | name3.append(", ")
181 | }
182 | }
183 | //TwilioApplication.get().showToast("Random user in all channels: "+name3.toString(), Toast.LENGTH_LONG);
184 | }
185 | INVITE_MEMBER -> showInviteMemberDialog()
186 | ADD_MEMBER -> showAddMemberDialog()
187 | REMOVE_MEMBER -> showRemoveMemberDialog()
188 | LEAVE -> channel!!.leave(ToastStatusListener(
189 | "Successfully left channel", "Error leaving channel") {
190 | finish()
191 | })
192 | CHANNEL_DESTROY -> channel!!.destroy(ToastStatusListener(
193 | "Successfully destroyed channel", "Error destroying channel") {
194 | finish()
195 | })
196 | CHANNEL_ATTRIBUTE -> try {
197 | TwilioApplication.instance.showToast(channel!!.attributes.toString())
198 | } catch (e: JSONException) {
199 | TwilioApplication.instance.showToast("JSON exception in channel attributes")
200 | }
201 | SET_CHANNEL_UNIQUE_NAME -> showChangeUniqueNameDialog()
202 | GET_CHANNEL_UNIQUE_NAME -> TwilioApplication.instance.showToast(channel!!.uniqueName)
203 | GET_MESSAGE_BY_INDEX -> channel!!.messages.getMessageByIndex(channel!!.messages.lastConsumedMessageIndex!!, ChatCallbackListener() {
204 | TwilioApplication.instance.showToast("SUCCESS GET MESSAGE BY IDX")
205 | error { "MESSAGES ${it.messages.toString()}, CHANNEL ${it.channel.sid}" }
206 | })
207 | SET_ALL_CONSUMED -> channel!!.messages.setAllMessagesConsumedWithResult(ChatCallbackListener()
208 | {unread -> TwilioApplication.instance.showToast("$unread messages still unread")})
209 | SET_NONE_CONSUMED -> channel!!.messages.setNoMessagesConsumedWithResult(ChatCallbackListener()
210 | {unread -> TwilioApplication.instance.showToast("${unread ?: "All"} messages still unread")})
211 | DISABLE_PUSHES -> channel!!.setNotificationLevel(Channel.NotificationLevel.MUTED, ToastStatusListener(
212 | "Successfully disabled pushes", "Error disabling pushes") {
213 | finish()
214 | })
215 | ENABLE_PUSHES -> channel!!.setNotificationLevel(Channel.NotificationLevel.DEFAULT, ToastStatusListener(
216 | "Successfully enabled pushes", "Error enabling pushes") {
217 | finish()
218 | })
219 | }
220 | }
221 | }
222 |
223 | private fun getUsersPage(userDescriptorPaginator: Paginator) {
224 | for (u in userDescriptorPaginator.items) {
225 | u.subscribe(ChatCallbackListener() {
226 | debug { "${it.identity} is a subscribed user now" }
227 | })
228 | }
229 | if (userDescriptorPaginator.hasNextPage()) {
230 | userDescriptorPaginator.requestNextPage(ChatCallbackListener>() {
231 | getUsersPage(it)
232 | })
233 | }
234 | }
235 |
236 | private fun showChangeNameDialog() {
237 | alert(R.string.title_update_friendly_name) {
238 | customView {
239 | val friendly_name = editText { text.append(channel!!.friendlyName) }
240 | positiveButton(R.string.update) {
241 | val friendlyName = friendly_name.text.toString()
242 | debug { friendlyName }
243 | channel!!.setFriendlyName(friendlyName, ToastStatusListener(
244 | "Successfully changed name", "Error changing name"))
245 | }
246 | negativeButton(R.string.cancel) {}
247 | }
248 | }.show()
249 | }
250 |
251 | private fun showChangeTopicDialog() {
252 | alert(R.string.title_update_topic) {
253 | customView {
254 | val topic = editText { text.append(channel!!.attributes.toString()) }
255 | positiveButton(R.string.change_topic) {
256 | val topicText = topic.text.toString()
257 | debug { topicText }
258 |
259 | try { // @todo Get attributes to update
260 | JSONObject().apply {
261 | put("Topic", topicText)
262 | channel!!.setAttributes(Attributes(this), ToastStatusListener(
263 | "Attributes were set successfullly.",
264 | "Setting attributes failed"))
265 | }
266 | } catch (ignored: JSONException) {
267 | // whatever
268 | }
269 | }
270 | negativeButton(R.string.cancel) {}
271 | }
272 | }.show()
273 | }
274 |
275 | private fun showInviteMemberDialog() {
276 | alert(R.string.title_invite_member) {
277 | customView {
278 | val member = editText { hint = "Enter user id" }
279 | positiveButton(R.string.invite_member) {
280 | val memberName = member.text.toString()
281 | debug { memberName }
282 | channel!!.members.inviteByIdentity(memberName, ToastStatusListener(
283 | "Invited user to channel",
284 | "Error in inviteByIdentity"))
285 | }
286 | negativeButton(R.string.cancel) {}
287 | }
288 | }.show()
289 | }
290 |
291 | private fun showAddMemberDialog() {
292 | alert(R.string.title_add_member) {
293 | customView {
294 | val member = editText { hint = "Enter user id" }
295 | positiveButton(R.string.invite_member) {
296 | val memberName = member.text.toString()
297 | debug { memberName }
298 | channel!!.members.addByIdentity(memberName, ToastStatusListener(
299 | "Successful addByIdentity",
300 | "Error adding member"))
301 | }
302 | negativeButton(R.string.cancel) {}
303 | }
304 | }.show()
305 | }
306 |
307 | private fun showRemoveMemberDialog() {
308 | alert("Remove members") {
309 | customView {
310 | verticalLayout {
311 | val view = recyclerView {}.lparams(width = dip(250), height = matchParent)
312 | negativeButton(R.string.cancel) {}
313 |
314 | view.adapter = SimpleRecyclerAdapter(
315 | ItemClickListener { member: Member, _, _ ->
316 | channel!!.members.remove(member, ToastStatusListener(
317 | "Successful removeMember operation",
318 | "Error in removeMember operation"))
319 | // @todo update memberList here
320 | },
321 | object : SimpleRecyclerAdapter.CreateViewHolder() {
322 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettableViewHolder {
323 | return MemberViewHolder(this@MessageActivity, parent)
324 | }
325 | },
326 | channel!!.members.membersList)
327 |
328 | view.layoutManager = LinearLayoutManager(this@MessageActivity).apply {
329 | orientation = LinearLayoutManager.VERTICAL
330 | }
331 | }
332 | }
333 | }.show()
334 | }
335 |
336 | private fun showUpdateMessageDialog(message: Message) {
337 | alert(R.string.title_update_message) {
338 | customView {
339 | val messageText = editText { text.append(message.messageBody) }
340 | positiveButton(R.string.update) {
341 | val text = messageText.text.toString()
342 | debug { text }
343 | message.updateMessageBody(text, ToastStatusListener(
344 | "Success updating message",
345 | "Error updating message") {
346 | // @todo only need to update one message body
347 | loadAndShowMessages()
348 | })
349 | }
350 | negativeButton(R.string.cancel) {}
351 | }
352 | }.show()
353 | }
354 |
355 | private fun showUpdateMessageAttributesDialog(message: Message) {
356 | alert(R.string.title_update_attributes) {
357 | customView {
358 | val messageAttrText = editText { text.append(message.attributes.toString()) }
359 | positiveButton(R.string.update) {
360 | val text = messageAttrText.text.toString()
361 | debug { text }
362 | try {
363 | JSONObject(text).apply {
364 | message.setAttributes(Attributes(this), ToastStatusListener(
365 | "Success updating message attributes",
366 | "Error updating message attributes") {
367 | // @todo only need to update one message
368 | loadAndShowMessages()
369 | })
370 | }
371 | } catch (e: JSONException) {
372 | error { "Invalid JSON attributes entered, using old value" }
373 | }
374 | }
375 | negativeButton(R.string.cancel) {}
376 | }
377 | }.show()
378 | }
379 |
380 | private fun showChangeUniqueNameDialog() {
381 | alert("Update channel unique name") {
382 | customView {
383 | val uniqueNameText = editText { text.append(channel!!.uniqueName) }
384 | positiveButton(R.string.update) {
385 | val uniqueName = uniqueNameText.text.toString()
386 | debug { uniqueName }
387 | channel!!.setUniqueName(uniqueName, ChatStatusListener());
388 | }
389 | negativeButton(R.string.cancel) {}
390 | }
391 | }.show()
392 | }
393 |
394 | private fun loadAndShowMessages() {
395 | channel!!.messages?.getLastMessages(50, ChatCallbackListener>() {
396 | messageItemList.clear()
397 | val members = channel!!.members
398 | if (it.isNotEmpty()) {
399 | for (i in it.indices) {
400 | messageItemList.add(MessageItem(it[i], members, identity))
401 | }
402 | }
403 | adapter.clear()
404 | adapter.addItems(messageItemList)
405 | adapter.notifyDataSetChanged()
406 | })
407 | }
408 |
409 | private fun setupInput() {
410 | // Setup our input methods. Enter key on the keyboard or pushing the send button
411 | messageInput.apply {
412 | addTextChangedListener(object : TextWatcher {
413 | override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
414 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
415 | override fun afterTextChanged(s: Editable) {
416 | if (channel != null) {
417 | channel!!.typing()
418 | }
419 | }
420 | })
421 |
422 | setOnEditorActionListener { _, actionId, keyEvent ->
423 | if (actionId == EditorInfo.IME_NULL && keyEvent.action == KeyEvent.ACTION_DOWN) {
424 | sendMessage()
425 | }
426 | true
427 | }
428 | }
429 |
430 | sendButton.apply {
431 | setOnClickListener { sendMessage() }
432 |
433 | setOnLongClickListener {
434 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
435 | addCategory(Intent.CATEGORY_OPENABLE)
436 | type = "*/*"
437 | }
438 | this@MessageActivity.startActivityForResult(intent, FILE_REQUEST)
439 | true
440 | }
441 | }
442 | }
443 |
444 | private inner class CustomMessageComparator : Comparator {
445 | override fun compare(lhs: Message?, rhs: Message?): Int {
446 | if (lhs == null) {
447 | return if (rhs == null) 0 else -1
448 | }
449 | if (rhs == null) {
450 | return 1
451 | }
452 | return lhs.dateCreated.compareTo(rhs.dateCreated)
453 | }
454 | }
455 |
456 | private fun setupListView(channel: Channel) {
457 | // message_list_view.viewTreeObserver.addOnScrollChangedListener {
458 | // if (message_list_view.lastVisiblePosition >= 0 && message_list_view.lastVisiblePosition < adapter.itemCount) {
459 | // val item = adapter.getItem(message_list_view.lastVisiblePosition)
460 | // if (item != null && messagesObject != null)
461 | // channel.messages.advanceLastConsumedMessageIndex(
462 | // item.message.messageIndex)
463 | // }
464 | // }
465 |
466 | adapter = SimpleRecyclerAdapter(
467 | ItemClickListener { _: MessageItem, viewHolder, _ ->
468 | (viewHolder as MessageViewHolder).toggleDateVisibility()
469 | },
470 | object : SimpleRecyclerAdapter.CreateViewHolder() {
471 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettableViewHolder {
472 | return MessageViewHolder(this@MessageActivity, parent);
473 | }
474 | })
475 |
476 | adapter.setLongClickListener(
477 | ItemLongClickListener { message: MessageItem, _, _ ->
478 | selector("Select an option", MESSAGE_OPTIONS) { dialog, which ->
479 | when (which) {
480 | REMOVE -> {
481 | dialog.cancel()
482 | channel.messages.removeMessage(
483 | message.message, ToastStatusListener(
484 | "Successfully removed message. It should be GONE!!",
485 | "Error removing message") {
486 | messageItemList.remove(message)
487 | adapter.notifyDataSetChanged()
488 | })
489 | }
490 | EDIT -> showUpdateMessageDialog(message.message)
491 | GET_ATTRIBUTES -> {
492 | try {
493 | TwilioApplication.instance.showToast(message.message.attributes.toString())
494 | } catch (e: JSONException) {
495 | TwilioApplication.instance.showToast("Error parsing message attributes")
496 | }
497 | }
498 | SET_ATTRIBUTES -> showUpdateMessageAttributesDialog(message.message)
499 | }
500 | }
501 | true
502 | }
503 | )
504 |
505 | message_list_view.adapter = adapter
506 | message_list_view.layoutManager = LinearLayoutManager(this).apply {
507 | orientation = LinearLayoutManager.VERTICAL
508 | }
509 |
510 | loadAndShowMessages()
511 | }
512 |
513 | private fun sendMessage(text: String) {
514 | channel!!.messages.sendMessage(Message.options().withBody(text), ChatCallbackListener() {
515 | TwilioApplication.instance.showToast("Successfully sent message");
516 | adapter.notifyDataSetChanged()
517 | messageInput.setText("")
518 | })
519 | }
520 |
521 | private fun sendMessage() {
522 | val input = messageInput.text.toString()
523 | if (input != "") {
524 | sendMessage(input)
525 | }
526 |
527 | messageInput.requestFocus()
528 | }
529 |
530 | /// Send media message
531 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
532 | if (requestCode == FILE_REQUEST && resultCode == Activity.RESULT_OK) {
533 | debug { "Uri: ${data?.data}" }
534 |
535 | startService(
536 | MediaService.EXTRA_ACTION to MediaService.EXTRA_ACTION_UPLOAD,
537 | MediaService.EXTRA_CHANNEL to channel as Parcelable,
538 | MediaService.EXTRA_MEDIA_URI to data?.data.toString())
539 | }
540 | }
541 |
542 | override fun onMessageAdded(message: Message) {
543 | setupListView(channel!!)
544 |
545 | startService(
546 | MediaService.EXTRA_ACTION to MediaService.EXTRA_ACTION_DOWNLOAD,
547 | MediaService.EXTRA_CHANNEL to channel as Parcelable,
548 | MediaService.EXTRA_MESSAGE_INDEX to message.messageIndex)
549 | }
550 |
551 | override fun onMessageUpdated(message: Message?, reason: Message.UpdateReason) {
552 | if (message != null) {
553 | TwilioApplication.instance.showToast("onMessageUpdated for ${message.sid}, changed because of ${reason}")
554 | } else {
555 | debug { "Received onMessageUpdated" }
556 | }
557 | }
558 |
559 | override fun onMessageDeleted(message: Message?) {
560 | if (message != null) {
561 | TwilioApplication.instance.showToast("onMessageDeleted for ${message.sid}")
562 | } else {
563 | debug { "Received onMessageDeleted." }
564 | }
565 | }
566 |
567 | override fun onMemberAdded(member: Member?) {
568 | if (member != null) {
569 | TwilioApplication.instance.showToast("${member.identity} joined")
570 | }
571 | }
572 |
573 | override fun onMemberUpdated(member: Member?, reason: Member.UpdateReason) {
574 | if (member != null) {
575 | TwilioApplication.instance.showToast("${member.identity} changed because of ${reason}")
576 | }
577 | }
578 |
579 | override fun onMemberDeleted(member: Member?) {
580 | if (member != null) {
581 | TwilioApplication.instance.showToast("${member.identity} deleted")
582 | }
583 | }
584 |
585 | override fun onTypingStarted(channel: Channel?, member: Member?) {
586 | if (member != null) {
587 | val text = "${member.identity} is typing ..."
588 | typingIndicator.text = text
589 | typingIndicator.setTextColor(Color.RED)
590 | debug { text }
591 | }
592 | }
593 |
594 | override fun onTypingEnded(channel: Channel?, member: Member?) {
595 | if (member != null) {
596 | typingIndicator.text = null
597 | debug { "${member.identity} finished typing" }
598 | }
599 | }
600 |
601 | override fun onSynchronizationChanged(channel: Channel) {
602 | debug { "Received onSynchronizationChanged callback for ${channel.friendlyName}" }
603 | }
604 |
605 | data class MessageItem(val message: Message, val members: Members, internal var currentUser: String);
606 |
607 | companion object {
608 | private val MESSAGE_OPTIONS = listOf("Remove", "Edit", "Get Attributes", "Edit Attributes")
609 | private val REMOVE = 0
610 | private val EDIT = 1
611 | private val GET_ATTRIBUTES = 2
612 | private val SET_ATTRIBUTES = 3
613 |
614 | private val EDIT_OPTIONS = listOf("Change Friendly Name", "Change Topic", "List Members", "Invite Member", "Add Member", "Remove Member", "Leave", "Destroy", "Get Attributes", "Change Unique Name", "Get Unique Name", "Get message index 0", "Set all consumed", "Set none consumed", "Disable Pushes", "Enable Pushes")
615 | private val NAME_CHANGE = 0
616 | private val TOPIC_CHANGE = 1
617 | private val LIST_MEMBERS = 2
618 | private val INVITE_MEMBER = 3
619 | private val ADD_MEMBER = 4
620 | private val REMOVE_MEMBER = 5
621 | private val LEAVE = 6
622 | private val CHANNEL_DESTROY = 7
623 | private val CHANNEL_ATTRIBUTE = 8
624 | private val SET_CHANNEL_UNIQUE_NAME = 9
625 | private val GET_CHANNEL_UNIQUE_NAME = 10
626 | private val GET_MESSAGE_BY_INDEX = 11
627 | private val SET_ALL_CONSUMED = 12
628 | private val SET_NONE_CONSUMED = 13
629 | private val DISABLE_PUSHES = 14
630 | private val ENABLE_PUSHES = 15
631 |
632 | private val FILE_REQUEST = 1000;
633 | }
634 | }
635 |
--------------------------------------------------------------------------------