Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,6 @@ gradle-app.setting

# vscode
.vscode/settings.json

# Firebase configuration (contains sensitive project info)
**/google-services.json
1 change: 1 addition & 0 deletions posthog-android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Next
- Allow collecting FCM device token in SDK ([#376](https://github.com/PostHog/posthog-android/pull/376))

## 3.29.0 - 2026-01-19

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ public class PostHogAndroid private constructor() {
return PostHog.with(config)
}

/**
* Registers a push notification token (FCM token) with PostHog.
* The SDK will automatically rate-limit registrations to once per day unless the token has changed.
*
* Users should retrieve the FCM token using:
* - Java: `FirebaseMessaging.getInstance().getToken()`
* - Kotlin: `Firebase.messaging.token`
*
* @param token The FCM registration token
* @return true if registration was successful, false otherwise
*/
public fun registerPushToken(token: String): Boolean {
return PostHog.registerPushToken(token)
}

private fun <T : PostHogAndroidConfig> setAndroidConfig(
context: Context,
config: T,
Expand Down
39 changes: 39 additions & 0 deletions posthog-samples/posthog-android-sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# PostHog Android Sample App

This sample app demonstrates how to integrate the PostHog Android SDK, including push notification support via Firebase Cloud Messaging (FCM).

## Setup

### Firebase Configuration (Optional - for Push Notifications)

To enable push notification features in this sample app, you need to configure Firebase:

1. **Create a Firebase Project** (if you don't have one):
- Go to [Firebase Console](https://console.firebase.google.com/)
- Click "Add project" and follow the setup wizard

2. **Add Android App to Firebase**:
- In your Firebase project, click "Add app" and select Android
- Enter the package name: `com.posthog.android.sample`
- Register the app

3. **Download `google-services.json`**:
- Download the `google-services.json` file from Firebase Console
- Place it in the `posthog-samples/posthog-android-sample/` directory (same level as `build.gradle.kts`)

4. **Verify the file location**:
```
posthog-samples/posthog-android-sample/
├── build.gradle.kts
├── google-services.json ← Place it here
└── src/
```

**Note:** The `google-services.json` file is gitignored as it contains project-specific configuration. Each developer needs to add their own file.

### Running Without Firebase

If you don't have a `google-services.json` file, the app will still run but push notification features will be disabled:
- FCM token registration will be skipped
- Push notifications will not be received
- All other PostHog SDK features will work normally
21 changes: 21 additions & 0 deletions posthog-samples/posthog-android-sample/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.google.gms:google-services:4.4.0")
}
}

plugins {
id("com.android.application")
kotlin("android")
// TODOdin: Add to gitignore and document that it's needed?
id("com.google.gms.google-services")
// so we don't upload mappings from our release builds on CI
if (!PosthogBuildConfig.isCI()) {
id("com.posthog.android")
Expand Down Expand Up @@ -86,6 +98,15 @@ dependencies {
implementation("androidx.compose.material3:material3")
implementation(platform("com.squareup.okhttp3:okhttp-bom:${PosthogBuildConfig.Dependencies.OKHTTP}"))
implementation("com.squareup.okhttp3:okhttp")

// Firebase dependencies for push notifications
implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
implementation("com.google.firebase:firebase-messaging")
// Firebase Analytics (optional - suppresses the analytics library warning)
// Uncomment if you want to use Firebase Analytics
// implementation("com.google.firebase:firebase-analytics")
implementation("com.google.android.gms:play-services-tasks:18.0.2")

debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

<!-- optional permission-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Notification permission for Android 13+ -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:allowBackup="true"
Expand All @@ -14,8 +16,16 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PostHogAndroidSample"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="34"
android:name=".MyApp">
<service
android:name=".PostHogFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<activity
android:name="com.posthog.android.sample.NormalActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,49 @@
package com.posthog.android.sample

import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.os.StrictMode
import android.util.Log
import androidx.core.content.ContextCompat
import com.google.android.gms.tasks.OnCompleteListener
import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging
import com.posthog.PostHogOnFeatureFlags
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
import com.posthog.android.sample.R

class MyApp : Application() {
private var isFirebaseAvailable = false

override fun onCreate() {
super.onCreate()

enableStrictMode()

// Initialize Firebase if google-services.json is present
// If not available, Firebase features will be disabled gracefully
isFirebaseAvailable = initializeFirebase()

// Ensure notification channel is created early (before any notifications arrive)
// Only create if Firebase is available
if (isFirebaseAvailable) {
createNotificationChannelEarly()
}

// Demo:
// val apiKey = "_6SG-F7I1vCuZ-HdJL3VZQqjBlaSb1_20hDPwqMNnGI"
// ManoelTesting:
val apiKey = "phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D"
// PaulKey
// val apiKey = "phc_GavhjwMwc75N4HsaLjMTEvH8Kpsz70rZ3N0E9ho89YJ"
// val config = PostHogAndroidConfig(apiKey, host = "https://3727-86-27-112-156.ngrok-free.app").apply {
val config =
PostHogAndroidConfig(apiKey).apply {
// Use a default API key for the sample app
// In production, you should use your actual PostHog API key
val apiKey = "phc_crqS8UmHy8fAhZRjNTqPr5HGsAnZEsEGsSRs8S9vKy4" // Default for local testing

// For local development, you can override the host
// Note: For Android emulator, use "http://10.0.2.2:8000" instead of "http://localhost:8000"
// For physical device, use your development machine's IP address (e.g., "http://192.168.1.100:8000")
val host = "http://10.0.2.2:8000" // Optional: override host for local testing

val config = PostHogAndroidConfig(apiKey, host = host).apply {
debug = true
flushAt = 1
captureDeepLinks = false
Expand All @@ -45,6 +68,102 @@ class MyApp : Application() {
errorTrackingConfig.autoCapture = true
}
PostHogAndroid.setup(this, config)

// Register FCM token with PostHog (only if Firebase is available)
// Note: onNewToken() in PostHogFirebaseMessagingService will be called automatically
// by Firebase when tokens are refreshed, but we need to manually fetch on first install
if (isFirebaseAvailable) {
Log.d(TAG, "FCM initializing token registration")
registerFCMToken()
} else {
Log.i(TAG, "FCM token registration skipped - Firebase not available")
}
}

/**
* Initialize Firebase if google-services.json is present.
* @return true if Firebase was successfully initialized, false otherwise
*/
private fun initializeFirebase(): Boolean {
return try {
// Check if Firebase is already initialized
val apps = FirebaseApp.getApps(this)
if (apps.isEmpty()) {
Log.d(TAG, "FCM Firebase not initialized, initializing now")
FirebaseApp.initializeApp(this)
Log.d(TAG, "FCM Firebase initialized successfully")
true
} else {
Log.d(TAG, "FCM Firebase already initialized (${apps.size} app(s))")
true
}
} catch (e: Exception) {
// google-services.json is likely missing - Firebase features will be disabled
Log.w(TAG, "FCM Firebase not available (google-services.json may be missing): ${e.message}")
Log.i(TAG, "FCM Push notification features will be disabled. Add google-services.json to enable them.")
false
}
}

private fun registerFCMToken() {
try {
Log.d(TAG, "FCM requesting token from FirebaseMessaging")
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w(TAG, "FCM fetching registration token failed", task.exception)
task.exception?.let {
Log.w(TAG, "FCM error details: ${it.message}", it)
}
return@OnCompleteListener
}

// Get new FCM registration token
val token = task.result
if (token.isNullOrBlank()) {
Log.w(TAG, "FCM received null or blank token")
return@OnCompleteListener
}

Log.d(TAG, "FCM registration token retrieved: $token")
Log.d(TAG, "FCM registering token with PostHog")

// Register token with PostHog
val success = PostHogAndroid.registerPushToken(token)
if (success) {
Log.d(TAG, "FCM token successfully registered with PostHog")
} else {
Log.e(TAG, "FCM failed to register token with PostHog")
}
})
} catch (e: Exception) {
// Firebase might not be initialized or available
Log.w(TAG, "FCM Firebase Messaging not available: ${e.message}", e)
}
}

/**
* Create notification channel early in Application onCreate
* This ensures the channel exists before any notifications arrive
*/
private fun createNotificationChannelEarly() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = getString(R.string.default_notification_channel_id)
val channelName = getString(R.string.default_notification_channel_name)
val channelDescription = getString(R.string.default_notification_channel_description)
val importance = NotificationManager.IMPORTANCE_HIGH

val channel = NotificationChannel(channelId, channelName, importance).apply {
description = channelDescription
enableLights(true)
enableVibration(true)
setShowBadge(true)
lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC
}

val notificationManager = ContextCompat.getSystemService(this, NotificationManager::class.java)
notificationManager?.createNotificationChannel(channel)
Log.d(TAG, "FCM Notification channel created early in Application onCreate: $channelId")
}
}

private fun enableStrictMode() {
Expand All @@ -56,4 +175,8 @@ class MyApp : Application() {
StrictMode.setVmPolicy(vmPolicyBuilder.build())
}
}

companion object {
private const val TAG = "MyApp"
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.posthog.android.sample

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.posthog.PostHog
import com.posthog.PostHogOkHttpInterceptor
import okhttp3.OkHttpClient
Expand All @@ -15,11 +21,24 @@ class NormalActivity : ComponentActivity() {
.addInterceptor(PostHogOkHttpInterceptor(captureNetworkTelemetry = true))
.build()

private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
Log.d(TAG, "FCM POST_NOTIFICATIONS permission granted")
} else {
Log.w(TAG, "FCM POST_NOTIFICATIONS permission denied - notifications will not be displayed")
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(R.layout.normal_activity)

// Request notification permission for Android 13+ (API 33+)
requestNotificationPermission()

// val webview = findViewById<WebView>(R.id.webview)
// webview.loadUrl("https://www.google.com")

Expand Down Expand Up @@ -75,4 +94,27 @@ class NormalActivity : ComponentActivity() {
}
}
}

private fun requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
when {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED -> {
Log.d(TAG, "FCM POST_NOTIFICATIONS permission already granted")
}
else -> {
Log.d(TAG, "FCM Requesting POST_NOTIFICATIONS permission")
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
} else {
Log.d(TAG, "FCM POST_NOTIFICATIONS permission not required (Android < 13)")
}
}

companion object {
private const val TAG = "NormalActivity"
}
}
Loading
Loading