Android Java Chat is an native Android Mobile client for ServiceStack Chat that was originally ported from C# Xamarin Android Chat into Java 8, created using Google's recommended Android Studio Development Environment. In addition to retaining the same functionality as the original C# Xamarin.Android Chat App, it also leverages the native Facebook and Twitter SDK's to enable seamless and persistent authentication via Facebook or Twitter Sign-in's.
The Java Add ServiceStack Reference support and Java Server Events Client are idiomatic ports of their C# equivalent Add ServiceStack Reference and Server Events Client enabling both projects to leverage an end-to-end Typed API that significantly reduces the effort to port from their original C# sources, rendering the porting effort down to a straight-forward 1:1 mapping exercise into Java 8 syntax.
The central hub that powers the Android Chat App is the Server Events Client connection initially declared in the App.java Application so its singleton instance is easily accessible from our entire App:
public App(Context context) {
this.context = context;
this.prefs = context.getSharedPreferences("servicestack.net.androidchat",Context.MODE_PRIVATE);
serverEventsClient = new AndroidServerEventsClient("http://chat.servicestack.net/", "home");
}
The Server Events connection itself is initialized in MainActivity.java when its first launched, after selecting how the User wants to Sign-in from the initial Login screen.
The complete Server Events registration below binds the Chat Server Events to our Application logic, where:
- Upon successful connection:
- Loads the Chat Message History for the channel
- Updates our User's Avatar
- When a new User Joins:
- Updates the
subscriberList
with a list of all Users in the channel - Tell our Message History to re-render because our dataset has changed
- Updates the
- When the Server Events Connection throws an Exception:
- Load an Alert dialog with the Error message
- It uses the Custom
ReceiverResolver
to initialize instances of our Receiver classes - Registers
ChatReceiver
to handle all messages sent withcmd.*
selector - Registers
TvReciever
to handle all messages sent withtv.*
selector - Registers
CssReceiver
to handle all messages sent withcss.*
selector
App.get().getServerEventsClient()
.setOnConnect(connectMsg -> {
Extensions.updateChatHistory(getClient(), cmdReceiver, () -> {
Extensions.updateUserProfile(connectMsg, mainActivity);
});
})
.setOnJoin(msg -> {
getClient().getChannelSubscribersAsync(r -> {
subscriberList = r;
messageHistoryAdapter.notifyDataSetChanged();
});
})
.setOnException(error -> mainActivity.runOnUiThread(() ->
Toast.makeText(this, "Error : " + error.getMessage(), Toast.LENGTH_LONG).show()))
.setResolver(new ReceiverResolver(cmdReceiver))
.registerReceiver(ChatReceiver.class)
.registerNamedReceiver("tv", TvReciever.class)
.registerNamedReceiver("css", CssReceiver.class);
Later in onPostCreate()
the ServerEventsClient starts the connection and begins listening to Server Events:
@Override
public void onPostCreate(Bundle savedInstanceState) {
//...
App.get().getServerEventsClient().start();
}
In order to inject our receivers their required dependencies we utilize a custom ReceiverResolver to take control over how Receiver classes are instantiated:
public class ReceiverResolver implements IResolver {
ChatCommandHandler messageHandler;
public ReceiverResolver(ChatCommandHandler messageHandler) {
this.messageHandler = messageHandler;
}
@Override
public Object TryResolve(Class cls){
if (cls == ChatReceiver.class){
return new ChatReceiver(this.messageHandler);
} else if (cls == TvReciever.class){
return new TvReciever(this.messageHandler);
} else if (cls == CssReceiver.class){
return new CssReceiver(this.messageHandler);
}
try {
return cls.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
The receiver classes themselves act like light-weight proxies which captures each event and forwards them to the ChatCommandHandler to perform the necessary UI updates:
public class ChatReceiver extends ServerEventReceiver {
private ChatCommandHandler chatMessageHandler;
public ChatReceiver(ChatCommandHandler chatMessageHandler) {
this.chatMessageHandler = chatMessageHandler;
}
public void chat(ChatMessage chatMessage){
chatMessageHandler.appendMessage(chatMessage);
}
public void announce(String message){
chatMessageHandler.announce(message);
}
}
public class CssReceiver extends ServerEventReceiver {
private ChatCommandHandler chatMessageHandler;
public CssReceiver(ChatCommandHandler chatMessageHandler){
this.chatMessageHandler = chatMessageHandler;
}
public void backgroundImage(String message){
chatMessageHandler.changeBackground(message);
}
public void background(String message){
chatMessageHandler.changeBackgroundColor(message, super.getRequest().getCssSelector());
}
}
public class TvReciever extends ServerEventReceiver {
private ChatCommandHandler chatMessageHandler;
public TvReciever(ChatCommandHandler chatMessageHandler) {
this.chatMessageHandler = chatMessageHandler;
}
public void watch(String videoUrl) {
chatMessageHandler.showVideo(videoUrl);
}
}
As we're now using Java we get direct access to the latest 3rd Party Android components which we've taken advantage of to leverage Facebook's and Twitter's SDK's to handle the OAuth flow allowing Users to Sign-in with their Facebook or Twitter account.
Before we can make use of their SDK's we need to configure them with our project by following their respective installation guides:
As they offer different level of customizations we've implemented 2 Login Activities, our first Activity shows how to integrate using Facebook's and Twitter's SDK Login buttons whilst the 2nd Login Activity shows how to use the SDK classes directly letting us use custom images for login buttons.
The UI and implementation for both Login Activities are below:
With each Activity declared in AndroidManifest.xml:
<activity android:name=".LoginActivity">
</activity>
<activity android:name=".LoginButtonsActivity">
<!-- Move to .LoginActivity if you prefer that login page instead -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
The <intent-filter>
is used to control which Activity our App loads when launched, in this case
it will load the LoginButtonsActivity
:
Where we just use Twitter's, Facebook's and Google's Login Button widgets to render the UI in login_buttons.xml:
<com.twitter.sdk.android.core.identity.TwitterLoginButton
android:id="@+id/btnTwitterLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.facebook.login.widget.LoginButton
android:id="@+id/btnFacebookLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp"
android:layout_marginBottom="30dp" />
<com.google.android.gms.common.SignInButton
android:id="@+id/sign_in_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:text="Guest Login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/btnGuestLogin" />
To use the Twitter SDK we need to first configure it with our Twitter App's ConsumerKey
and ConsumerSecret
:
Fabric.with(this, new Twitter(new TwitterAuthConfig(
getString(R.string.twitter_key),
getString(R.string.twitter_secret))));
If you don't have a Twitter App, one can be created at dev.twitter.com/apps
After that it's simply a matter of handling the Twitter success()
and failure()
callbacks. When the
success()
callback is fired it means the User has successfully Signed into our Android App, we then
need to Authenticate with our ServiceStack Chat Server by making an Authenticated request using the
User's Twitter AccessToken and Secret:
btnTwitterLogin = (TwitterLoginButton) findViewById(R.id.btnTwitterLogin);
btnTwitterLogin.setCallback(new Callback<TwitterSession>() {
@Override
public void success(Result<TwitterSession> result) {
startProgressBar();
UiHelpers.setStatus(txtStatus, "Local twitter sign-in successful, signing into server...");
TwitterSession session = result.data;
App.get().getServiceClient().postAsync(new dtos.Authenticate()
.setProvider("twitter")
.setAccessToken(session.getAuthToken().token)
.setAccessTokenSecret(session.getAuthToken().secret)
.setRememberMe(true),
r -> {
UiHelpers.setStatus(txtStatus, "Server twitter sign-in successful, opening chat...");
App.get().saveTwitterAccessToken(session.getAuthToken());
Intent intent = new Intent(activity, MainActivity.class);
stopProgressBar();
startActivity(intent);
},
error -> {
UiHelpers.setStatusError(txtStatus, "Server twitter sign-in failed", error);
stopProgressBar();
});
}
@Override
public void failure(TwitterException exception) {
Log.e(exception);
stopProgressBar();
}
});
The Server's Typed Request DTO's like Authenticate
can be generated by adding a
Java ServiceStack Reference to
chat.servicestack.net.
Behind the scenes the *Async
ServiceClient APIs are executed on an
Android's AsyncTask
where non-blocking HTTP Requests are performed on a background thread whilst their callbacks are
automatically executed on the UI thread so clients are able to update the UI with ServiceClient responses
without needing to marshal their UI updates on the UI Thread.
If the Server Authentication was successful we save the User's AuthToken which we'll use later so the next time the User launches the App they can automatically sign-in.
Now that the User has authenticated with the Chat Server, the Authenticated
Session Cookies are configured on our Service Client so we can now
open our MainActivity
and establish an authenticated Server Event connection to the Chat Server.
An additional callback we need to handle is onActivityResult()
to notify the Twitter and Facebook Login
buttons that the activity they've launched to capture the User's consent has completed:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
btnTwitterLogin.onActivityResult(requestCode, resultCode, data);
facebookCallback.onActivityResult(requestCode, resultCode, data);
if (requestCode == RC_SIGN_IN) {
handleGoogleSignInResult(Auth.GoogleSignInApi.getSignInResultFromIntent(data));
}
}
Facebook's Login button doesn't need us to explicitly specify our Facebook Application Id as it looks for it in our AndroidManifest.xml:
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
If you don't have a Facebook App, one can be created at developers.facebook.com/apps
The implementation for Facebook's LoginButton
follows a similar process to Twitter's where we need to
register a callback on Facebook's Login button to handle its onSuccess()
, onCancel()
and onError()
callbacks:
facebookCallback = CallbackManager.Factory.create();
btnFacebookLogin = (LoginButton) findViewById(R.id.btnFacebookLogin);
btnFacebookLogin.setReadPermissions("email"); // Ask user for permission to view access email address
btnFacebookLogin.registerCallback(facebookCallback, new FacebookCallback<LoginResult>() {
@Override
public void onSuccess(LoginResult loginResult) {
UiHelpers.setStatus(txtStatus, "Local facebook sign-in successful, signing into server...");
App.get().getServiceClient().postAsync(new dtos.Authenticate()
.setProvider("facebook")
.setAccessToken(loginResult.getAccessToken().getToken())
.setRememberMe(true),
r -> {
UiHelpers.setStatus(txtStatus, "Server facebook sign-in successful, opening chat...");
Intent intent = new Intent(activity, MainActivity.class);
stopProgressBar();
startActivity(intent);
},
error -> {
UiHelpers.setStatusError(txtStatus, "Server facebook sign-in failed", error);
stopProgressBar();
});
}
@Override
public void onCancel() {
stopProgressBar();
}
@Override
public void onError(FacebookException exception) {
Log.e(exception);
stopProgressBar();
}
});
The Authentication request to our Chat Server is similar to Twitter's except we only need to send 1 AccessToken to Authenticate with the Server and we don't need to explicitly save the User's Access Token as Facebook's SDK does this for us behind the scenes.
Whilst the sign-in process is similar, Google SignIn requires more effort to setup as you're left with implementing a lot of the mechanics yourself starting with having to choose an arbitrary Request Code which you'll need to use to manually check when the Google SignIn Activity has completed, this can be any number, e.g:
private static final int RC_SIGN_IN = 9001; //Arbitrary Request Code
We'll then need to configure your preferred GoogleSignInOptions
, as we want to be able to retrieve the
User's AccessToken we need to popoulate requestServerAuthCode()
with our Google OAuth App Id:
If you don't have a Google App, one can be created at console.developers.google.com/apis/credentials
SignInButton btnGoogleSignIn = (SignInButton) findViewById(R.id.sign_in_button);
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestServerAuthCode(getResources().getString(R.string.google_key))
.build();
googleApiClient = new GoogleApiClient.Builder(this)
.enableAutoManage(this, r -> { /* Handle On Connection Failed...*/ })
.addApi(Auth.GOOGLE_SIGN_IN_API, gso)
.build();
btnGoogleSignIn.setOnClickListener(v -> {
Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient);
startActivityForResult(signInIntent, RC_SIGN_IN);
});
We use that to configure a GoogleApiClient
and then manually bind Google's SignInButton
click handler
to launch a new SignIn Intent with our custom RC_SIGN_IN
.
Once the User has authorized with Google we're notified in onActivityResult()
which gets called back
with our custom RC_SIGN_IN
to let us know we can process the Google SignIn Result:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
//...
if (requestCode == RC_SIGN_IN) {
handleGoogleSignInResult(Auth.GoogleSignInApi.getSignInResultFromIntent(data));
}
}
If the GoogleSignInResult
was successful the User has Signed in locally to our App at that point but
as we need to Authenticate the User with the Chat Server we also need to retrieve their AccessToken.
Unfortunately Google only returns us a Server Auth Code which we need to use to call another Google API,
passing in our OAuth App Secret to get the User's AccessToken for our App:
private void handleGoogleSignInResult(GoogleSignInResult result) {
if (result.isSuccess()) {
GoogleSignInAccount acct = result.getSignInAccount();
UiHelpers.setStatus(txtStatus, "Local google sign-in successful, signing into server...");
Activity activity = this;
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = new FormBody.Builder()
.add("grant_type", "authorization_code")
.add("client_id", getResources().getString(R.string.google_key))
.add("client_secret", getResources().getString(R.string.google_secret))
.add("redirect_uri","")
.add("code", acct.getServerAuthCode())
.build();
Request request = new Request.Builder()
.url("https://www.googleapis.com/oauth2/v4/token")
.post(requestBody)
.build();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(Call call, IOException e) {
UiHelpers.setStatus(txtStatus, "Failed to retrieve AccessToken from Google");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String json = response.body().string();
JsonObject obj = JsonUtils.toJsonObject(json);
String accessToken = obj.get("access_token").getAsString();
App.get().saveGoogleAccessToken(accessToken);
App.get().getServiceClient().postAsync(new dtos.Authenticate()
.setProvider("GoogleOAuth")
.setAccessToken(accessToken)
.setRememberMe(true),
r -> {
UiHelpers.setStatus(txtStatus, "Server google sign-in successful, opening chat...");
stopProgressBar();
startActivity(new Intent(activity, MainActivity.class));
},
error -> {
UiHelpers.setStatusError(txtStatus, "Server google sign-in failed", error);
stopProgressBar();
});
}
});
}
}
Once we retrieve the accessToken
the process is similar to Twitter's where we save the AccessToken
to allow auto-SignIn's on restarts of our App, we then use the accessToken
to Authenticate with the
Chat Server before loading the MainActivity
to establish our Authenticated Server Events connection.
If the User doesn't have a Twitter or Facebook account we also let them login as a guest by skipping
Authentication with the Chat Server and open the MainActivity
where they'll connect as an
(Unauthenticated) anonymous user:
Button btnGuestLogin = (Button)findViewById(R.id.btnGuestLogin);
btnGuestLogin.setOnClickListener(view -> {
UiHelpers.setStatus(txtStatus, "Opening chat as guest...");
App.get().getServiceClient().clearCookies();
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
});
Another feature that's easily implemented is automatically signing in Users who've previously Signed-in and had their AccessToken saved which is done for both Twitter and Facebook with the code below:
dtos.Authenticate authDto = App.get().getSavedAccessToken();
if (authDto != null){
UiHelpers.setStatus(txtStatus, "Signing in with saved " + authDto.getProvider() + " AccessToken...");
App.get().getServiceClient().postAsync(authDto,
r -> {
Intent intent = new Intent(activity, MainActivity.class);
stopProgressBar();
startActivity(intent);
},
error -> {
UiHelpers.setStatusError(txtStatus,
"Error logging into " + authDto.getProvider() + " using Saved AccessToken", error);
stopProgressBar();
});
}
Which asks our App singleton to return a populated Authenticate
Request DTO if the User had previously
had their AccessToken saved.
For Facebook we can query AccessToken.getCurrentAccessToken()
to check for an existing AccessToken
whilst for Twitter and Google we check the Users SharedPreferences
which we manage ourselves:
public dtos.Authenticate getSavedAccessToken(){
AccessToken facebookAccessToken = AccessToken.getCurrentAccessToken();
if (facebookAccessToken != null){
return new dtos.Authenticate()
.setProvider("facebook")
.setAccessToken(facebookAccessToken.getToken())
.setRememberMe(true);
}
String googleAccessToken = prefs.getString("google.AccessToken", null);
if (googleAccessToken != null){
return new dtos.Authenticate()
.setProvider("GoogleOAuth")
.setAccessToken(googleAccessToken)
.setRememberMe(true);
}
String twitterAccessToken = prefs.getString("twitter.AccessToken", null);
String twitterAccessSecret = prefs.getString("twitter.AccessTokenSecret", null);
if (twitterAccessToken == null || twitterAccessSecret == null)
return null;
return new dtos.Authenticate()
.setProvider("twitter")
.setAccessToken(twitterAccessToken)
.setAccessTokenSecret(twitterAccessSecret)
.setRememberMe(true);
}
public void saveTwitterAccessToken(TwitterAuthToken authToken){
SharedPreferences.Editor editor = prefs.edit();
editor.putString("twitter.AccessToken", authToken.token);
editor.putString("twitter.AccessTokenSecret", authToken.secret);
editor.apply();
}
public void saveGoogleAccessToken(String accessToken){
SharedPreferences.Editor editor = prefs.edit();
editor.putString("google.AccessToken", accessToken);
editor.apply();
}
The LoginActivity
screen shows an example of using custom vector images for Sign In buttons. Clicking on the Logout Menu
Item on the Top Right Menu or sending the /logout
text message will launch the LoginActivity
:
As they look nicer in all resolutions the LoginActivity uses Vectors for its Image Buttons:
Which are referenced in backgrounds of custom ImageButton
in the Activity's
login.xml layout:
<ImageButton
android:id="@+id/btnTwitter"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_margin="10dp"
android:background="@drawable/ic_twitter_logo_blue" />
<ImageButton
android:id="@+id/btnFacebook"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_margin="10dp"
android:background="@drawable/ic_facebook_logo" />
<ImageButton
android:id="@+id/btnAnon"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_margin="10dp"
android:background="@drawable/ic_no_profile" />
We then assign a click handler on the ImageButton
which uses the TwitterAuthClient
directly to
initiate the OAuth flow for capturing our Users sign-in AccessToken which we can handle in the same
success()
callback to Authenticate with the Chat Server:
ImageButton btnTwitter = (ImageButton)findViewById(R.id.btnTwitter);
twitterAuth = new TwitterAuthClient();
btnTwitter.setOnClickListener(view -> {
startProgressBar();
twitterAuth.authorize(activity, new Callback<TwitterSession>() {
@Override
public void success(Result<TwitterSession> result) {
UiHelpers.setStatus(txtStatus, "Local twitter sign-in successful, signing into server...");
TwitterSession session = result.data;
App.get().getServiceClient().postAsync(new dtos.Authenticate()
.setProvider("twitter")
.setAccessToken(session.getAuthToken().token)
.setAccessTokenSecret(session.getAuthToken().secret)
.setRememberMe(true),
r -> {
UiHelpers.setStatus(txtStatus, "Server twitter sign-in successful, opening chat...");
App.get().saveTwitterAccessToken(session.getAuthToken());
Intent intent = new Intent(activity, MainActivity.class);
stopProgressBar();
startActivity(intent);
},
error -> {
UiHelpers.setStatusError(txtStatus, "Server twitter sign-in failed", error);
stopProgressBar();
});
}
@Override
public void failure(TwitterException exception) {
exception.printStackTrace();
stopProgressBar();
}
});
});
To enable custom Sign-ins with Facebook we need to use its LoginManager
singleton instance to register our
callback and initiate the User's OAuth Sign-in flow:
facebookCallback = CallbackManager.Factory.create();
LoginManager.getInstance().registerCallback(facebookCallback, new FacebookCallback<LoginResult>() {
@Override
public void onSuccess(LoginResult loginResult) {
UiHelpers.setStatus(txtStatus, "Local facebook sign-in successful, signing into server...");
App.get().getServiceClient().postAsync(new dtos.Authenticate()
.setProvider("facebook")
.setAccessToken(loginResult.getAccessToken().getToken())
.setRememberMe(true),
r -> {
UiHelpers.setStatus(txtStatus, "Server facebook sign-in successful, opening chat...");
Intent intent = new Intent(activity, MainActivity.class);
stopProgressBar();
startActivity(intent);
},
error -> {
UiHelpers.setStatusError(txtStatus, "Server facebook sign-in failed", error);
stopProgressBar();
});
}
@Override
public void onCancel() {
stopProgressBar();
}
@Override
public void onError(FacebookException exception) {
Log.e(exception);
stopProgressBar();
}
});
ImageButton btnFacebook = (ImageButton)findViewById(R.id.btnFacebook);
btnFacebook.setOnClickListener(view -> {
startProgressBar();
LoginManager.getInstance().logInWithReadPermissions(this, Arrays.asList("email"));
});
We also need to remember to notify the twitterAuth
and facebookCallback
that their Sign In Activities
have completed in our overridden onActivityResult()
:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
twitterAuth.onActivityResult(requestCode, resultCode, data);
facebookCallback.onActivityResult(requestCode, resultCode, data);
}
Non-blocking requests with Async Tasks are particularly nice in Java Android as they're performed on a background thread with responses transparently executed on the UI Thread enabling a simple UX-friendly programming model without suffering the same uncomposable viral nature of C#'s async.
This makes it easy to perform non-blocking tasks in Android and update UI widgets with their response as visible when loading Bitmaps which can directly update UI Widgets with a Java 8 lambda or method reference:
public void changeBackground(String message)
{
String url = message.startsWith("url(") ? message.substring(4, message.length() - 1) : message;
ImageView chatBackground = (ImageView)parentActivity.findViewById(R.id.chat_background);
App.get().readBitmap(url, chatBackground::setImageBitmap);
}
Which calls the simple LRU Cache in
App.java that leverages ServiceStack's AsyncUtils.readBitmap()
to download images from
URLs into Bitmaps:
private LruCache bitmapCache = new LruCache(4 * 1024 * 1024) {// 4MiB
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
public void readBitmap(final String url, final AsyncSuccess<Bitmap> success){
Bitmap cachedBitmap = (Bitmap)bitmapCache.get(url);
if (cachedBitmap != null){
success.success(cachedBitmap);
return;
}
AsyncUtils.readBitmap(url, imageBitmap -> {
bitmapCache.put(url, imageBitmap);
success.success(imageBitmap);
},
Throwable::printStackTrace);
}
For more info on ServiceStack's Java support utilized in this example checkout: