Kotlin

PushExpress -- SDK Kotlin

You will need to integrate your Push.Express account with Firebase.

Setup Firebase

  1. Go to Firebase Console and create a new project (or use existing one)

    You can use one project for all your apps.

  2. Open Project Settings -> General

  3. Create new Android app or just download google-services.json from existing app

If you need to create new app, just:

  • Register it

  • Download google-services.json

  • Press next-next-next =)

  1. Put google-services.json to your app dir (like <project>/app/google-services.json)

  2. Add the plugin as a dependency to your project-level build.gradle.kts file: Root-level (project-level) Gradle file (/build.gradle.kts)::

    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    plugins {
        alias(libs.plugins.android.application) apply false
        alias(libs.plugins.kotlin.android) apply false
        alias(libs.plugins.kotlin.compose) apply false
        // Add the dependency for the Google services Gradle plugin
        id("com.google.gms.google-services") version "4.4.3" apply false
    }
  3. In your module (app-level) Gradle file (<project>/<app-module>/build.gradle.kts), add the Firebase Cloud Messaging dependency:

    plugins {
        alias(libs.plugins.android.application)
        alias(libs.plugins.kotlin.android)
        alias(libs.plugins.kotlin.compose)
    
        id("com.google.gms.google-services")
    }
    
    dependencies {
        // Import the Firebase BoM
        implementation(platform("com.google.firebase:firebase-bom:33.0.0"))
    
        // ...
    }

Full official instructions can be found in Firebase Cloud Messaging Android guide.

Setup Push.Express

Get Firebase Private key

  1. Go to Firebase Console and create a new project (or use existing one)

    You can use one project for all your apps.

  2. Open Project Settings

  3. Go to Service accounts, press Generate new private key and save it to file private-key.json (you can use same key for all apps)

Integrate your Push.Express App with Firebase

  1. Go to your Push.Express account

  2. Open existing App settings or create a new App

  3. Switch type application Firebase

  4. Paste private-key.json file to Firebase Admin SDK private key textbox

Add sdk in your application

Before proceeding with the integration, make sure you have already installed the Firebase SDK in your application. If not, follow steps 4-6 in the Firebase Cloud Messaging integration guide

Add the JitPack repository to your build file

Ensure you have the latest Android Studio and Android Gradle Plugin!

In your settings.gradle.kts, add the Jitpack repo to repositories list (only if you use Gradle Centralized Repository Declaration feature, default for new projects since Android Studio Electric Eel):

// settings.gradle (Project Settings) in Android Studio
// ...
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        // ...
         maven { url = uri("https://jitpack.io") }
    }
}
Alternatively, if you use old `allprojects` style

In your root-level (project-level) Gradle file (<project>/build.gradle), add the Jitpack repo to repositories list:

// build.gradle (Project: My_Application) in Android Studio
// ...
allprojects {
    repositories {
        // ...
        maven { url "https://jitpack.io" }
    }
}

Add Push.Express SDK dependency

In your module (app-level) Gradle file (<project>/<app-module>/build.gradle), add the pushexpress-android-sdk dependency:

// build.gradle (Module :app) in Android Studio
// ...
dependencies {
    // ...
     implementation ("com.github.pushexpress:pushexpress-android-sdk:v1.4.0")
}

Add AndroidManifest.xml

To correctly open links from push notifications in the app, add an Intent filter:

 <!-- Intent filter for launching the application -->
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <!-- Intent filter for deep links with yourapp:// scheme -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data
                    android:scheme="yourapp"
                    android:host="open" />
            </intent-filter>

            <!-- Intent filter for deep links with myapp:// scheme -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data
                    android:scheme="myapp"
                    android:host="open" />
            </intent-filter>

            <!-- Intent filter for HTTPS URLs (for push notifications) -->
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data
                    android:scheme="https"
                    android:host="push.express" />
                <data android:scheme="http" />
            </intent-filter>

            <!-- Fallback filters to catch invalid deep links from PushExpress SDK -->
            
            <!-- Option 1: WITHOUT data element - for any VIEW without URI -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            
            <!-- Option 2: Empty scheme - for empty URIs -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="" />
            </intent-filter>
            
            <!-- Option 3: Schemes for plain strings without protocol -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="ai-bot" />
                <data android:scheme="content" />
                <data android:scheme="file" />
            </intent-filter>

