Skip to content

Commit

Permalink
Add Push Notification example (#54)
Browse files Browse the repository at this point in the history
* add api client with grpc kotlin

* first pass at notification service code for decoding

* update the pending intent

* add more setup for firebase

* copy over the proto code

* fix package naming

* add XMTPPush file to library

* clean up code a bit

* add the xmtp push subscribe and register

* testing pushes locally

* clean up the generated code for readability

* correctly export the java

* fix up lint

* more code clean up

* remove google servies json that should be added by the tester

* add it to git ignore

* add a readme to the push folder

* the linter requires the google-services json file

* add a few clean up nits

* pull loading keys out into a resuable function

* add required items to the gradle file but comment out so not included in library

* add commented out gradle

* add extra details to the read me

* jha first pass of push README

* update based on naomi's feedback <3

* a few more README tweaks

* add link to push readme to main readme

* Update README.md

---------

Co-authored-by: Jennifer Hasegawa <[email protected]>
  • Loading branch information
nplasterer and jhaaaa authored Mar 22, 2023
1 parent 1e26193 commit ab674a3
Show file tree
Hide file tree
Showing 19 changed files with 5,437 additions and 18 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ To learn more about XMTP and get answers to frequently asked questions, see [FAQ

For a basic demonstration of the core concepts and capabilities of the `xmtp-android` client SDK, see the [Example app project](https://github.com/xmtp/xmtp-android/tree/main/example). This is currently a work in progress.

To learn about example app push notifications, see [Enable the example app to send push notifications](library/src/main/java/org/xmtp/android/library/push/README.md).

## Install from Maven Central

You can find the latest package version on [Maven Central](https://central.sonatype.com/artifact/org.xmtp/android/0.0.5/versions)
Expand Down
13 changes: 11 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
//buildscript {
// repositories {
// google()
// mavenCentral()
// }
// dependencies {
// classpath 'com.google.gms:google-services:4.3.15'
// }
//}

plugins {
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
id 'io.github.gradle-nexus.publish-plugin' version "1.2.0"
}

Expand Down
7 changes: 5 additions & 2 deletions example/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
buildscript {
ext {
lifecycle_version = '2.5.1'
lifecycle_version = '2.6.0'
}

}

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
// id 'com.google.gms.google-services'
}

android {
Expand Down Expand Up @@ -55,11 +57,12 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'com.google.firebase:firebase-messaging:23.1.2'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.fragment:fragment-ktx:1.5.5'
implementation 'androidx.activity:activity-ktx:1.6.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'dev.pinkroom:walletconnectkit:0.3.2'
implementation 'org.web3j:crypto:5.0.0'

Expand Down
49 changes: 49 additions & 0 deletions example/google-services.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"project_info": {
"project_number": "609788839593",
"project_id": "example-notification-server",
"storage_bucket": "example-notification-server.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:609788839593:android:0ecb48b4bdb4d0bc240bb8",
"android_client_info": {
"package_name": "org.xmtp.android.example"
}
},
"oauth_client": [
{
"client_id": "609788839593-82gepnf8nv5ml9oh34or96qqijl1si0i.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBnPkGEV3VYzx0VK42Q2lmhZaxufb7VIWc"
},
{
"current_key": "AIzaSyA1jP-bfReDs_fGP4uJ7tFzItVS15tgFyg"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "609788839593-82gepnf8nv5ml9oh34or96qqijl1si0i.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "609788839593-jvlrovi291ge1rblkb7blhvopoas2hpb.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "org.reactjs.native.example.example-chat-rn"
}
}
]
}
}
}
],
"configuration_version": "1"
}
14 changes: 12 additions & 2 deletions example/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
Expand Down Expand Up @@ -34,10 +36,10 @@
</activity>
<activity
android:name=".connect.ConnectWalletActivity"
android:exported="false"></activity>
android:exported="false" />
<activity
android:name=".conversation.ConversationDetailActivity"
android:exported="false"></activity>
android:exported="false" />

<service
android:name=".account.AuthenticatorService"
Expand All @@ -49,5 +51,13 @@
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>

<service
android:name=".pushnotifications.PushNotificationsService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder

object ClientManager {

val CLIENT_OPTIONS = ClientOptions(api = ClientOptions.Api(XMTPEnvironment.DEV))
val CLIENT_OPTIONS = ClientOptions(api = ClientOptions.Api(XMTPEnvironment.PRODUCTION))

private val _clientState = MutableStateFlow<ClientState>(ClientState.Unknown)
val clientState: StateFlow<ClientState> = _clientState
Expand Down
13 changes: 6 additions & 7 deletions example/src/main/java/org/xmtp/android/example/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import org.xmtp.android.example.conversation.ConversationsAdapter
import org.xmtp.android.example.conversation.ConversationsClickListener
import org.xmtp.android.example.conversation.NewConversationBottomSheet
import org.xmtp.android.example.databinding.ActivityMainBinding
import org.xmtp.android.example.pushnotifications.PushNotificationTokenManager
import org.xmtp.android.example.utils.KeyUtil
import org.xmtp.android.library.Conversation

class MainActivity : AppCompatActivity(),
Expand All @@ -37,8 +39,10 @@ class MainActivity : AppCompatActivity(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
accountManager = AccountManager.get(this)
PushNotificationTokenManager.init(this, "10.0.2.2:8080")
viewModel.setupPush()

val keys = loadKeys()
val keys = KeyUtil(this).loadKeys()
if (keys == null) {
showSignIn()
return
Expand Down Expand Up @@ -156,12 +160,6 @@ class MainActivity : AppCompatActivity(),
}
}

private fun loadKeys(): String? {
val accounts = accountManager.getAccountsByType(resources.getString(R.string.account_type))
val account = accounts.firstOrNull() ?: return null
return accountManager.getPassword(account)
}

private fun showSignIn() {
startActivity(Intent(this, ConnectWalletActivity::class.java))
finish()
Expand All @@ -174,6 +172,7 @@ class MainActivity : AppCompatActivity(),

private fun disconnectWallet() {
ClientManager.clearClient()
PushNotificationTokenManager.clearXMTPPush()
val accounts = accountManager.getAccountsByType(resources.getString(R.string.account_type))
accounts.forEach { account ->
accountManager.removeAccount(account, null, null, null)
Expand Down
16 changes: 13 additions & 3 deletions example/src/main/java/org/xmtp/android/example/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import org.xmtp.android.example.extension.flowWhileShared
import org.xmtp.android.example.extension.stateFlow
import org.xmtp.android.example.pushnotifications.PushNotificationTokenManager
import org.xmtp.android.library.Conversation
import org.xmtp.android.library.DecodedMessage

Expand All @@ -25,6 +26,13 @@ class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading(null))
val uiState: StateFlow<UiState> = _uiState

@UiThread
fun setupPush() {
viewModelScope.launch(Dispatchers.IO) {
PushNotificationTokenManager.ensurePushTokenIsConfigured()
}
}

@UiThread
fun fetchConversations() {
when (val uiState = uiState.value) {
Expand All @@ -34,8 +42,10 @@ class MainViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
val listItems = mutableListOf<MainListItem>()
try {
val conversations = ClientManager.client.conversations.list()
PushNotificationTokenManager.xmtpPush.subscribe(conversations.map { it.topic })
listItems.addAll(
ClientManager.client.conversations.list().map { conversation ->
conversations.map { conversation ->
val lastMessage = fetchMostRecentMessage(conversation)
MainListItem.ConversationItem(
id = conversation.topic,
Expand Down Expand Up @@ -99,13 +109,13 @@ class MainViewModel : ViewModel() {
data class ConversationItem(
override val id: String,
val conversation: Conversation,
val mostRecentMessage: DecodedMessage?
val mostRecentMessage: DecodedMessage?,
) : MainListItem(id, ITEM_TYPE_CONVERSATION)

data class Footer(
override val id: String,
val address: String,
val environment: String
val environment: String,
) : MainListItem(id, ITEM_TYPE_FOOTER)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import java.util.regex.Pattern
import kotlinx.coroutines.launch
import org.xmtp.android.example.R
import org.xmtp.android.example.databinding.BottomSheetNewConversationBinding
import java.util.regex.Pattern

class NewConversationBottomSheet : BottomSheetDialogFragment() {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.xmtp.android.example.pushnotifications

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.util.Log
import androidx.annotation.UiThread
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.FirebaseMessagingService
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.runBlocking
import org.xmtp.android.example.R
import org.xmtp.android.library.push.XMTPPush

object PushNotificationTokenManager {

private const val TAG = "PushTokenManager"
private lateinit var applicationContext: Context

private val _xmtpPushState = MutableStateFlow<XMTPPushState>(XMTPPushState.Unknown)
val xmtpPushState: StateFlow<XMTPPushState> = _xmtpPushState

private var _xmtpPush: XMTPPush? = null

val xmtpPush: XMTPPush
get() = if (xmtpPushState.value == XMTPPushState.Ready) {
_xmtpPush!!
} else {
throw IllegalStateException("Push not setup")
}

fun init(applicationContext: Context, pushServer: String) {
this.applicationContext = applicationContext
createXMTPPush(pushServer)
}

fun ensurePushTokenIsConfigured() {
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { request ->
if (!request.isSuccessful) {
Log.e(TAG, "Firebase getInstanceId() failed", request.exception)
return@OnCompleteListener
}
request.result?.let {
if (xmtpPushState.value is XMTPPushState.Ready) {
xmtpPush.register(it)
configureNotificationChannels()
}
}
})
}

internal fun syncPushNotificationsToken(token: String) {
if (xmtpPushState.value is XMTPPushState.Ready) {
runBlocking { xmtpPush.register(token) }
}
}

private fun configureNotificationChannels() {
val channel = NotificationChannel(
PushNotificationsService.CHANNEL_ID,
applicationContext.getString(R.string.xmtp_direct_message),
NotificationManager.IMPORTANCE_DEFAULT
)

val notificationManager = applicationContext.getSystemService(
FirebaseMessagingService.NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.createNotificationChannel(channel)
}

@UiThread
fun createXMTPPush(pushServer: String) {
if (xmtpPushState.value is XMTPPushState.Ready) return
try {
_xmtpPush = XMTPPush(applicationContext, pushServer)
_xmtpPushState.value = XMTPPushState.Ready
} catch (e: Exception) {
_xmtpPushState.value = XMTPPushState.Error(e.localizedMessage.orEmpty())
}
}

@UiThread
fun clearXMTPPush() {
_xmtpPushState.value = XMTPPushState.Unknown
_xmtpPush = null
}

sealed class XMTPPushState {
object Unknown : XMTPPushState()
object Ready : XMTPPushState()
data class Error(val message: String) : XMTPPushState()
}
}
Loading

0 comments on commit ab674a3

Please sign in to comment.