diff --git a/AndroidPushTutorial/README.md b/AndroidPushTutorial/README.md new file mode 100644 index 00000000..c0c02d41 --- /dev/null +++ b/AndroidPushTutorial/README.md @@ -0,0 +1,24 @@ +[![Ably](https://s3.amazonaws.com/files.ably.io/logo-with-type.png)](https://www.ably.io) + +--- + +# Android Push Tutorials + +This tutorial explains how to use Ably to send Push notification from your private server. + +There are some pre-requisites before you can run the app. + +1. Register FCM Token from Google Firebase Console +2. Install nodejs to run the local server insider `Server/` folder +3. Setup *.ngrok.io to make the local server accessible via Internet +4. Register an account with Ably and create required App keys. + + +## Tutorials Setup +1. After the above steps are done, open the `push-demo-server-registration` folder as a project in Android Studio +2. The project might take some time to sync. +3. Replace `google-services.json` into `push-tutorial-one` and `push-demo-server-registration` folder. +4. For this tutorial we have used `io.ably.tutorial.push_tutorial_two` as applicationId for the app respectively. So ensure your FCM Keys have same applicationId. Feel free to modify the tutorials to suit your purpose. +5. Edit `local.properties` file and add the keys, `ably.key` (retrieved from dashboard), `ably.env` (either production or sanbox), `base.url` (pointing to your ngrok.io domain) + + diff --git a/AndroidPushTutorial/Server/main.js b/AndroidPushTutorial/Server/main.js new file mode 100644 index 00000000..ab6d019a --- /dev/null +++ b/AndroidPushTutorial/Server/main.js @@ -0,0 +1,109 @@ +var Ably = require('ably') +//replace with actual API key or token +var client = Ably.Realtime(API_KEY) +var express = require('express') +var app = express() + +app.get('/auth', (req, res) => { + var tokenParams = { + 'clientId': req.query.clientId + }; + console.log("Authenticating client:", JSON.stringify(tokenParams)); + client.auth.createTokenRequest(tokenParams, function(err, tokenRequest) { + if (err) { + res.status(500).send('Error requesting token: ' + JSON.stringify(err)); + } else { + res.setHeader('Content-Type', 'application/json'); + res.send(JSON.stringify(tokenRequest)); + } + }); +}) + +app.get('/register', (req, res) => { + + console.log('Registering device') + var deviceId = req.query.deviceId; + var registrationToken = req.query.registrationToken; + var clientId = req.query.clientId; + //var deviceId and deviceToken to be received in the request object + var recipientDetails = { + //It's "fcm" for Android. + transportType: 'fcm', + //replace with actual device token + registrationToken:registrationToken + } + + var myDevice = { + id: deviceId, + clientId:clientId, + formFactor: 'phone', + metadata: 'PST', + platform: 'android', + push: { + recipient: recipientDetails + } + } + client.push.admin.deviceRegistrations.save(myDevice, (err, device) => { + if(err){ + console.log(err); + } else{ + console.log(device); + subscribeDevice(device); + res.setHeader('Content-Type', 'application/json'); + res.send(device); + } + + }) + +}) + +app.get('/push/device', function (req, res) { + var deviceId = req.query.deviceId; + + var recipient = { + deviceId: deviceId + + }; + var notification = { + notification: { + title: 'Hello from Ably!' + } + + }; + realtime.push.publish(recipient, notification, function(err) { + if (err) { + console.log('Unable to publish push notification; err = ' + err.message); + return; + } + console.log('Push notification published'); + res.send("Push Sent"); + }); +}) + +function subscribeDevice (device){ + var channelSub = { + channel: 'test_push_channel', + deviceId: device.id + } + client.push.admin.channelSubscriptions.save(channelSub, (err, channelSub) => { + if(err){ + console.log(err); + } else{ + console.log('Device subscribed to push channel with deviceId' + device.id) + } + }) +} + +app.get('/', (req, res) => { + console.log('Push Notifications tutorial with Ably') +}) + +app.listen(3000, () => { + console.log('APP LISTENING ON PORT 3000') +}) + + +/*Steps to run this server */ +// 1. Download the ably-js 1.1 node library using `npm install ably` +// 2. Replace the 'API_KEY' string with an actual key +// 3. Run the server using `node main.js` \ No newline at end of file diff --git a/AndroidPushTutorial/push-demo-server-registration/.gitignore b/AndroidPushTutorial/push-demo-server-registration/.gitignore new file mode 100644 index 00000000..40e59569 --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.idea +google-services.json diff --git a/AndroidPushTutorial/push-demo-server-registration/app/.gitignore b/AndroidPushTutorial/push-demo-server-registration/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/AndroidPushTutorial/push-demo-server-registration/app/build.gradle b/AndroidPushTutorial/push-demo-server-registration/app/build.gradle new file mode 100644 index 00000000..87a64d01 --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/build.gradle @@ -0,0 +1,49 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "io.ably.tutorial.push_tutorial_two" + minSdkVersion 23 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + Properties properties = new Properties() + properties.load(project.rootProject.file('local.properties').newDataInputStream()) + + buildConfigField "String", "ABLY_KEY", "\"${properties.getProperty('ably.key')}\"" + buildConfigField "String", "ABLY_ENV", "\"${properties.getProperty('ably.env')}\"" + buildConfigField "String", "SERVER_BASE_URL", "\"${properties.getProperty('base.url')}\"" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + //noinspection GradleCompatible + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:design:28.0.0' + implementation 'com.google.code.gson:gson:2.5' + + implementation 'com.google.firebase:firebase-messaging:17.3.4' + implementation 'com.google.firebase:firebase-core:16.0.1' + + implementation 'io.ably:ably-android:1.1.0' + implementation 'com.squareup.retrofit2:retrofit:2.5.0' + implementation 'com.squareup.retrofit2:converter-gson:2.5.0' + + implementation 'com.android.support.constraint:constraint-layout:1.1.3' +} + +/** + * Please ensure to add google-services.json file from your FCM Console. + */ +apply plugin: 'com.google.gms.google-services' diff --git a/AndroidPushTutorial/push-demo-server-registration/app/proguard-rules.pro b/AndroidPushTutorial/push-demo-server-registration/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/AndroidPushTutorial/push-demo-server-registration/app/src/main/AndroidManifest.xml b/AndroidPushTutorial/push-demo-server-registration/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c8981e95 --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/MainActivity.java b/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/MainActivity.java new file mode 100644 index 00000000..96c85d84 --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/MainActivity.java @@ -0,0 +1,216 @@ +package io.ably.tutorial.push_tutorial_two; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.provider.Settings; +import android.support.annotation.Nullable; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import com.google.gson.Gson; + +import io.ably.lib.push.LocalDevice; +import io.ably.lib.realtime.AblyRealtime; +import io.ably.lib.realtime.ConnectionStateListener; +import io.ably.lib.types.AblyException; +import io.ably.lib.types.ClientOptions; +import io.ably.lib.types.ErrorInfo; +import io.ably.lib.types.Param; +import io.ably.lib.util.IntentUtils; +import io.ably.tutorial.push_tutorial_two.receivers.AblyPushMessagingService; +import io.ably.tutorial.push_tutorial_two.server.NetResponse; +import io.ably.tutorial.push_tutorial_two.server.ServerAPI; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * Created by Amit S. + */ +public class MainActivity extends AppCompatActivity { + + public static final int SUCCESS = 0; + public static final int FAILURE = 1; + public static final int UPDATE_LOGS = 2; + + public static final String STEP_1 = "Initialize Ably"; + public static final String STEP_2 = "Register & Subscribe Channels via Server"; + + private static final String PRIVATE_SERVER_AUTH_URL = BuildConfig.SERVER_BASE_URL + "/auth"; + + //Broadcast receiver actions + public static final String ABLY_PUSH_ACTIVATE_ACTION = "io.ably.broadcast.PUSH_ACTIVATE"; + + private TextView rollingLogs; + private Button stepsButton; + private StringBuilder logs = new StringBuilder(); + private AblyRealtime ablyRealtime; + + + private Handler handler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case SUCCESS: + stepsButton.setText((String) msg.obj); + stepsButton.setEnabled(true); + break; + case FAILURE: + stepsButton.setEnabled(true); + break; + case UPDATE_LOGS: + rollingLogs.setText(logs.toString()); + break; + } + return false; + } + }); + + private BroadcastReceiver pushReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (ABLY_PUSH_ACTIVATE_ACTION.equalsIgnoreCase(intent.getAction())) { + ErrorInfo error = IntentUtils.getErrorInfo(intent); + if (error != null) { + logMessage("Error activating push service: " + error); + handler.sendMessage(handler.obtainMessage(FAILURE)); + return; + } + return; + } + + } + }; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + rollingLogs = findViewById(R.id.rolling_logs); + stepsButton = findViewById(R.id.steps); + LocalBroadcastManager.getInstance(this).registerReceiver(pushReceiver, new IntentFilter(ABLY_PUSH_ACTIVATE_ACTION)); + LocalBroadcastManager.getInstance(this).registerReceiver(pushReceiver, new IntentFilter(AblyPushMessagingService.PUSH_NOTIFICATION_ACTION)); + } + + public void performAction(View view) { + try { + Button button = (Button) view; + button.setEnabled(false); + String step = button.getText().toString(); + logMessage("Performing Step: " + step); + switch (step) { + case STEP_1: + initAblyRuntime(); + break; + case STEP_2: + initAblyPush(); + break; + } + } catch (AblyException e) { + logMessage("AblyException " + e.getMessage()); + handler.sendMessage(handler.obtainMessage(FAILURE)); + } + } + + /** + * By default we set client ID as the androidID. + * + * @return + */ + private String getClientId() { + String clientId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID); + return clientId; + } + + /** + * Step 1: Initialize Ably library and perform authentication with private server + * + * @throws AblyException + */ + private void initAblyRuntime() throws AblyException { + + ClientOptions options = new ClientOptions(); + options.environment = BuildConfig.ABLY_ENV; + options.key = BuildConfig.ABLY_KEY; + options.authUrl = PRIVATE_SERVER_AUTH_URL; + options.authParams = new Param[]{new Param("clientId", getClientId())}; + + ablyRealtime = new AblyRealtime(options); + ablyRealtime.setAndroidContext(getApplicationContext()); + ablyRealtime.connect(); + + ablyRealtime.connection.on(new ConnectionStateListener() { + @Override + public void onConnectionStateChanged(ConnectionStateChange state) { + logMessage("Connection state changed to : " + state.current.name()); + switch (state.current) { + case connected: + //Go to step 2 + handler.sendMessage(handler.obtainMessage(SUCCESS, STEP_2)); + break; + case disconnected: + case failed: + handler.sendMessage(handler.obtainMessage(FAILURE)); + break; + } + } + }); + } + + /** + * Step 2: Register for Push notification through private server. + * We also subscribe for the required channels on Server for the relevant device Id. + * + * @throws AblyException + */ + private void initAblyPush() throws AblyException { + LocalDevice device = ablyRealtime.push.getActivationContext().getLocalDevice(); + if (device.push.recipient == null) { + logMessage("Push not initialized. Please check Firebase settings"); + return; + } + String registrationToken = device.push.recipient.get("registrationToken").getAsString(); + if (registrationToken == null || registrationToken.length() == 0) { + logMessage("Registration token cannot be null. Please check Firebase settings"); + return; + } + String deviceId = device.id; + String clientId = getClientId(); + + logMessage("Sending registration Token: " + registrationToken); + logMessage("Device ID: " + deviceId); + logMessage("Client ID: " + clientId); + logMessage("Registering device via Server"); + logMessage("Subscribing channels"); + + ServerAPI.getInstance().api().register(deviceId, registrationToken, clientId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + //This is where device is successfully registered via Server. + logMessage("Successfully registered: " + new Gson().toJson(response.body())); + } + + @Override + public void onFailure(Call call, Throwable t) { + logMessage("Error registering with server: " + t.getMessage()); + handler.sendMessage(handler.obtainMessage(FAILURE)); + } + }); + } + + private void logMessage(String message) { + Log.i(MainActivity.class.getSimpleName(), message); + logs.append(message); + logs.append("\n"); + handler.sendMessage(handler.obtainMessage(UPDATE_LOGS)); + } +} diff --git a/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/receivers/AblyPushMessagingService.java b/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/receivers/AblyPushMessagingService.java new file mode 100644 index 00000000..09b4a68f --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/receivers/AblyPushMessagingService.java @@ -0,0 +1,23 @@ +package io.ably.tutorial.push_tutorial_two.receivers; + +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; + +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; + +/** + * + */ +public class AblyPushMessagingService extends FirebaseMessagingService { + public static final String PUSH_NOTIFICATION_ACTION = AblyPushMessagingService.class.getName() + ".PUSH_NOTIFICATION_MESSAGE"; + + @Override + public void onMessageReceived(RemoteMessage message) { + //FCM data is received here. + + Intent intent = new Intent(PUSH_NOTIFICATION_ACTION); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); + } + +} diff --git a/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/receivers/AblyPushRegistrationService.java b/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/receivers/AblyPushRegistrationService.java new file mode 100644 index 00000000..78df9262 --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/receivers/AblyPushRegistrationService.java @@ -0,0 +1,15 @@ +package io.ably.tutorial.push_tutorial_two.receivers; + +import io.ably.lib.push.AblyFirebaseInstanceIdService; + +/** + * Leave this empty as the base class AblyFirebaseInstanceIdService does the FCM token registration. + * In case your app requires access to FCM token as well, then override io.ably.lib.push.AblyFirebaseInstanceIdService#onTokenRefresh() + */ +public class AblyPushRegistrationService extends AblyFirebaseInstanceIdService { + @Override + public void onTokenRefresh() { + //Make sure to call super.onTokenRefresh to initialize Ably push environment. + super.onTokenRefresh(); + } +} diff --git a/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/server/NetResponse.java b/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/server/NetResponse.java new file mode 100644 index 00000000..987013fa --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/server/NetResponse.java @@ -0,0 +1,18 @@ +package io.ably.tutorial.push_tutorial_two.server; + +import io.ably.lib.rest.DeviceDetails; + +/** + * Created by Amit S. + *

+ * {"id":"123","platform":"android","formFactor":"phone","clientId":"123", + * "metadata":"PST", + * "deviceIdentityToken":{"token":"TOKEN","keyName":"KEY_NAME","issued":1551196978541,"expires":1582732978541,"capability":"{}","clientId":"123","deviceId":"123"},"push":{"recipient":{"transportType":"fcm","registrationToken":"123"},"state":"ACTIVE"}} + */ +public class NetResponse { + public String id; + public String platform; + public String formFactor; + public String clientId; + DeviceDetails.Push push; +} diff --git a/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/server/ServerAPI.java b/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/server/ServerAPI.java new file mode 100644 index 00000000..d64fccac --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/src/main/java/io/ably/tutorial/push_tutorial_two/server/ServerAPI.java @@ -0,0 +1,45 @@ +package io.ably.tutorial.push_tutorial_two.server; + +import io.ably.tutorial.push_tutorial_two.BuildConfig; +import retrofit2.Call; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.http.GET; +import retrofit2.http.Query; + +/** + * Created by Amit S. + */ +public class ServerAPI { + + + public interface API { + @GET("/register") + Call register(@Query("deviceId") String deviceId, @Query("registrationToken") String registrationToken, @Query("clientId") String clientId); + } + + + private static ServerAPI instance; + + public static ServerAPI getInstance() { + if (instance == null) { + instance = new ServerAPI(); + } + return instance; + } + + private final API api; + + private ServerAPI() { + Retrofit.Builder builder = new Retrofit.Builder(); + builder.baseUrl(BuildConfig.SERVER_BASE_URL); + builder.addConverterFactory(GsonConverterFactory.create()); + + Retrofit retrofit = builder.build(); + api = retrofit.create(API.class); + } + + public API api() { + return api; + } +} diff --git a/AndroidPushTutorial/push-demo-server-registration/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/AndroidPushTutorial/push-demo-server-registration/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..1f6bb290 --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/AndroidPushTutorial/push-demo-server-registration/app/src/main/res/drawable/ic_launcher_background.xml b/AndroidPushTutorial/push-demo-server-registration/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..0d025f9b --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AndroidPushTutorial/push-demo-server-registration/app/src/main/res/layout/activity_main.xml b/AndroidPushTutorial/push-demo-server-registration/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..828ac425 --- /dev/null +++ b/AndroidPushTutorial/push-demo-server-registration/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,27 @@ + + + + + +