├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── twilio │ │ └── twiliochat │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── twilio │ │ │ └── twiliochat │ │ │ ├── application │ │ │ ├── AlertDialogHandler.java │ │ │ ├── SessionManager.java │ │ │ └── TwilioChatApplication.java │ │ │ ├── chat │ │ │ ├── ChatClientBuilder.java │ │ │ ├── ChatClientManager.java │ │ │ ├── MainChatActivity.java │ │ │ ├── MainChatFragment.java │ │ │ ├── accesstoken │ │ │ │ ├── AccessTokenFetcher.java │ │ │ │ └── TokenRequest.java │ │ │ ├── channels │ │ │ │ ├── ChannelAdapter.java │ │ │ │ ├── ChannelExtractor.java │ │ │ │ ├── ChannelManager.java │ │ │ │ ├── CustomChannelComparator.java │ │ │ │ └── LoadChannelListener.java │ │ │ ├── listeners │ │ │ │ ├── InputOnClickListener.java │ │ │ │ └── TaskCompletionListener.java │ │ │ └── messages │ │ │ │ ├── ChatMessage.java │ │ │ │ ├── DateFormatter.java │ │ │ │ ├── JoinedStatusMessage.java │ │ │ │ ├── LeftStatusMessage.java │ │ │ │ ├── MessageAdapter.java │ │ │ │ ├── StatusMessage.java │ │ │ │ └── UserMessage.java │ │ │ └── landing │ │ │ ├── LaunchActivity.java │ │ │ └── LoginActivity.java │ └── res │ │ ├── drawable-hdpi │ │ ├── add_channel_button.png │ │ ├── home_bg.png │ │ ├── ic_user.png │ │ ├── landing_logo.png │ │ ├── password_field_bg.png │ │ ├── user_panel_line.png │ │ └── username_field_bg.png │ │ ├── drawable-mdpi │ │ ├── add_channel_button.png │ │ ├── home_bg.png │ │ ├── ic_user.png │ │ ├── landing_logo.png │ │ ├── password_field_bg.png │ │ ├── user_panel_line.png │ │ └── username_field_bg.png │ │ ├── drawable-xhdpi │ │ ├── add_channel_button.png │ │ ├── home_bg.png │ │ ├── ic_user.png │ │ ├── landing_logo.png │ │ ├── password_field_bg.png │ │ ├── user_panel_line.png │ │ └── username_field_bg.png │ │ ├── drawable-xxhdpi │ │ ├── add_channel_button.png │ │ ├── home_bg.png │ │ ├── ic_user.png │ │ ├── landing_logo.png │ │ ├── password_field_bg.png │ │ ├── user_panel_line.png │ │ └── username_field_bg.png │ │ ├── drawable-xxxhdpi │ │ ├── add_channel_button.png │ │ ├── home_bg.png │ │ ├── ic_user.png │ │ ├── landing_logo.png │ │ ├── password_field_bg.png │ │ ├── user_panel_line.png │ │ └── username_field_bg.png │ │ ├── drawable │ │ └── message_input_shape.xml │ │ ├── layout │ │ ├── activity_login.xml │ │ ├── activity_main_chat.xml │ │ ├── app_bar_main_chat.xml │ │ ├── channel.xml │ │ ├── fragment_main_chat.xml │ │ ├── input_dialog_view.xml │ │ ├── message.xml │ │ ├── nav_header_main_chat.xml │ │ └── status_message.xml │ │ ├── menu │ │ └── main_chat.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── drawables.xml │ │ ├── keys.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── twilio │ └── twiliochat │ ├── DateFormatterTest.java │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | /build 18 | 19 | # Local configuration file (sdk path, etc) 20 | /local.properties 21 | 22 | # Android Studio Files 23 | *.iml 24 | .idea/ 25 | .DS_Store 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | dist: trusty 3 | android: 4 | components: 5 | - platform-tools 6 | - tools 7 | - build-tools-29.0.2 8 | - android-28 9 | - sys-img-armeabi-v7a-android-22 10 | - extra-google-google_play_services 11 | - extra-google-m2repository 12 | - extra-android-m2repository 13 | script: 14 | - ./gradlew build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 TwilioDevEd 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | # Important Notice 6 | 7 | We intend to sunset the Programmable Chat API on July 25, 2022 to focus on the next generation of chat: the [Twilio Conversations API](https://www.twilio.com/docs/conversations). Find out about the [EOL process](https://www.twilio.com/changelog/programmable-chat-end-of-life). We have also prepared [this Migration Guide](https://www.twilio.com/docs/conversations/migrating-chat-conversations) to assist in the transition from Chat to Conversations. 8 | 9 | # Twilio chat - Android 10 | [![Build Status](https://travis-ci.org/TwilioDevEd/twiliochat-android.svg?branch=master)](https://travis-ci.org/TwilioDevEd/twiliochat-android) 11 | 12 | Learn to implement a simple chat application using Twilio Programmable Chat Client 13 | 14 | [Read the full tutorial here!](https://www.twilio.com/docs/tutorials/walkthrough/chat/android/java) 15 | 16 | ### Local development 17 | 18 | 1. Clone the repository. 19 | 20 | ```bash 21 | $ git clone https://github.com/TwilioDevEd/twiliochat-android.git 22 | $ cd twiliochat-android 23 | ``` 24 | 25 | 1. This application was developed using [Android Studio](http://developer.android.com/tools/studio/index.html). 26 | So if you are using a different tool like Eclipse with ADT, there might be some additional 27 | steps you need to follow. If you are using Android Studio just open the project 28 | using the IDE. 29 | 30 | 1. [Twilio's Programmable Chat Client](https://www.twilio.com/docs/api/chat) requires an 31 | [access token](https://www.twilio.com/docs/chat/identity) generated using your 32 | Twilio credentials in order to connect. First we need to setup a server that will generate this token 33 | for the mobile application to use. We have created web versions of Twilio Chat, you can use any of these 34 | applications to generate the token that this mobile app requires, just pick you favorite flavor: 35 | 36 | * [PHP - Laravel](https://github.com/TwilioDevEd/twiliochat-laravel) 37 | * [C# - .NET MVC](https://github.com/TwilioDevEd/twiliochat-csharp) 38 | * [Java - Servlets](https://github.com/TwilioDevEd/twiliochat-servlets) 39 | * [JS - Node](https://github.com/TwilioDevEd/twiliochat-node) 40 | 41 | Look for instructions on how to setup these servers in any of the links above. 42 | 43 | 1. Once you have the server running (from the previous step), you need to edit one file in this android 44 | application. 45 | 46 | ``` 47 | ProjectRoot(app) -> res -> values -> keys.xml (on the Android Studio) 48 | or 49 | ProjectRoot/app/src/main/res/values/keys.xml (on the file system) 50 | ``` 51 | This file contains the `token_url` key. The default values is `http://10.0.2.2:8000/token`. This 52 | address refers to the host machine loopback interface (127.0.0.1) when running this application 53 | in the android emulator. You must change this value to match the address of your server running 54 | the token generation application. We are using the [PHP - Laravel](https://github.com/TwilioDevEd/twiliochat-laravel) 55 | version in this case, that's why we use port 8000. 56 | 57 | ***Note:*** In some operating systems you need to specify the address for the development server 58 | when you run the Laravel application, here's an example: 59 | ``` 60 | $ php artisan serve --host=127.0.0.1 61 | ``` 62 | 63 | 1. Now Twilio Chat is ready to go. Run the application on the android emulator or your own device. 64 | Make sure that you have properly set up the token generation server and the `token_url` key. 65 | To run the application in a real device you'll need to expose your local token generation server 66 | by manually forwarding ports, or using a tool like [ngrok](https://ngrok.com/). 67 | If you decide to work with ngrok, your keys.xml file should hold a key like the following: 68 | 69 | ``` 70 | "http://.ngrok.io/token" 71 | ``` 72 | No need to specify the port in this url, as ngrok will forward the request to the specified port. 73 | 74 | 75 | ## Meta 76 | * No warranty expressed or implied. Software is as is. Diggity. 77 | * [MIT License](http://www.opensource.org/licenses/mit-license.html) 78 | * Lovingly crafted by Twilio Developer Education. 79 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | defaultConfig { 7 | applicationId "com.twilio.twiliochat" 8 | minSdkVersion 19 9 | targetSdkVersion 26 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation 'androidx.appcompat:appcompat:1.0.0' 24 | implementation 'com.google.android.material:material:1.0.0' 25 | implementation 'joda-time:joda-time:2.9.1' 26 | implementation 'com.mcxiaoke.volley:library-aar:1.0.1' 27 | testImplementation 'junit:junit:4.12' 28 | implementation "com.twilio:chat-android:6.0.0" 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/mceli/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/twilio/twiliochat/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat; 2 | 3 | /** 4 | * Testing Fundamentals 5 | */ 6 | public class ApplicationTest { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/application/AlertDialogHandler.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.application; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.view.ContextThemeWrapper; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.widget.EditText; 10 | import android.widget.TextView; 11 | 12 | import com.twilio.twiliochat.R; 13 | import com.twilio.twiliochat.chat.listeners.InputOnClickListener; 14 | 15 | public class AlertDialogHandler { 16 | public static void displayAlertWithMessage(String message, Context context) { 17 | AlertDialog.Builder builder = 18 | new AlertDialog.Builder(new ContextThemeWrapper(context, R.style.AlertDialogCustom)); 19 | builder.setMessage(message).setCancelable(false).setPositiveButton("OK", null); 20 | 21 | AlertDialog alert = builder.create(); 22 | alert.show(); 23 | } 24 | 25 | public static void displayCancellableAlertWithHandler(String message, Context context, 26 | DialogInterface.OnClickListener handler) { 27 | AlertDialog.Builder builder = 28 | new AlertDialog.Builder(new ContextThemeWrapper(context, R.style.AlertDialogCustom)); 29 | builder.setMessage(message).setPositiveButton("OK", handler).setNegativeButton("Cancel", null); 30 | 31 | AlertDialog alert = builder.create(); 32 | alert.show(); 33 | } 34 | 35 | public static void displayInputDialog(String message, Context context, 36 | final InputOnClickListener handler) { 37 | LayoutInflater li = LayoutInflater.from(context); 38 | View promptsView = li.inflate(R.layout.input_dialog_view, null); 39 | 40 | AlertDialog.Builder builder = 41 | new AlertDialog.Builder(new ContextThemeWrapper(context, R.style.AlertDialogCustom)); 42 | builder.setView(promptsView); 43 | 44 | final EditText userInput = (EditText) promptsView.findViewById(R.id.editTextUserInput); 45 | final TextView promptMessage = (TextView) promptsView.findViewById(R.id.textViewMessage); 46 | promptMessage.setText(message); 47 | 48 | 49 | builder.setCancelable(false).setPositiveButton("OK", new DialogInterface.OnClickListener() { 50 | @Override 51 | public void onClick(DialogInterface dialog, int which) { 52 | handler.onClick(userInput.getText().toString()); 53 | } 54 | }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { 55 | public void onClick(DialogInterface dialog, int id) { 56 | dialog.cancel(); 57 | } 58 | }); 59 | 60 | AlertDialog alertDialog = builder.create(); 61 | 62 | alertDialog.show(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/application/SessionManager.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.application; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.content.SharedPreferences.Editor; 6 | 7 | import java.util.HashMap; 8 | 9 | public class SessionManager { 10 | public static final String KEY_USERNAME = "username"; 11 | private static final String PREF_NAME = "TWILIOCHAT"; 12 | private static final String IS_LOGGED_IN = "IsLoggedIn"; 13 | private static SessionManager instance = 14 | new SessionManager(TwilioChatApplication.get().getApplicationContext()); 15 | SharedPreferences pref; 16 | Editor editor; 17 | Context context; 18 | int PRIVATE_MODE = 0; 19 | 20 | private SessionManager(Context context) { 21 | this.context = context; 22 | pref = context.getSharedPreferences(PREF_NAME, PRIVATE_MODE); 23 | editor = pref.edit(); 24 | } 25 | 26 | public static SessionManager getInstance() { 27 | return instance; 28 | } 29 | 30 | public void createLoginSession(String username) { 31 | editor.putBoolean(IS_LOGGED_IN, true); 32 | editor.putString(KEY_USERNAME, username); 33 | // commit changes 34 | editor.commit(); 35 | } 36 | 37 | public HashMap getUserDetails() { 38 | HashMap user = new HashMap(); 39 | user.put(KEY_USERNAME, pref.getString(KEY_USERNAME, null)); 40 | 41 | return user; 42 | } 43 | 44 | public String getUsername() { 45 | return pref.getString(KEY_USERNAME, null); 46 | } 47 | 48 | public void logoutUser() { 49 | editor = editor.clear(); 50 | editor.commit(); 51 | } 52 | 53 | public boolean isLoggedIn() { 54 | return pref.getBoolean(IS_LOGGED_IN, false); 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/application/TwilioChatApplication.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.application; 2 | 3 | import android.app.Application; 4 | 5 | import com.twilio.twiliochat.chat.ChatClientManager; 6 | 7 | public class TwilioChatApplication extends Application { 8 | private static TwilioChatApplication instance; 9 | private ChatClientManager basicClient; 10 | 11 | public static TwilioChatApplication get() { 12 | return instance; 13 | } 14 | 15 | public ChatClientManager getChatClientManager() { 16 | return this.basicClient; 17 | } 18 | 19 | public static final String TAG = "TwilioChat"; 20 | 21 | @Override 22 | public void onCreate() { 23 | super.onCreate(); 24 | 25 | TwilioChatApplication.instance = this; 26 | basicClient = new ChatClientManager(getApplicationContext()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/ChatClientBuilder.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat; 2 | 3 | import android.content.Context; 4 | 5 | import com.twilio.chat.CallbackListener; 6 | import com.twilio.chat.ChatClient; 7 | import com.twilio.chat.ErrorInfo; 8 | import com.twilio.twiliochat.chat.listeners.TaskCompletionListener; 9 | 10 | public class ChatClientBuilder extends CallbackListener { 11 | 12 | private Context context; 13 | private TaskCompletionListener buildListener; 14 | 15 | public ChatClientBuilder(Context context) { 16 | this.context = context; 17 | } 18 | 19 | public void build(String token, final TaskCompletionListener listener) { 20 | ChatClient.Properties props = 21 | new ChatClient.Properties.Builder() 22 | .setRegion("us1") 23 | .createProperties(); 24 | 25 | this.buildListener = listener; 26 | ChatClient.create(context.getApplicationContext(), 27 | token, 28 | props, 29 | this); 30 | } 31 | 32 | 33 | @Override 34 | public void onSuccess(ChatClient chatClient) { 35 | this.buildListener.onSuccess(chatClient); 36 | } 37 | 38 | @Override 39 | public void onError(ErrorInfo errorInfo) { 40 | this.buildListener.onError(errorInfo.getMessage()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/ChatClientManager.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat; 2 | 3 | import android.content.Context; 4 | 5 | import com.twilio.chat.ChatClient; 6 | import com.twilio.chat.ChatClientListener; 7 | import com.twilio.twiliochat.chat.accesstoken.AccessTokenFetcher; 8 | import com.twilio.twiliochat.chat.listeners.TaskCompletionListener; 9 | 10 | public class ChatClientManager { 11 | private ChatClient chatClient; 12 | private Context context; 13 | private AccessTokenFetcher accessTokenFetcher; 14 | private ChatClientBuilder chatClientBuilder; 15 | 16 | 17 | public ChatClientManager(Context context) { 18 | this.context = context; 19 | this.accessTokenFetcher = new AccessTokenFetcher(this.context); 20 | this.chatClientBuilder = new ChatClientBuilder(this.context); 21 | } 22 | 23 | public void addClientListener(ChatClientListener listener) { 24 | if (this.chatClient != null) { 25 | this.chatClient.addListener(listener); 26 | } 27 | } 28 | 29 | public ChatClient getChatClient() { 30 | return this.chatClient; 31 | } 32 | 33 | public void setChatClient(ChatClient client) { 34 | this.chatClient = client; 35 | } 36 | 37 | public void connectClient(final TaskCompletionListener listener) { 38 | ChatClient.setLogLevel(android.util.Log.DEBUG); 39 | 40 | accessTokenFetcher.fetch(new TaskCompletionListener() { 41 | @Override 42 | public void onSuccess(String token) { 43 | buildClient(token, listener); 44 | } 45 | 46 | @Override 47 | public void onError(String message) { 48 | if (listener != null) { 49 | listener.onError(message); 50 | } 51 | } 52 | }); 53 | } 54 | 55 | private void buildClient(String token, final TaskCompletionListener listener) { 56 | chatClientBuilder.build(token, new TaskCompletionListener() { 57 | @Override 58 | public void onSuccess(ChatClient chatClient) { 59 | ChatClientManager.this.chatClient = chatClient; 60 | listener.onSuccess(null); 61 | } 62 | 63 | @Override 64 | public void onError(String message) { 65 | listener.onError(message); 66 | } 67 | }); 68 | } 69 | 70 | public void shutdown() { 71 | if(chatClient != null) { 72 | chatClient.shutdown(); 73 | } 74 | } 75 | 76 | public AccessTokenFetcher getAccessTokenFetcher() { 77 | return accessTokenFetcher; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/MainChatActivity.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat; 2 | 3 | import android.app.Activity; 4 | import android.app.ProgressDialog; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.content.Intent; 8 | import android.content.res.Resources; 9 | import android.os.Bundle; 10 | import android.os.Handler; 11 | import android.util.Log; 12 | import android.view.Menu; 13 | import android.view.MenuItem; 14 | import android.view.View; 15 | import android.widget.AdapterView; 16 | import android.widget.Button; 17 | import android.widget.ListView; 18 | import android.widget.TextView; 19 | 20 | import androidx.appcompat.app.ActionBarDrawerToggle; 21 | import androidx.appcompat.app.AppCompatActivity; 22 | import androidx.appcompat.widget.Toolbar; 23 | import androidx.core.view.GravityCompat; 24 | import androidx.drawerlayout.widget.DrawerLayout; 25 | import androidx.fragment.app.FragmentTransaction; 26 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 27 | 28 | import com.twilio.chat.Channel; 29 | import com.twilio.chat.ChatClient; 30 | import com.twilio.chat.ChatClientListener; 31 | import com.twilio.chat.ErrorInfo; 32 | import com.twilio.chat.StatusListener; 33 | import com.twilio.chat.User; 34 | import com.twilio.twiliochat.R; 35 | import com.twilio.twiliochat.application.AlertDialogHandler; 36 | import com.twilio.twiliochat.application.SessionManager; 37 | import com.twilio.twiliochat.application.TwilioChatApplication; 38 | import com.twilio.twiliochat.chat.channels.ChannelAdapter; 39 | import com.twilio.twiliochat.chat.channels.ChannelManager; 40 | import com.twilio.twiliochat.chat.channels.LoadChannelListener; 41 | import com.twilio.twiliochat.chat.listeners.InputOnClickListener; 42 | import com.twilio.twiliochat.chat.listeners.TaskCompletionListener; 43 | import com.twilio.twiliochat.landing.LoginActivity; 44 | 45 | import java.util.List; 46 | 47 | public class MainChatActivity extends AppCompatActivity implements ChatClientListener { 48 | private Context context; 49 | private Activity mainActivity; 50 | private Button logoutButton; 51 | private Button addChannelButton; 52 | private TextView usernameTextView; 53 | private ChatClientManager chatClientManager; 54 | private ListView channelsListView; 55 | private ChannelAdapter channelAdapter; 56 | private ChannelManager channelManager; 57 | private MainChatFragment chatFragment; 58 | private DrawerLayout drawer; 59 | private ProgressDialog progressDialog; 60 | private MenuItem leaveChannelMenuItem; 61 | private MenuItem deleteChannelMenuItem; 62 | private SwipeRefreshLayout refreshLayout; 63 | 64 | @Override 65 | protected void onDestroy() { 66 | super.onDestroy(); 67 | new Handler().post(new Runnable() { 68 | @Override 69 | public void run() { 70 | chatClientManager.shutdown(); 71 | TwilioChatApplication.get().getChatClientManager().setChatClient(null); 72 | } 73 | }); 74 | } 75 | 76 | @Override 77 | protected void onCreate(Bundle savedInstanceState) { 78 | super.onCreate(savedInstanceState); 79 | setContentView(R.layout.activity_main_chat); 80 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 81 | setSupportActionBar(toolbar); 82 | 83 | drawer = (DrawerLayout) findViewById(R.id.drawer_layout); 84 | ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, 85 | R.string.navigation_drawer_open, R.string.navigation_drawer_close); 86 | drawer.setDrawerListener(toggle); 87 | toggle.syncState(); 88 | 89 | refreshLayout = (SwipeRefreshLayout) findViewById(R.id.refreshLayout); 90 | 91 | FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); 92 | 93 | chatFragment = new MainChatFragment(); 94 | fragmentTransaction.add(R.id.fragment_container, chatFragment); 95 | fragmentTransaction.commit(); 96 | 97 | context = this; 98 | mainActivity = this; 99 | logoutButton = (Button) findViewById(R.id.buttonLogout); 100 | addChannelButton = (Button) findViewById(R.id.buttonAddChannel); 101 | usernameTextView = (TextView) findViewById(R.id.textViewUsername); 102 | channelsListView = (ListView) findViewById(R.id.listViewChannels); 103 | 104 | channelManager = ChannelManager.getInstance(); 105 | setUsernameTextView(); 106 | 107 | setUpListeners(); 108 | checkTwilioClient(); 109 | } 110 | 111 | private void setUpListeners() { 112 | logoutButton.setOnClickListener(new View.OnClickListener() { 113 | @Override 114 | public void onClick(View v) { 115 | promptLogout(); 116 | } 117 | }); 118 | addChannelButton.setOnClickListener(new View.OnClickListener() { 119 | @Override 120 | public void onClick(View v) { 121 | showAddChannelDialog(); 122 | } 123 | }); 124 | refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 125 | @Override 126 | public void onRefresh() { 127 | refreshLayout.setRefreshing(true); 128 | refreshChannels(); 129 | } 130 | }); 131 | channelsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 132 | @Override 133 | public void onItemClick(AdapterView parent, View view, int position, long id) { 134 | setChannel(position); 135 | } 136 | }); 137 | } 138 | 139 | @Override 140 | public void onBackPressed() { 141 | if (drawer.isDrawerOpen(GravityCompat.START)) { 142 | drawer.closeDrawer(GravityCompat.START); 143 | } else { 144 | super.onBackPressed(); 145 | } 146 | } 147 | 148 | @Override 149 | public boolean onCreateOptionsMenu(Menu menu) { 150 | getMenuInflater().inflate(R.menu.main_chat, menu); 151 | this.leaveChannelMenuItem = menu.findItem(R.id.action_leave_channel); 152 | this.leaveChannelMenuItem.setVisible(false); 153 | this.deleteChannelMenuItem = menu.findItem(R.id.action_delete_channel); 154 | this.deleteChannelMenuItem.setVisible(false); 155 | return true; 156 | } 157 | 158 | @Override 159 | public boolean onOptionsItemSelected(MenuItem item) { 160 | int id = item.getItemId(); 161 | 162 | if (id == R.id.action_leave_channel) { 163 | leaveCurrentChannel(); 164 | return true; 165 | } 166 | if (id == R.id.action_delete_channel) { 167 | promptChannelDeletion(); 168 | } 169 | 170 | return super.onOptionsItemSelected(item); 171 | } 172 | 173 | private String getStringResource(int id) { 174 | Resources resources = getResources(); 175 | return resources.getString(id); 176 | } 177 | 178 | private void refreshChannels() { 179 | channelManager.populateChannels(new LoadChannelListener() { 180 | @Override 181 | public void onChannelsFinishedLoading(final List channels) { 182 | runOnUiThread(new Runnable() { 183 | @Override 184 | public void run() { 185 | channelAdapter.setChannels(channels); 186 | refreshLayout.setRefreshing(false); 187 | } 188 | }); 189 | } 190 | }); 191 | } 192 | 193 | private void populateChannels() { 194 | channelManager.setChannelListener(this); 195 | channelManager.populateChannels(new LoadChannelListener() { 196 | @Override 197 | public void onChannelsFinishedLoading(List channels) { 198 | channelAdapter = new ChannelAdapter(mainActivity, channels); 199 | channelsListView.setAdapter(channelAdapter); 200 | MainChatActivity.this.channelManager 201 | .joinOrCreateGeneralChannelWithCompletion(new StatusListener() { 202 | @Override 203 | public void onSuccess() { 204 | runOnUiThread(new Runnable() { 205 | @Override 206 | public void run() { 207 | channelAdapter.notifyDataSetChanged(); 208 | stopActivityIndicator(); 209 | setChannel(0); 210 | } 211 | }); 212 | } 213 | 214 | @Override 215 | public void onError(ErrorInfo errorInfo) { 216 | showAlertWithMessage(getStringResource(R.string.generic_error) + " - " + errorInfo.getMessage()); 217 | } 218 | }); 219 | } 220 | }); 221 | } 222 | 223 | private void setChannel(final int position) { 224 | List channels = channelManager.getChannels(); 225 | if (channels == null) { 226 | return; 227 | } 228 | final Channel currentChannel = chatFragment.getCurrentChannel(); 229 | final Channel selectedChannel = channels.get(position); 230 | if (currentChannel != null && currentChannel.getSid().contentEquals(selectedChannel.getSid())) { 231 | drawer.closeDrawer(GravityCompat.START); 232 | return; 233 | } 234 | hideMenuItems(position); 235 | if (selectedChannel != null) { 236 | showActivityIndicator("Joining " + selectedChannel.getFriendlyName() + " channel"); 237 | if (currentChannel != null && currentChannel.getStatus() == Channel.ChannelStatus.JOINED) { 238 | this.channelManager.leaveChannelWithHandler(currentChannel, new StatusListener() { 239 | @Override 240 | public void onSuccess() { 241 | joinChannel(selectedChannel); 242 | } 243 | 244 | @Override 245 | public void onError(ErrorInfo errorInfo) { 246 | stopActivityIndicator(); 247 | } 248 | }); 249 | return; 250 | } 251 | joinChannel(selectedChannel); 252 | stopActivityIndicator(); 253 | } else { 254 | stopActivityIndicator(); 255 | showAlertWithMessage(getStringResource(R.string.generic_error)); 256 | Log.e(TwilioChatApplication.TAG,"Selected channel out of range"); 257 | } 258 | } 259 | 260 | private void joinChannel(final Channel selectedChannel) { 261 | runOnUiThread(new Runnable() { 262 | @Override 263 | public void run() { 264 | chatFragment.setCurrentChannel(selectedChannel, new StatusListener() { 265 | @Override 266 | public void onSuccess() { 267 | MainChatActivity.this.stopActivityIndicator(); 268 | } 269 | 270 | @Override 271 | public void onError(ErrorInfo errorInfo) { 272 | } 273 | }); 274 | setTitle(selectedChannel.getFriendlyName()); 275 | drawer.closeDrawer(GravityCompat.START); 276 | } 277 | }); 278 | } 279 | 280 | private void hideMenuItems(final int position) { 281 | runOnUiThread(new Runnable() { 282 | @Override 283 | public void run() { 284 | MainChatActivity.this.leaveChannelMenuItem.setVisible(position != 0); 285 | MainChatActivity.this.deleteChannelMenuItem.setVisible(position != 0); 286 | } 287 | }); 288 | } 289 | 290 | private void showAddChannelDialog() { 291 | String message = getStringResource(R.string.new_channel_prompt); 292 | AlertDialogHandler.displayInputDialog(message, context, new InputOnClickListener() { 293 | @Override 294 | public void onClick(String input) { 295 | if (input.length() == 0) { 296 | showAlertWithMessage(getStringResource(R.string.channel_name_required_message)); 297 | return; 298 | } 299 | createChannelWithName(input); 300 | } 301 | }); 302 | } 303 | 304 | private void createChannelWithName(String name) { 305 | name = name.trim(); 306 | if (name.toLowerCase() 307 | .contentEquals(this.channelManager.getDefaultChannelName().toLowerCase())) { 308 | showAlertWithMessage(getStringResource(R.string.channel_name_equals_default_name)); 309 | return; 310 | } 311 | this.channelManager.createChannelWithName(name, new StatusListener() { 312 | @Override 313 | public void onSuccess() { 314 | refreshChannels(); 315 | } 316 | 317 | @Override 318 | public void onError(ErrorInfo errorInfo) { 319 | showAlertWithMessage(getStringResource(R.string.generic_error) + " - " + errorInfo.getMessage()); 320 | } 321 | }); 322 | } 323 | 324 | private void promptChannelDeletion() { 325 | String message = getStringResource(R.string.channel_delete_prompt_message); 326 | AlertDialogHandler.displayCancellableAlertWithHandler(message, context, 327 | new DialogInterface.OnClickListener() { 328 | @Override 329 | public void onClick(DialogInterface dialog, int which) { 330 | deleteCurrentChannel(); 331 | } 332 | }); 333 | } 334 | 335 | private void deleteCurrentChannel() { 336 | Channel currentChannel = chatFragment.getCurrentChannel(); 337 | channelManager.deleteChannelWithHandler(currentChannel, new StatusListener() { 338 | @Override 339 | public void onSuccess() { 340 | } 341 | 342 | @Override 343 | public void onError(ErrorInfo errorInfo) { 344 | showAlertWithMessage(getStringResource(R.string.message_deletion_forbidden)); 345 | } 346 | }); 347 | } 348 | 349 | private void leaveCurrentChannel() { 350 | final Channel currentChannel = chatFragment.getCurrentChannel(); 351 | if (currentChannel.getStatus() == Channel.ChannelStatus.NOT_PARTICIPATING) { 352 | setChannel(0); 353 | return; 354 | } 355 | channelManager.leaveChannelWithHandler(currentChannel, new StatusListener() { 356 | @Override 357 | public void onSuccess() { 358 | setChannel(0); 359 | } 360 | 361 | @Override 362 | public void onError(ErrorInfo errorInfo) { 363 | stopActivityIndicator(); 364 | } 365 | }); 366 | } 367 | 368 | private void checkTwilioClient() { 369 | showActivityIndicator(getStringResource(R.string.loading_channels_message)); 370 | chatClientManager = TwilioChatApplication.get().getChatClientManager(); 371 | if (chatClientManager.getChatClient() == null) { 372 | initializeClient(); 373 | } else { 374 | populateChannels(); 375 | } 376 | } 377 | 378 | private void initializeClient() { 379 | chatClientManager.connectClient(new TaskCompletionListener() { 380 | @Override 381 | public void onSuccess(Void aVoid) { 382 | populateChannels(); 383 | } 384 | 385 | @Override 386 | public void onError(String errorMessage) { 387 | stopActivityIndicator(); 388 | showAlertWithMessage("Client connection error: " + errorMessage); 389 | } 390 | }); 391 | } 392 | 393 | private void promptLogout() { 394 | final String message = getStringResource(R.string.logout_prompt_message); 395 | runOnUiThread(new Runnable() { 396 | @Override 397 | public void run() { 398 | AlertDialogHandler.displayCancellableAlertWithHandler(message, context, 399 | new DialogInterface.OnClickListener() { 400 | @Override 401 | public void onClick(DialogInterface dialog, int which) { 402 | SessionManager.getInstance().logoutUser(); 403 | showLoginActivity(); 404 | } 405 | }); 406 | } 407 | }); 408 | 409 | } 410 | 411 | private void showLoginActivity() { 412 | Intent launchIntent = new Intent(); 413 | launchIntent.setClass(getApplicationContext(), LoginActivity.class); 414 | startActivity(launchIntent); 415 | finish(); 416 | } 417 | 418 | private void setUsernameTextView() { 419 | String username = 420 | SessionManager.getInstance().getUserDetails().get(SessionManager.KEY_USERNAME); 421 | usernameTextView.setText(username); 422 | } 423 | 424 | private void stopActivityIndicator() { 425 | runOnUiThread(new Runnable() { 426 | @Override 427 | public void run() { 428 | if (progressDialog.isShowing()) { 429 | progressDialog.dismiss(); 430 | } 431 | } 432 | }); 433 | } 434 | 435 | private void showActivityIndicator(final String message) { 436 | runOnUiThread(new Runnable() { 437 | @Override 438 | public void run() { 439 | progressDialog = new ProgressDialog(MainChatActivity.this.mainActivity); 440 | progressDialog.setMessage(message); 441 | progressDialog.show(); 442 | progressDialog.setCanceledOnTouchOutside(false); 443 | progressDialog.setCancelable(false); 444 | } 445 | }); 446 | } 447 | 448 | private void showAlertWithMessage(final String message) { 449 | runOnUiThread(new Runnable() { 450 | @Override 451 | public void run() { 452 | AlertDialogHandler.displayAlertWithMessage(message, context); 453 | } 454 | }); 455 | } 456 | 457 | 458 | 459 | @Override 460 | public void onChannelAdded(Channel channel) { 461 | Log.d(TwilioChatApplication.TAG,"Channel Added"); 462 | refreshChannels(); 463 | } 464 | 465 | @Override 466 | public void onChannelDeleted(final Channel channel) { 467 | Log.d(TwilioChatApplication.TAG,"Channel Deleted"); 468 | Channel currentChannel = chatFragment.getCurrentChannel(); 469 | if (channel.getSid().contentEquals(currentChannel.getSid())) { 470 | chatFragment.setCurrentChannel(null, null); 471 | setChannel(0); 472 | } 473 | refreshChannels(); 474 | } 475 | 476 | @Override 477 | public void onChannelInvited(Channel channel) { 478 | 479 | } 480 | 481 | @Override 482 | public void onChannelSynchronizationChange(Channel channel) { 483 | 484 | } 485 | 486 | @Override 487 | public void onError(ErrorInfo errorInfo) { 488 | 489 | } 490 | 491 | @Override 492 | public void onClientSynchronization(ChatClient.SynchronizationStatus synchronizationStatus) { 493 | 494 | } 495 | 496 | @Override 497 | public void onConnectionStateChange(ChatClient.ConnectionState connectionState) { 498 | 499 | } 500 | 501 | @Override 502 | public void onTokenExpired() { 503 | 504 | } 505 | 506 | @Override 507 | public void onTokenAboutToExpire() { 508 | 509 | } 510 | 511 | @Override 512 | public void onChannelJoined(Channel channel) { 513 | 514 | } 515 | 516 | @Override 517 | public void onChannelUpdated(Channel channel, Channel.UpdateReason updateReason) { 518 | 519 | } 520 | 521 | @Override 522 | public void onUserUpdated(User user, User.UpdateReason updateReason) { 523 | 524 | } 525 | 526 | @Override 527 | public void onUserSubscribed(User user) { 528 | 529 | } 530 | 531 | @Override 532 | public void onUserUnsubscribed(User user) { 533 | 534 | } 535 | 536 | @Override 537 | public void onNewMessageNotification(String s, String s1, long l) { 538 | 539 | } 540 | 541 | @Override 542 | public void onAddedToChannelNotification(String s) { 543 | 544 | } 545 | 546 | @Override 547 | public void onInvitedToChannelNotification(String s) { 548 | 549 | } 550 | 551 | @Override 552 | public void onRemovedFromChannelNotification(String s) { 553 | 554 | } 555 | 556 | @Override 557 | public void onNotificationSubscribed() { 558 | 559 | } 560 | 561 | @Override 562 | public void onNotificationFailed(ErrorInfo errorInfo) { 563 | 564 | } 565 | } 566 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/MainChatFragment.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import androidx.fragment.app.Fragment; 7 | import android.view.KeyEvent; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.Button; 12 | import android.widget.EditText; 13 | import android.widget.ListView; 14 | import android.widget.TextView; 15 | 16 | import com.twilio.chat.CallbackListener; 17 | import com.twilio.chat.Channel; 18 | import com.twilio.chat.ChannelListener; 19 | import com.twilio.chat.ErrorInfo; 20 | import com.twilio.chat.Member; 21 | import com.twilio.chat.Message; 22 | import com.twilio.chat.Messages; 23 | import com.twilio.chat.StatusListener; 24 | import com.twilio.twiliochat.R; 25 | import com.twilio.twiliochat.chat.messages.JoinedStatusMessage; 26 | import com.twilio.twiliochat.chat.messages.LeftStatusMessage; 27 | import com.twilio.twiliochat.chat.messages.MessageAdapter; 28 | import com.twilio.twiliochat.chat.messages.StatusMessage; 29 | 30 | import java.util.List; 31 | 32 | public class MainChatFragment extends Fragment implements ChannelListener { 33 | Context context; 34 | Activity mainActivity; 35 | Button sendButton; 36 | ListView messagesListView; 37 | EditText messageTextEdit; 38 | 39 | MessageAdapter messageAdapter; 40 | Channel currentChannel; 41 | Messages messagesObject; 42 | 43 | public MainChatFragment() { 44 | } 45 | 46 | public static MainChatFragment newInstance() { 47 | MainChatFragment fragment = new MainChatFragment(); 48 | return fragment; 49 | } 50 | 51 | @Override 52 | public void onCreate(Bundle savedInstanceState) { 53 | super.onCreate(savedInstanceState); 54 | context = this.getActivity(); 55 | mainActivity = this.getActivity(); 56 | } 57 | 58 | @Override 59 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 60 | Bundle savedInstanceState) { 61 | View view = inflater.inflate(R.layout.fragment_main_chat, container, false); 62 | sendButton = (Button) view.findViewById(R.id.buttonSend); 63 | messagesListView = (ListView) view.findViewById(R.id.listViewMessages); 64 | messageTextEdit = (EditText) view.findViewById(R.id.editTextMessage); 65 | 66 | messageAdapter = new MessageAdapter(mainActivity); 67 | messagesListView.setAdapter(messageAdapter); 68 | setUpListeners(); 69 | setMessageInputEnabled(false); 70 | 71 | return view; 72 | } 73 | 74 | @Override 75 | public void onAttach(Context context) { 76 | super.onAttach(context); 77 | } 78 | 79 | @Override 80 | public void onDetach() { 81 | super.onDetach(); 82 | } 83 | 84 | public Channel getCurrentChannel() { 85 | return currentChannel; 86 | } 87 | 88 | public void setCurrentChannel(Channel currentChannel, final StatusListener handler) { 89 | if (currentChannel == null) { 90 | this.currentChannel = null; 91 | return; 92 | } 93 | if (!currentChannel.equals(this.currentChannel)) { 94 | setMessageInputEnabled(false); 95 | this.currentChannel = currentChannel; 96 | this.currentChannel.addListener(this); 97 | if (this.currentChannel.getStatus() == Channel.ChannelStatus.JOINED) { 98 | loadMessages(handler); 99 | } else { 100 | this.currentChannel.join(new StatusListener() { 101 | @Override 102 | public void onSuccess() { 103 | loadMessages(handler); 104 | } 105 | 106 | @Override 107 | public void onError(ErrorInfo errorInfo) { 108 | } 109 | }); 110 | } 111 | } 112 | } 113 | 114 | private void loadMessages(final StatusListener handler) { 115 | this.messagesObject = this.currentChannel.getMessages(); 116 | 117 | if (messagesObject != null) { 118 | messagesObject.getLastMessages(100, new CallbackListener>() { 119 | @Override 120 | public void onSuccess(List messageList) { 121 | messageAdapter.setMessages(messageList); 122 | setMessageInputEnabled(true); 123 | messageTextEdit.requestFocus(); 124 | handler.onSuccess(); 125 | } 126 | }); 127 | } 128 | } 129 | 130 | private void setUpListeners() { 131 | sendButton.setOnClickListener(new View.OnClickListener() { 132 | @Override 133 | public void onClick(View v) { 134 | sendMessage(); 135 | } 136 | }); 137 | messageTextEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() { 138 | @Override 139 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 140 | return false; 141 | } 142 | }); 143 | } 144 | 145 | private void sendMessage() { 146 | String messageText = getTextInput(); 147 | if (messageText.length() == 0) { 148 | return; 149 | } 150 | Message.Options messageOptions = Message.options().withBody(messageText); 151 | this.messagesObject.sendMessage(messageOptions, null); 152 | clearTextInput(); 153 | } 154 | 155 | private void setMessageInputEnabled(final boolean enabled) { 156 | mainActivity.runOnUiThread(new Runnable() { 157 | @Override 158 | public void run() { 159 | MainChatFragment.this.sendButton.setEnabled(enabled); 160 | MainChatFragment.this.messageTextEdit.setEnabled(enabled); 161 | } 162 | }); 163 | } 164 | 165 | private String getTextInput() { 166 | return messageTextEdit.getText().toString(); 167 | } 168 | 169 | private void clearTextInput() { 170 | messageTextEdit.setText(""); 171 | } 172 | 173 | @Override 174 | public void onMessageAdded(Message message) { 175 | messageAdapter.addMessage(message); 176 | } 177 | 178 | @Override 179 | public void onMemberAdded(Member member) { 180 | StatusMessage statusMessage = new JoinedStatusMessage(member.getIdentity()); 181 | this.messageAdapter.addStatusMessage(statusMessage); 182 | } 183 | 184 | @Override 185 | public void onMemberDeleted(Member member) { 186 | StatusMessage statusMessage = new LeftStatusMessage(member.getIdentity()); 187 | this.messageAdapter.addStatusMessage(statusMessage); 188 | } 189 | 190 | @Override 191 | public void onMessageUpdated(Message message, Message.UpdateReason updateReason) { 192 | } 193 | 194 | @Override 195 | public void onMessageDeleted(Message message) { 196 | } 197 | 198 | @Override 199 | public void onMemberUpdated(Member member, Member.UpdateReason updateReason) { 200 | } 201 | 202 | @Override 203 | public void onTypingStarted(Channel channel, Member member) { 204 | } 205 | 206 | @Override 207 | public void onTypingEnded(Channel channel, Member member) { 208 | } 209 | 210 | @Override 211 | public void onSynchronizationChanged(Channel channel) { 212 | } 213 | 214 | 215 | } 216 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/accesstoken/AccessTokenFetcher.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.accesstoken; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.util.Log; 6 | 7 | import com.android.volley.Request; 8 | import com.android.volley.Response; 9 | import com.android.volley.VolleyError; 10 | import com.android.volley.toolbox.JsonObjectRequest; 11 | import com.twilio.twiliochat.R; 12 | import com.twilio.twiliochat.application.SessionManager; 13 | import com.twilio.twiliochat.application.TwilioChatApplication; 14 | import com.twilio.twiliochat.chat.listeners.TaskCompletionListener; 15 | 16 | import org.json.JSONException; 17 | import org.json.JSONObject; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | public class AccessTokenFetcher { 23 | 24 | private Context context; 25 | 26 | public AccessTokenFetcher(Context context) { 27 | this.context = context; 28 | } 29 | 30 | public void fetch(final TaskCompletionListener listener) { 31 | JSONObject obj = new JSONObject(getTokenRequestParams(context)); 32 | String identity = SessionManager.getInstance().getUsername(); 33 | String requestUrl = getStringResource(R.string.token_url) + "?identity=" + identity; 34 | Log.d(TwilioChatApplication.TAG, "Requesting access token from: " + requestUrl); 35 | 36 | JsonObjectRequest jsonObjReq = 37 | new JsonObjectRequest(Request.Method.GET, requestUrl, obj, new Response.Listener() { 38 | 39 | @Override 40 | public void onResponse(JSONObject response) { 41 | String token = null; 42 | try { 43 | token = response.getString("token"); 44 | } catch (JSONException e) { 45 | Log.e(TwilioChatApplication.TAG, e.getLocalizedMessage(), e); 46 | listener.onError("Failed to parse token JSON response"); 47 | } 48 | listener.onSuccess(token); 49 | } 50 | }, new Response.ErrorListener() { 51 | 52 | @Override 53 | public void onErrorResponse(VolleyError error) { 54 | Log.e(TwilioChatApplication.TAG, error.getLocalizedMessage(), error); 55 | listener.onError("Failed to fetch token"); 56 | } 57 | }); 58 | jsonObjReq.setShouldCache(false); 59 | TokenRequest.getInstance().addToRequestQueue(jsonObjReq); 60 | } 61 | 62 | private Map getTokenRequestParams(Context context) { 63 | Map params = new HashMap<>(); 64 | params.put("identity", SessionManager.getInstance().getUsername()); 65 | return params; 66 | } 67 | 68 | private String getStringResource(int id) { 69 | Resources resources = TwilioChatApplication.get().getResources(); 70 | return resources.getString(id); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/accesstoken/TokenRequest.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.accesstoken; 2 | 3 | import com.android.volley.Request; 4 | import com.android.volley.RequestQueue; 5 | import com.android.volley.toolbox.Volley; 6 | import com.twilio.twiliochat.application.TwilioChatApplication; 7 | 8 | public class TokenRequest { 9 | private static TokenRequest mInstance; 10 | private RequestQueue mRequestQueue; 11 | 12 | private TokenRequest() { 13 | mRequestQueue = Volley.newRequestQueue(TwilioChatApplication.get().getApplicationContext()); 14 | } 15 | 16 | public static synchronized TokenRequest getInstance() { 17 | if (mInstance == null) { 18 | mInstance = new TokenRequest(); 19 | } 20 | return mInstance; 21 | } 22 | 23 | public void addToRequestQueue(Request req) { 24 | mRequestQueue.add(req); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/channels/ChannelAdapter.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.channels; 2 | 3 | import android.app.Activity; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.BaseAdapter; 8 | import android.widget.TextView; 9 | 10 | import com.twilio.chat.Channel; 11 | import com.twilio.twiliochat.R; 12 | 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | public class ChannelAdapter extends BaseAdapter { 17 | private LayoutInflater layoutInflater; 18 | private List channels; 19 | 20 | public ChannelAdapter(Activity activity, List channels) { 21 | this.layoutInflater = activity.getLayoutInflater(); 22 | this.channels = channels; 23 | } 24 | 25 | public void setChannels(List channels) { 26 | this.channels = channels; 27 | Collections.sort(this.channels, new CustomChannelComparator()); 28 | notifyDataSetChanged(); 29 | } 30 | 31 | public void addChannel(Channel channel) { 32 | this.channels.add(channel); 33 | Collections.sort(this.channels, new CustomChannelComparator()); 34 | notifyDataSetChanged(); 35 | } 36 | 37 | public void deleteChannel(Channel channel) { 38 | this.channels.remove(channel); 39 | notifyDataSetChanged(); 40 | } 41 | 42 | @Override 43 | public int getCount() { 44 | return channels.size(); 45 | } 46 | 47 | @Override 48 | public Object getItem(int position) { 49 | return channels.get(position); 50 | } 51 | 52 | @Override 53 | public long getItemId(int position) { 54 | return position; 55 | } 56 | 57 | @Override 58 | public View getView(int i, View convertView, ViewGroup viewGroup) { 59 | if (convertView == null) { 60 | int res = R.layout.channel; 61 | convertView = layoutInflater.inflate(res, viewGroup, false); 62 | } 63 | Channel channel = channels.get(i); 64 | TextView channelTextView = (TextView) convertView.findViewById(R.id.textViewChannel); 65 | channelTextView.setText(channel.getFriendlyName()); 66 | return convertView; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/channels/ChannelExtractor.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.channels; 2 | 3 | import com.twilio.chat.CallbackListener; 4 | import com.twilio.chat.Channel; 5 | import com.twilio.chat.ChannelDescriptor; 6 | import com.twilio.chat.ErrorInfo; 7 | import com.twilio.chat.Paginator; 8 | import com.twilio.twiliochat.chat.listeners.TaskCompletionListener; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | 15 | public class ChannelExtractor { 16 | 17 | public void extractAndSortFromChannelDescriptor(Paginator paginator, 18 | final TaskCompletionListener, String> listener) { 19 | 20 | extractFromChannelDescriptor(paginator, new TaskCompletionListener, String>() { 21 | @Override 22 | public void onSuccess(List channels) { 23 | Collections.sort(channels, new CustomChannelComparator()); 24 | listener.onSuccess(channels); 25 | } 26 | 27 | @Override 28 | public void onError(String s) { 29 | listener.onError(s); 30 | } 31 | }); 32 | } 33 | 34 | private void extractFromChannelDescriptor(Paginator paginator, 35 | final TaskCompletionListener, String> listener) { 36 | 37 | final List channels = new ArrayList<>(); 38 | final AtomicInteger channelDescriptorCount = new AtomicInteger(paginator.getItems().size()); 39 | for (ChannelDescriptor channelDescriptor : paginator.getItems()) { 40 | channelDescriptor.getChannel(new CallbackListener() { 41 | @Override 42 | public void onSuccess(Channel channel) { 43 | channels.add(channel); 44 | int channelDescriptorsLeft = channelDescriptorCount.decrementAndGet(); 45 | if(channelDescriptorsLeft == 0) { 46 | listener.onSuccess(channels); 47 | } 48 | } 49 | 50 | @Override 51 | public void onError(ErrorInfo errorInfo) { 52 | listener.onError(errorInfo.getMessage()); 53 | } 54 | }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/channels/ChannelManager.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.channels; 2 | 3 | import android.content.res.Resources; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.util.Log; 7 | 8 | import com.twilio.chat.CallbackListener; 9 | import com.twilio.chat.Channel; 10 | import com.twilio.chat.Channel.ChannelType; 11 | import com.twilio.chat.ChannelDescriptor; 12 | import com.twilio.chat.Channels; 13 | import com.twilio.chat.ChatClient; 14 | import com.twilio.chat.ChatClientListener; 15 | import com.twilio.chat.ErrorInfo; 16 | import com.twilio.chat.Paginator; 17 | import com.twilio.chat.StatusListener; 18 | import com.twilio.chat.User; 19 | import com.twilio.twiliochat.R; 20 | import com.twilio.twiliochat.application.TwilioChatApplication; 21 | import com.twilio.twiliochat.chat.ChatClientManager; 22 | import com.twilio.twiliochat.chat.accesstoken.AccessTokenFetcher; 23 | import com.twilio.twiliochat.chat.listeners.TaskCompletionListener; 24 | 25 | import java.util.ArrayList; 26 | import java.util.Collections; 27 | import java.util.List; 28 | 29 | public class ChannelManager implements ChatClientListener { 30 | private static ChannelManager sharedManager = new ChannelManager(); 31 | public Channel generalChannel; 32 | private ChatClientManager chatClientManager; 33 | private ChannelExtractor channelExtractor; 34 | private List channels; 35 | private Channels channelsObject; 36 | private ChatClientListener listener; 37 | private String defaultChannelName; 38 | private String defaultChannelUniqueName; 39 | private Handler handler; 40 | private Boolean isRefreshingChannels = false; 41 | 42 | private ChannelManager() { 43 | this.chatClientManager = TwilioChatApplication.get().getChatClientManager(); 44 | this.channelExtractor = new ChannelExtractor(); 45 | this.listener = this; 46 | defaultChannelName = getStringResource(R.string.default_channel_name); 47 | defaultChannelUniqueName = getStringResource(R.string.default_channel_unique_name); 48 | handler = setupListenerHandler(); 49 | } 50 | 51 | public static ChannelManager getInstance() { 52 | return sharedManager; 53 | } 54 | 55 | public List getChannels() { 56 | return channels; 57 | } 58 | 59 | public String getDefaultChannelName() { 60 | return this.defaultChannelName; 61 | } 62 | 63 | public void leaveChannelWithHandler(Channel channel, StatusListener handler) { 64 | channel.leave(handler); 65 | } 66 | 67 | public void deleteChannelWithHandler(Channel channel, StatusListener handler) { 68 | channel.destroy(handler); 69 | } 70 | 71 | public void populateChannels(final LoadChannelListener listener) { 72 | if (this.chatClientManager == null || this.isRefreshingChannels) { 73 | return; 74 | } 75 | this.isRefreshingChannels = true; 76 | 77 | handler.post(new Runnable() { 78 | @Override 79 | public void run() { 80 | channelsObject = chatClientManager.getChatClient().getChannels(); 81 | 82 | channelsObject.getPublicChannelsList(new CallbackListener>() { 83 | @Override 84 | public void onSuccess(Paginator channelDescriptorPaginator) { 85 | extractChannelsFromPaginatorAndPopulate(channelDescriptorPaginator, listener); 86 | } 87 | }); 88 | 89 | } 90 | }); 91 | } 92 | 93 | private void extractChannelsFromPaginatorAndPopulate(final Paginator channelsPaginator, 94 | final LoadChannelListener listener) { 95 | channels = new ArrayList<>(); 96 | ChannelManager.this.channels.clear(); 97 | channelExtractor.extractAndSortFromChannelDescriptor(channelsPaginator, 98 | new TaskCompletionListener, String>() { 99 | @Override 100 | public void onSuccess(List channels) { 101 | ChannelManager.this.channels.addAll(channels); 102 | Collections.sort(ChannelManager.this.channels, new CustomChannelComparator()); 103 | ChannelManager.this.isRefreshingChannels = false; 104 | chatClientManager.addClientListener(ChannelManager.this); 105 | listener.onChannelsFinishedLoading(ChannelManager.this.channels); 106 | } 107 | 108 | @Override 109 | public void onError(String errorText) { 110 | Log.e(TwilioChatApplication.TAG,"Error populating channels: " + errorText); 111 | } 112 | }); 113 | } 114 | 115 | public void createChannelWithName(String name, final StatusListener handler) { 116 | this.channelsObject 117 | .channelBuilder() 118 | .withFriendlyName(name) 119 | .withType(ChannelType.PUBLIC) 120 | .build(new CallbackListener() { 121 | @Override 122 | public void onSuccess(final Channel newChannel) { 123 | handler.onSuccess(); 124 | } 125 | 126 | @Override 127 | public void onError(ErrorInfo errorInfo) { 128 | handler.onError(errorInfo); 129 | } 130 | }); 131 | } 132 | 133 | public void joinOrCreateGeneralChannelWithCompletion(final StatusListener listener) { 134 | channelsObject.getChannel(defaultChannelUniqueName, new CallbackListener() { 135 | @Override 136 | public void onSuccess(Channel channel) { 137 | ChannelManager.this.generalChannel = channel; 138 | if (channel != null) { 139 | joinGeneralChannelWithCompletion(listener); 140 | } else { 141 | createGeneralChannelWithCompletion(listener); 142 | } 143 | } 144 | }); 145 | } 146 | 147 | private void joinGeneralChannelWithCompletion(final StatusListener listener) { 148 | if (generalChannel.getStatus() == Channel.ChannelStatus.JOINED) { 149 | listener.onSuccess(); 150 | return; 151 | } 152 | this.generalChannel.join(new StatusListener() { 153 | @Override 154 | public void onSuccess() { 155 | listener.onSuccess(); 156 | } 157 | 158 | @Override 159 | public void onError(ErrorInfo errorInfo) { 160 | listener.onError(errorInfo); 161 | } 162 | }); 163 | } 164 | 165 | private void createGeneralChannelWithCompletion(final StatusListener listener) { 166 | this.channelsObject 167 | .channelBuilder() 168 | .withFriendlyName(defaultChannelName) 169 | .withUniqueName(defaultChannelUniqueName) 170 | .withType(ChannelType.PUBLIC) 171 | .build(new CallbackListener() { 172 | @Override 173 | public void onSuccess(final Channel channel) { 174 | ChannelManager.this.generalChannel = channel; 175 | ChannelManager.this.channels.add(channel); 176 | joinGeneralChannelWithCompletion(listener); 177 | } 178 | 179 | @Override 180 | public void onError(ErrorInfo errorInfo) { 181 | listener.onError(errorInfo); 182 | } 183 | }); 184 | } 185 | 186 | public void setChannelListener(ChatClientListener listener) { 187 | this.listener = listener; 188 | } 189 | 190 | private String getStringResource(int id) { 191 | Resources resources = TwilioChatApplication.get().getResources(); 192 | return resources.getString(id); 193 | } 194 | 195 | @Override 196 | public void onChannelAdded(Channel channel) { 197 | if (listener != null) { 198 | listener.onChannelAdded(channel); 199 | } 200 | } 201 | 202 | @Override 203 | public void onChannelUpdated(Channel channel, Channel.UpdateReason updateReason) { 204 | if (listener != null) { 205 | listener.onChannelUpdated(channel, updateReason); 206 | } 207 | } 208 | 209 | @Override 210 | public void onChannelDeleted(Channel channel) { 211 | if (listener != null) { 212 | listener.onChannelDeleted(channel); 213 | } 214 | } 215 | 216 | @Override 217 | public void onChannelSynchronizationChange(Channel channel) { 218 | if (listener != null) { 219 | listener.onChannelSynchronizationChange(channel); 220 | } 221 | } 222 | 223 | @Override 224 | public void onError(ErrorInfo errorInfo) { 225 | if (listener != null) { 226 | listener.onError(errorInfo); 227 | } 228 | } 229 | 230 | @Override 231 | public void onClientSynchronization(ChatClient.SynchronizationStatus synchronizationStatus) { 232 | 233 | } 234 | 235 | @Override 236 | public void onChannelJoined(Channel channel) { 237 | 238 | } 239 | 240 | @Override 241 | public void onChannelInvited(Channel channel) { 242 | 243 | } 244 | 245 | @Override 246 | public void onUserUpdated(User user, User.UpdateReason updateReason) { 247 | if (listener != null) { 248 | listener.onUserUpdated(user, updateReason); 249 | } 250 | } 251 | 252 | @Override 253 | public void onUserSubscribed(User user) { 254 | 255 | } 256 | 257 | @Override 258 | public void onUserUnsubscribed(User user) { 259 | 260 | } 261 | 262 | @Override 263 | public void onNewMessageNotification(String s, String s1, long l) { 264 | 265 | } 266 | 267 | @Override 268 | public void onAddedToChannelNotification(String s) { 269 | 270 | } 271 | 272 | @Override 273 | public void onInvitedToChannelNotification(String s) { 274 | 275 | } 276 | 277 | @Override 278 | public void onRemovedFromChannelNotification(String s) { 279 | 280 | } 281 | 282 | @Override 283 | public void onNotificationSubscribed() { 284 | 285 | } 286 | 287 | @Override 288 | public void onNotificationFailed(ErrorInfo errorInfo) { 289 | 290 | } 291 | 292 | @Override 293 | public void onConnectionStateChange(ChatClient.ConnectionState connectionState) { 294 | 295 | } 296 | 297 | @Override 298 | public void onTokenExpired() { 299 | refreshAccessToken(); 300 | } 301 | 302 | @Override 303 | public void onTokenAboutToExpire() { 304 | refreshAccessToken(); 305 | } 306 | 307 | private void refreshAccessToken() { 308 | AccessTokenFetcher accessTokenFetcher = chatClientManager.getAccessTokenFetcher(); 309 | accessTokenFetcher.fetch(new TaskCompletionListener() { 310 | @Override 311 | public void onSuccess(String token) { 312 | ChannelManager.this.chatClientManager.getChatClient().updateToken(token, new StatusListener() { 313 | @Override 314 | public void onSuccess() { 315 | Log.d(TwilioChatApplication.TAG, "Successfully updated access token."); 316 | } 317 | }); 318 | } 319 | 320 | @Override 321 | public void onError(String message) { 322 | Log.e(TwilioChatApplication.TAG,"Error trying to fetch token: " + message); 323 | } 324 | }); 325 | } 326 | 327 | private Handler setupListenerHandler() { 328 | Looper looper; 329 | Handler handler; 330 | if ((looper = Looper.myLooper()) != null) { 331 | handler = new Handler(looper); 332 | } else if ((looper = Looper.getMainLooper()) != null) { 333 | handler = new Handler(looper); 334 | } else { 335 | throw new IllegalArgumentException("Channel Listener must have a Looper."); 336 | } 337 | return handler; 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/channels/CustomChannelComparator.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.channels; 2 | 3 | import com.twilio.chat.Channel; 4 | import com.twilio.twiliochat.R; 5 | import com.twilio.twiliochat.application.TwilioChatApplication; 6 | 7 | import java.util.Comparator; 8 | 9 | public class CustomChannelComparator implements Comparator { 10 | private String defaultChannelName; 11 | 12 | CustomChannelComparator() { 13 | defaultChannelName = 14 | TwilioChatApplication.get().getResources().getString(R.string.default_channel_name); 15 | } 16 | 17 | @Override 18 | public int compare(Channel lhs, Channel rhs) { 19 | if (lhs.getFriendlyName().contentEquals(defaultChannelName)) { 20 | return -100; 21 | } else if (rhs.getFriendlyName().contentEquals(defaultChannelName)) { 22 | return 100; 23 | } 24 | return lhs.getFriendlyName().toLowerCase().compareTo(rhs.getFriendlyName().toLowerCase()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/channels/LoadChannelListener.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.channels; 2 | 3 | import com.twilio.chat.Channel; 4 | 5 | import java.util.List; 6 | 7 | public interface LoadChannelListener { 8 | 9 | void onChannelsFinishedLoading(List channels); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/listeners/InputOnClickListener.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.listeners; 2 | 3 | public interface InputOnClickListener { 4 | 5 | void onClick(String input); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/listeners/TaskCompletionListener.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.listeners; 2 | 3 | public interface TaskCompletionListener { 4 | 5 | void onSuccess(T t); 6 | 7 | void onError(U u); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/messages/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.messages; 2 | 3 | public interface ChatMessage { 4 | 5 | String getMessageBody(); 6 | 7 | String getAuthor(); 8 | 9 | String getDateCreated(); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/messages/DateFormatter.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.messages; 2 | 3 | import org.joda.time.DateTime; 4 | import org.joda.time.format.DateTimeFormatter; 5 | import org.joda.time.format.ISODateTimeFormat; 6 | 7 | public class DateFormatter { 8 | private DateFormatter() { 9 | } 10 | 11 | public static DateTime getDateFromISOString(String inputDate) { 12 | DateTimeFormatter isoFormatter = ISODateTimeFormat.dateTime(); 13 | DateTime date = null; 14 | 15 | try { 16 | date = isoFormatter.parseDateTime(inputDate); 17 | } catch (IllegalArgumentException e) { 18 | e.printStackTrace(); 19 | } 20 | return date; 21 | } 22 | 23 | public static String getDateTodayString(DateTime today) { 24 | DateTime todayMidnight = new DateTime().withTimeAtStartOfDay(); 25 | String stringDate; 26 | if (todayMidnight.isEqual(today.withTimeAtStartOfDay())) { 27 | stringDate = "Today - "; 28 | } else { 29 | stringDate = today.toString("MMM. dd - "); 30 | } 31 | 32 | stringDate = stringDate.concat(today.toString("hh:mma")); 33 | 34 | return stringDate; 35 | } 36 | 37 | public static String getFormattedDateFromISOString(String inputDate) { 38 | return getDateTodayString(getDateFromISOString(inputDate)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/messages/JoinedStatusMessage.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.messages; 2 | 3 | public class JoinedStatusMessage extends StatusMessage { 4 | 5 | 6 | public JoinedStatusMessage(String author) { 7 | super(author); 8 | } 9 | 10 | @Override 11 | public String getMessageBody() { 12 | return this.getAuthor() + " joined the channel"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/messages/LeftStatusMessage.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.messages; 2 | 3 | public class LeftStatusMessage extends StatusMessage { 4 | 5 | 6 | public LeftStatusMessage(String author) { 7 | super(author); 8 | } 9 | 10 | 11 | @Override 12 | public String getMessageBody() { 13 | return this.getAuthor() + " left the channel"; 14 | } 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/messages/MessageAdapter.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.messages; 2 | 3 | import android.app.Activity; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.BaseAdapter; 8 | import android.widget.TextView; 9 | 10 | import com.twilio.chat.Message; 11 | import com.twilio.twiliochat.R; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.TreeSet; 16 | 17 | public class MessageAdapter extends BaseAdapter { 18 | private final int TYPE_MESSAGE = 0; 19 | private final int TYPE_STATUS = 1; 20 | 21 | private List messages; 22 | private LayoutInflater layoutInflater; 23 | private TreeSet statusMessageSet = new TreeSet<>(); 24 | 25 | public MessageAdapter(Activity activity) { 26 | layoutInflater = activity.getLayoutInflater(); 27 | messages = new ArrayList<>(); 28 | } 29 | 30 | public void setMessages(List messages) { 31 | this.messages = convertTwilioMessages(messages); 32 | this.statusMessageSet.clear(); 33 | notifyDataSetChanged(); 34 | } 35 | 36 | public void addMessage(Message message) { 37 | messages.add(new UserMessage(message)); 38 | notifyDataSetChanged(); 39 | } 40 | 41 | public void addStatusMessage(StatusMessage message) { 42 | messages.add(message); 43 | statusMessageSet.add(messages.size() - 1); 44 | notifyDataSetChanged(); 45 | } 46 | 47 | public void removeMessage(Message message) { 48 | messages.remove(messages.indexOf(message)); 49 | notifyDataSetChanged(); 50 | } 51 | 52 | private List convertTwilioMessages(List messages) { 53 | List chatMessages = new ArrayList<>(); 54 | for (Message message : messages) { 55 | chatMessages.add(new UserMessage(message)); 56 | } 57 | return chatMessages; 58 | } 59 | 60 | @Override 61 | public int getViewTypeCount() { 62 | return 2; 63 | } 64 | 65 | @Override 66 | public int getItemViewType(int position) { 67 | return statusMessageSet.contains(position) ? TYPE_STATUS : TYPE_MESSAGE; 68 | } 69 | 70 | @Override 71 | public int getCount() { 72 | return messages.size(); 73 | } 74 | 75 | @Override 76 | public Object getItem(int i) { 77 | return messages.get(i); 78 | } 79 | 80 | @Override 81 | public long getItemId(int i) { 82 | return i; 83 | } 84 | 85 | @Override 86 | public View getView(int position, View convertView, ViewGroup viewGroup) { 87 | int type = getItemViewType(position); 88 | int res; 89 | switch (type) { 90 | case TYPE_MESSAGE: 91 | res = R.layout.message; 92 | convertView = layoutInflater.inflate(res, viewGroup, false); 93 | ChatMessage message = messages.get(position); 94 | TextView textViewMessage = (TextView) convertView.findViewById(R.id.textViewMessage); 95 | TextView textViewAuthor = (TextView) convertView.findViewById(R.id.textViewAuthor); 96 | TextView textViewDate = (TextView) convertView.findViewById(R.id.textViewDate); 97 | textViewMessage.setText(message.getMessageBody()); 98 | textViewAuthor.setText(message.getAuthor()); 99 | textViewDate.setText(DateFormatter.getFormattedDateFromISOString(message.getDateCreated())); 100 | break; 101 | case TYPE_STATUS: 102 | res = R.layout.status_message; 103 | convertView = layoutInflater.inflate(res, viewGroup, false); 104 | ChatMessage status = messages.get(position); 105 | TextView textViewStatus = (TextView) convertView.findViewById(R.id.textViewStatusMessage); 106 | String statusMessage = status.getMessageBody(); 107 | textViewStatus.setText(statusMessage); 108 | break; 109 | } 110 | return convertView; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/messages/StatusMessage.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.messages; 2 | 3 | 4 | public class StatusMessage implements ChatMessage { 5 | private String author = ""; 6 | 7 | public StatusMessage(String author) { 8 | this.author = author; 9 | } 10 | 11 | @Override 12 | public String getAuthor() { 13 | return author; 14 | } 15 | 16 | @Override 17 | public String getDateCreated() { 18 | throw new UnsupportedOperationException(); 19 | } 20 | 21 | @Override 22 | public String getMessageBody() { 23 | throw new UnsupportedOperationException(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/chat/messages/UserMessage.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.chat.messages; 2 | 3 | import com.twilio.chat.Message; 4 | 5 | public class UserMessage implements ChatMessage { 6 | 7 | private String author = ""; 8 | private String dateCreated = ""; 9 | private String messageBody = ""; 10 | 11 | public UserMessage(Message message) { 12 | this.author = message.getAuthor(); 13 | this.dateCreated = message.getDateCreated(); 14 | this.messageBody = message.getMessageBody(); 15 | } 16 | 17 | @Override 18 | public String getMessageBody() { 19 | return messageBody; 20 | } 21 | 22 | @Override 23 | public String getAuthor() { 24 | return author; 25 | } 26 | 27 | @Override 28 | public String getDateCreated() { 29 | return dateCreated; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/landing/LaunchActivity.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.landing; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | 7 | import com.twilio.twiliochat.application.SessionManager; 8 | import com.twilio.twiliochat.chat.MainChatActivity; 9 | 10 | public class LaunchActivity extends Activity { 11 | 12 | @Override 13 | public void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | 16 | Intent launchIntent = new Intent(); 17 | Class launchActivity; 18 | 19 | launchActivity = getLaunchClass(); 20 | launchIntent.setClass(getApplicationContext(), launchActivity); 21 | startActivity(launchIntent); 22 | 23 | finish(); 24 | } 25 | 26 | private Class getLaunchClass() { 27 | if (SessionManager.getInstance().isLoggedIn()) { 28 | return MainChatActivity.class; 29 | } else { 30 | return LoginActivity.class; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/twiliochat/landing/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package com.twilio.twiliochat.landing; 2 | 3 | import android.app.ProgressDialog; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.res.Resources; 7 | import android.os.Bundle; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import android.view.KeyEvent; 10 | import android.view.View; 11 | import android.widget.Button; 12 | import android.widget.EditText; 13 | import android.widget.TextView; 14 | 15 | import com.twilio.twiliochat.R; 16 | import com.twilio.twiliochat.application.AlertDialogHandler; 17 | import com.twilio.twiliochat.application.SessionManager; 18 | import com.twilio.twiliochat.application.TwilioChatApplication; 19 | import com.twilio.twiliochat.chat.ChatClientManager; 20 | import com.twilio.twiliochat.chat.MainChatActivity; 21 | import com.twilio.twiliochat.chat.listeners.TaskCompletionListener; 22 | 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | 26 | public class LoginActivity extends AppCompatActivity { 27 | final Context context = this; 28 | private final String USERNAME_FORM_FIELD = "username"; 29 | private ProgressDialog progressDialog; 30 | private Button loginButton; 31 | private EditText usernameEditText; 32 | 33 | private ChatClientManager clientManager; 34 | 35 | @Override 36 | protected void onCreate(Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | setContentView(R.layout.activity_login); 39 | setUIComponents(); 40 | 41 | clientManager = TwilioChatApplication.get().getChatClientManager(); 42 | } 43 | 44 | private void setUIComponents() { 45 | loginButton = (Button) findViewById(R.id.buttonLogin); 46 | usernameEditText = (EditText) findViewById(R.id.editTextUsername); 47 | 48 | setUpListeners(); 49 | } 50 | 51 | private void setUpListeners() { 52 | loginButton.setOnClickListener(new View.OnClickListener() { 53 | @Override 54 | public void onClick(View v) { 55 | login(); 56 | } 57 | }); 58 | TextView.OnEditorActionListener actionListener = new TextView.OnEditorActionListener() { 59 | @Override 60 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 61 | int viewId = v.getImeActionId(); 62 | if (viewId == 100) { 63 | login(); 64 | return true; 65 | } 66 | return false; 67 | } 68 | }; 69 | usernameEditText.setOnEditorActionListener(actionListener); 70 | } 71 | 72 | private void login() { 73 | Map formInput = getFormInput(); 74 | if (formInput.size() < 1) { 75 | displayAllFieldsRequiredMessage(); 76 | return; 77 | } 78 | 79 | startStatusDialogWithMessage(getStringResource(R.string.login_user_progress_message)); 80 | SessionManager.getInstance().createLoginSession(formInput.get(USERNAME_FORM_FIELD)); 81 | initializeChatClient(); 82 | } 83 | 84 | private Map getFormInput() { 85 | String username = usernameEditText.getText().toString(); 86 | 87 | Map formInput = new HashMap<>(); 88 | 89 | if (username.length() > 0) { 90 | formInput.put(USERNAME_FORM_FIELD, username); 91 | } 92 | return formInput; 93 | } 94 | 95 | private void initializeChatClient() { 96 | clientManager.connectClient(new TaskCompletionListener() { 97 | @Override 98 | public void onSuccess(Void aVoid) { 99 | showMainChatActivity(); 100 | } 101 | 102 | @Override 103 | public void onError(String errorMessage) { 104 | stopStatusDialog(); 105 | showAlertWithMessage(errorMessage); 106 | } 107 | }); 108 | } 109 | 110 | private void displayAllFieldsRequiredMessage() { 111 | String message = getStringResource(R.string.login_all_fields_required); 112 | showAlertWithMessage(message); 113 | } 114 | 115 | private void startStatusDialogWithMessage(String message) { 116 | setFormEnabled(false); 117 | showActivityIndicator(message); 118 | } 119 | 120 | private void stopStatusDialog() { 121 | if (progressDialog.isShowing()) { 122 | progressDialog.dismiss(); 123 | } 124 | setFormEnabled(true); 125 | } 126 | 127 | private void setFormEnabled(Boolean enabled) { 128 | usernameEditText.setEnabled(enabled); 129 | } 130 | 131 | private void showActivityIndicator(String message) { 132 | progressDialog = new ProgressDialog(this); 133 | progressDialog.setMessage(message); 134 | progressDialog.show(); 135 | progressDialog.setCanceledOnTouchOutside(false); 136 | progressDialog.setCancelable(false); 137 | } 138 | 139 | private void showMainChatActivity() { 140 | Intent launchIntent = new Intent(); 141 | launchIntent.setClass(getApplicationContext(), MainChatActivity.class); 142 | startActivity(launchIntent); 143 | 144 | finish(); 145 | } 146 | 147 | private String getStringResource(int id) { 148 | Resources resources = getResources(); 149 | return resources.getString(id); 150 | } 151 | 152 | private void showAlertWithMessage(String message) { 153 | AlertDialogHandler.displayAlertWithMessage(message, context); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/add_channel_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-hdpi/add_channel_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-hdpi/home_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-hdpi/ic_user.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/landing_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-hdpi/landing_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/password_field_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-hdpi/password_field_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/user_panel_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-hdpi/user_panel_line.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/username_field_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-hdpi/username_field_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/add_channel_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-mdpi/add_channel_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-mdpi/home_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-mdpi/ic_user.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/landing_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-mdpi/landing_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/password_field_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-mdpi/password_field_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/user_panel_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-mdpi/user_panel_line.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/username_field_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-mdpi/username_field_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/add_channel_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xhdpi/add_channel_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xhdpi/home_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xhdpi/ic_user.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/landing_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xhdpi/landing_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/password_field_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xhdpi/password_field_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/user_panel_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xhdpi/user_panel_line.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/username_field_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xhdpi/username_field_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/add_channel_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxhdpi/add_channel_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxhdpi/home_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxhdpi/ic_user.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/landing_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxhdpi/landing_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/password_field_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxhdpi/password_field_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/user_panel_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxhdpi/user_panel_line.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/username_field_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxhdpi/username_field_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/add_channel_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxxhdpi/add_channel_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxxhdpi/home_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxxhdpi/ic_user.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/landing_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxxhdpi/landing_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/password_field_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxxhdpi/password_field_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/user_panel_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxxhdpi/user_panel_line.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/username_field_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/twiliochat-android/ef307f88df7bf7a45c7bb74a0aacb0c24b64b8a8/app/src/main/res/drawable-xxxhdpi/username_field_bg.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/message_input_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 23 | 24 | 33 | 34 | 43 | 44 | 51 | 52 | 58 | 59 | 63 | 64 | 71 | 72 | 73 | 74 |