Add required code

  1. Get your PUSHEXPRESS_APP_ID from Push.Express account page

  2. Add code to your Android Studio app

    import com.pushexpress.sdk.main.SdkPushExpress
    
    const val PUSHEXPRESS_APP_ID = "####-######"
    
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
         askNotificationPermission()
    
         if (savedInstanceState == null) {
             getFirebaseTokenAndActivate()
         }
    }
  3. Ask for notification permissions

     private val notificationPermissionLauncher = registerForActivityResult(
         ActivityResultContracts.RequestPermission()
     ) { isGranted: Boolean ->
         if (isGranted) {
             Toast.makeText(this, "Notifications permission granted", Toast.LENGTH_SHORT)
                 .show()
         } else {
             Toast.makeText(
                 this,
                 "FCM can't post notifications without POST_NOTIFICATIONS permission",
                 Toast.LENGTH_LONG
             ).show()
         }
     }
    
     private fun askNotificationPermission() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
             if (ContextCompat.checkSelfPermission(this,
                     android.Manifest.permission.POST_NOTIFICATIONS) ==
                 PackageManager.PERMISSION_GRANTED
             ) {
                 // Permission already granted
             } else {
                 notificationPermissionLauncher.launch(
                     android.Manifest.permission.POST_NOTIFICATIONS)
             }
         }
     }
  4. Generate a firebase token and transfer it to the push express sdk before activating the sdk.

    private fun getFirebaseTokenAndActivate() {
            FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
                if (task.isSuccessful) {
                    val token = task.result
                    Log.d("MyApp", "Firebase token: $token")
    
                    CoroutineScope(Dispatchers.IO).launch {
                        SdkPushExpress.setFirebaseToken(token)
                        initializeSdk()
    
                        Log.d("MyApp", "SDK activated with token")
                    }
                } else {
                    Log.e("MyApp", "Failed to get Firebase token", task.exception)
                }
            }
        }
    private fun initializeSdk() {
            lifecycleScope.launch {
                try {
                    SdkPushExpress.initialize(PUSHEXPRESS_APP_ID)
                    SdkPushExpress.setExternalId("some_external_id")
                    SdkPushExpress.activate()
    
                    Log.d("Myapp", "App Instance Token: ${SdkPushExpress.getInstanceToken()}")
                    Log.d("Myapp", "App External ID: ${SdkPushExpress.getExternalId()}")
                } catch (e: Exception) {
                    Log.e("Myapp", "SDK initialization failed", e)
                }
            }
        }
  5. Login logout functionality

    To create different accounts on the same device, use the sdk activation and decoding functionality. To log out of your account, use SdkPushExpress.deactivate(). To log in to another account, create a new setExternalId - SdkPushExpress.setExternalId("new_ext_id") run SdkPushExpress.activate(). Example of the implementation of login logout functions in MainActivity.kt:

        private fun logout() {
         lifecycleScope.launch {
             try {
                    SdkPushExpress.deactivate()
                    Toast.makeText(this@MainActivity, "Logout initiated", Toast.LENGTH_SHORT).show()
                    Log.d("Myapp", "Deactivate called - logout initiated")
                } catch (e: Exception) {
                    Log.e("Myapp", "Logout failed", e)
                }
            }
        }
    
        private fun login() {
            lifecycleScope.launch {
                try {
                    SdkPushExpress.deactivate()
                    val timestamp = System.currentTimeMillis()
                    SdkPushExpress.setExternalId("ext_id_$timestamp")
                    Log.d("Myapp", "App External ID: ${SdkPushExpress.getExternalId()}")
                    SdkPushExpress.activate()
                    Toast.makeText(this@MainActivity, "Login initiated", Toast.LENGTH_SHORT).show()
                    Log.d("Myapp", "Activate called - login initiated")
                } catch (e: Exception) {
                    Log.e("Myapp", "Login failed", e)
                }
            }
        }
  6. Example processing links from push notifications in MainActivity.kt

        override fun onNewIntent(intent: Intent) {
            super.onNewIntent(intent)
            Log.d("Myapp", "onNewIntent called with: ${intent.data}")
    
            setIntent(intent)
    
            handleIntent(intent)
        }
    
        private fun handleIntent(intent: Intent?) {
            if (intent == null) {
                Log.d("Myapp", "handleIntent: intent is null")
                showMainScreen()
                return
            }
    
            Log.d("Myapp", "handleIntent called, action: ${intent.action}, data: ${intent.data}")
    
            intent.extras?.let { bundle ->
                Log.d("Myapp", "Intent extras:")
                for (key in bundle.keySet()) {
                    Log.d("Myapp", "  $key: ${bundle.get(key)}")
                }
            }
    
            try {
                when {
                    // Deep link from URI
                    intent.action == Intent.ACTION_VIEW && intent.data != null -> {
                        val uri = intent.data!!
                        Log.d("Myapp", "Deep Link received: $uri")
                        
                        // Checking the validity of the URI
                        if (isValidUri(uri)) {
                            processDeepLink(uri)
                        } else {
                            Log.w("Myapp", "Invalid URI received: $uri")
                            Toast.makeText(this, "Invalid deep was received link: $uri", Toast.LENGTH_LONG).show()
                            showMainScreen()
                        }
                        return
                    }
    
                    // Deep link from extras
                    intent.hasExtra("deep_link") -> {
                        val deepLink = intent.getStringExtra("deep_link")
                        Log.d("Myapp", "Deep Link from extra: $deepLink")
                        deepLink?.let { 
                            parseAndProcessDeepLink(it)
                        }
                        return
                    }
    
                    // Push notification data
                    intent.hasExtra("url") || intent.hasExtra("link") -> {
                        val url = intent.getStringExtra("url") ?: intent.getStringExtra("link")
                        Log.d("Myapp", "URL from push: $url")
                        url?.let { 
                            parseAndProcessDeepLink(it)
                        }
                        return
                    }
                }
            } catch (e: Exception) {
                Log.e("Myapp", "Error handling intent", e)
                Toast.makeText(this, "Link processing error: ${e.message}", Toast.LENGTH_SHORT).show()
            }
    
            // Normal application launch
            showMainScreen()
        }
    
        private fun isValidUri(uri: Uri): Boolean {
            val scheme = uri.scheme
            
            // Check that the URI has a valid scheme
            if (scheme.isNullOrBlank()) {
                return false
            }
    
            // Check that the scheme is supported
            val supportedSchemes = listOf("http", "https", "myapp", "yourapp")
            return scheme.lowercase() in supportedSchemes
        }
    
        private fun parseAndProcessDeepLink(urlString: String) {
            try {
                // Check that the string is not empty
                if (urlString.isBlank()) {
                    Log.w("Myapp", "Empty URL string")
                    Toast.makeText(this, "Empty link received", Toast.LENGTH_SHORT).show()
                    return
                }
    
                // Parse URI
                val uri = Uri.parse(urlString)
                
                // Check that URI has a valid scheme
                val scheme = uri.scheme
                if (scheme.isNullOrBlank()) {
                    Log.w("Myapp", "Invalid URL: no scheme found in '$urlString'")
                    Toast.makeText(this, "Invalid link format: scheme is missing (http://, https://, myapp://, etc.)", Toast.LENGTH_LONG).show()
                    showMainScreen()
                    return
                }
    
                // Check that the scheme is supported
                val supportedSchemes = listOf("http", "https", "myapp", "yourapp")
                if (scheme.lowercase() !in supportedSchemes) {
                    Log.w("Myapp", "Unsupported URL scheme: $scheme in '$urlString'")
                    Toast.makeText(this, "Unsupported scheme: $scheme", Toast.LENGTH_SHORT).show()
                    showMainScreen()
                    return
                }
    
                // If everything is valid, process the deep link
                processDeepLink(uri)
                
            } catch (e: Exception) {
                Log.e("Myapp", "Error parsing URL: '$urlString'", e)
                Toast.makeText(this, "Link parsing error: ${e.message}", Toast.LENGTH_LONG).show()
                showMainScreen()
            }
        }
    
        private fun processDeepLink(uri: Uri) {
            Log.d("Myapp", "Processing deep link: $uri")
            Toast.makeText(this, "Deep link: $uri", Toast.LENGTH_LONG).show()
    
            // Add your deep link processing logic here
            // For example, navigation to a specific screen
        }
    
        private fun showMainScreen() {
            Log.d("Myapp", "Show main screen")
        }
  7. Example MainActivity.kt

    package com.example.kotlinbug3
    
    import android.content.Context
    import android.content.Intent
    import android.content.pm.PackageManager
    import android.net.Uri
    import android.os.Build
    import android.os.Bundle
    import android.util.Log
    import android.widget.Toast
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import androidx.activity.result.contract.ActivityResultContracts
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.foundation.layout.padding
    import androidx.compose.material3.Button
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.Text
    import androidx.compose.ui.unit.dp
    import androidx.compose.runtime.Composable
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.tooling.preview.Preview
    import androidx.core.content.ContextCompat
    import com.example.kotlinbug3.ui.theme.KotlinBug3Theme
    import androidx.lifecycle.lifecycleScope
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.CoroutineScope
    import kotlinx.coroutines.Dispatchers
    import com.google.firebase.messaging.FirebaseMessaging
    import com.pushexpress.sdk.main.SdkPushExpress
    
    
    const val PUSHEXPRESS_APP_ID = "43766"
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            Log.d("Myapp", "onCreate called")
    
            askNotificationPermission()
    
            if (savedInstanceState == null) {
                getFirebaseTokenAndActivate()
            }
    
            enableEdgeToEdge()
            setContent {
                KotlinBug3Theme {
                    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                        Greeting(
                            name = "Android",
                            modifier = Modifier.padding(innerPadding),
                            onLogoutClick = { logout() },
                            onLoginClick = { login() }
                        )
                    }
                }
            }
    
            handleIntent(intent)
        }
    
        private fun getFirebaseTokenAndActivate() {
            FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
                if (task.isSuccessful) {
                    val token = task.result
                    Log.d("MyApp", "Firebase token: $token")
    
                    CoroutineScope(Dispatchers.IO).launch {
                        SdkPushExpress.setFirebaseToken(token)
                        initializeSdk()
    
                        Log.d("MyApp", "SDK activated with token")
                    }
                } else {
                    Log.e("MyApp", "Failed to get Firebase token", task.exception)
                }
            }
        }
    
        private fun initializeSdk() {
            lifecycleScope.launch {
                try {
                    SdkPushExpress.initialize(PUSHEXPRESS_APP_ID)
                    SdkPushExpress.setExternalId("some_external_id")
                    SdkPushExpress.activate()
    
                    Log.d("Myapp", "App Instance Token: ${SdkPushExpress.getInstanceToken()}")
                    Log.d("Myapp", "App External ID: ${SdkPushExpress.getExternalId()}")
                } catch (e: Exception) {
                    Log.e("Myapp", "SDK initialization failed", e)
                }
            }
        }
    
        override fun onNewIntent(intent: Intent) {
            super.onNewIntent(intent)
            Log.d("Myapp", "onNewIntent called with: ${intent.data}")
    
            setIntent(intent)
    
            handleIntent(intent)
        }
    
        private fun handleIntent(intent: Intent?) {
            if (intent == null) {
                Log.d("Myapp", "handleIntent: intent is null")
                showMainScreen()
                return
            }
    
            Log.d("Myapp", "handleIntent called, action: ${intent.action}, data: ${intent.data}")
    
            intent.extras?.let { bundle ->
                Log.d("Myapp", "Intent extras:")
                for (key in bundle.keySet()) {
                    Log.d("Myapp", "  $key: ${bundle.get(key)}")
                }
            }
    
            try {
                when {
                    // Deep link from URI
                    intent.action == Intent.ACTION_VIEW && intent.data != null -> {
                        val uri = intent.data!!
                        Log.d("Myapp", "Deep Link received: $uri")
    
                        // Checking the validity of the URI
                        if (isValidUri(uri)) {
                            processDeepLink(uri)
                        } else {
                            Log.w("Myapp", "Invalid URI received: $uri")
                            Toast.makeText(this, "Invalid deep was received link: $uri", Toast.LENGTH_LONG).show()
                            showMainScreen()
                        }
                        return
                    }
    
                    // Deep link from extras
                    intent.hasExtra("deep_link") -> {
                        val deepLink = intent.getStringExtra("deep_link")
                        Log.d("Myapp", "Deep Link from extra: $deepLink")
                        deepLink?.let {
                            parseAndProcessDeepLink(it)
                        }
                        return
                    }
    
                    // Push notification data
                    intent.hasExtra("url") || intent.hasExtra("link") -> {
                        val url = intent.getStringExtra("url") ?: intent.getStringExtra("link")
                        Log.d("Myapp", "URL from push: $url")
                        url?.let {
                            parseAndProcessDeepLink(it)
                        }
                        return
                    }
                }
            } catch (e: Exception) {
                Log.e("Myapp", "Error handling intent", e)
                Toast.makeText(this, "Link processing error: ${e.message}", Toast.LENGTH_SHORT).show()
            }
    
            // Normal application launch
            showMainScreen()
        }
    
        private fun isValidUri(uri: Uri): Boolean {
            val scheme = uri.scheme
    
            // Check that the URI has a valid scheme
            if (scheme.isNullOrBlank()) {
                return false
            }
    
            // Check that the scheme is supported
            val supportedSchemes = listOf("http", "https", "myapp", "yourapp")
            return scheme.lowercase() in supportedSchemes
        }
    
        private fun parseAndProcessDeepLink(urlString: String) {
            try {
                // Check that the string is not empty
                if (urlString.isBlank()) {
                    Log.w("Myapp", "Empty URL string")
                    Toast.makeText(this, "Empty link received", Toast.LENGTH_SHORT).show()
                    return
                }
    
                // Parse URI
                val uri = Uri.parse(urlString)
    
                // Check that URI has a valid scheme
                val scheme = uri.scheme
                if (scheme.isNullOrBlank()) {
                    Log.w("Myapp", "Invalid URL: no scheme found in '$urlString'")
                    Toast.makeText(this, "Invalid link format: scheme is missing (http://, https://, myapp://, etc.)", Toast.LENGTH_LONG).show()
                    showMainScreen()
                    return
                }
    
                // Check that the scheme is supported
                val supportedSchemes = listOf("http", "https", "myapp", "yourapp")
                if (scheme.lowercase() !in supportedSchemes) {
                    Log.w("Myapp", "Unsupported URL scheme: $scheme in '$urlString'")
                    Toast.makeText(this, "Unsupported scheme: $scheme", Toast.LENGTH_SHORT).show()
                    showMainScreen()
                    return
                }
    
                // If everything is valid, process the deep link
                processDeepLink(uri)
    
            } catch (e: Exception) {
                Log.e("Myapp", "Error parsing URL: '$urlString'", e)
                Toast.makeText(this, "Link parsing error: ${e.message}", Toast.LENGTH_LONG).show()
                showMainScreen()
            }
        }
    
        private fun processDeepLink(uri: Uri) {
            Log.d("Myapp", "Processing deep link: $uri")
            Toast.makeText(this, "Deep link: $uri", Toast.LENGTH_LONG).show()
    
            // Add your deep link processing logic here
            // For example, navigation to a specific screen
        }
    
        private fun showMainScreen() {
            Log.d("Myapp", "Show main screen")
        }
    
        private val notificationPermissionLauncher = registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { isGranted: Boolean ->
            if (isGranted) {
                Toast.makeText(this, "Notifications permission granted", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(
                    this,
                    "FCM can't post notifications without POST_NOTIFICATIONS permission",
                    Toast.LENGTH_LONG
                ).show()
            }
        }
    
        private fun askNotificationPermission() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                if (ContextCompat.checkSelfPermission(
                        this,
                        android.Manifest.permission.POST_NOTIFICATIONS
                    ) != PackageManager.PERMISSION_GRANTED
                ) {
                    notificationPermissionLauncher.launch(
                        android.Manifest.permission.POST_NOTIFICATIONS
                    )
                }
            }
        }
    
        private fun logout() {
            lifecycleScope.launch {
                try {
                    SdkPushExpress.deactivate()
                    Toast.makeText(this@MainActivity, "Logout initiated", Toast.LENGTH_SHORT).show()
                    Log.d("Myapp", "Deactivate called - logout initiated")
                } catch (e: Exception) {
                    Log.e("Myapp", "Logout failed", e)
                }
            }
        }
        private fun login() {
            lifecycleScope.launch {
                try {
                    SdkPushExpress.deactivate()
                    val timestamp = System.currentTimeMillis()
                    SdkPushExpress.setExternalId("ext_id_$timestamp")
                    Log.d("Myapp", "App External ID: ${SdkPushExpress.getExternalId()}")
                    SdkPushExpress.activate()
                    Toast.makeText(this@MainActivity, "Login initiated", Toast.LENGTH_SHORT).show()
                    Log.d("Myapp", "Activate called - login initiated")
                } catch (e: Exception) {
                    Log.e("Myapp", "Login failed", e)
                }
            }
        }
    }
    
    @Composable
    fun Greeting(
        name: String,
        modifier: Modifier = Modifier,
        onLogoutClick: () -> Unit = {},
        onLoginClick: () -> Unit = {}
    ) {
        Column(modifier = modifier.padding(16.dp)) {
            Text(
                text = "Hello $name!",
                modifier = modifier
            )
            Button(
                onClick = onLogoutClick,
                modifier = Modifier.padding(top = 16.dp)
            ) {
                Text("Logout")
            }
            Button(
                onClick = onLoginClick,
                modifier = Modifier.padding(top = 16.dp)
            ) {
                Text("Login")
            }
        }
    }
    
    @Preview(showBackground = true)
    @Composable
    fun GreetingPreview() {
        KotlinBug3Theme {
            Greeting("Android")
        }
    }

Last updated