Skip to content

Android SDK

Native Android SDK for screen recording and feedback collection.

Requirements

  • Android 7.0 (API 24) or higher
  • Kotlin 1.9+ or Java 11+
  • Android Studio Hedgehog (2023.1.1) or later

Installation

Gradle (Kotlin DSL)

Add the dependency to your app's build.gradle.kts:

kotlin
dependencies {
    implementation("io.horus:horus-sdk:1.0.0")
}

Gradle (Groovy)

groovy
dependencies {
    implementation 'io.horus:horus-sdk:1.0.0'
}

Local Development

To build and use the SDK locally:

bash
cd android
./gradlew :horus-sdk:publishToMavenLocal

Then add mavenLocal() to your repositories:

kotlin
repositories {
    mavenLocal()
    // ... other repositories
}

Permissions

The SDK requires these permissions (automatically merged from the library manifest):

xml
<!-- Required for screen recording (Android 10+) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />

<!-- Required for audio recording -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<!-- Required for network status detection -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- Required for uploading recordings -->
<uses-permission android:name="android.permission.INTERNET" />

Runtime Permissions

You must request RECORD_AUDIO permission at runtime before starting a recording with audio:

kotlin
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
    != PackageManager.PERMISSION_GRANTED) {

    ActivityCompat.requestPermissions(
        this,
        arrayOf(Manifest.permission.RECORD_AUDIO),
        AUDIO_PERMISSION_REQUEST_CODE
    )
}

Quick Start

1. Initialize the SDK

Initialize in your Application class:

kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        HorusSDK.init(
            context = this,
            config = HorusConfig(
                embedToken = "proj_your_token_here",
                userInfo = UserInfo(
                    email = "user@example.com",
                    name = "John Doe",
                    userId = "user_123"
                ),
                customMetadata = mapOf(
                    "plan" to "premium",
                    "company" to "Acme Inc"
                )
            )
        )
    }
}

2. Request Screen Capture Permission

From your Activity:

kotlin
class MainActivity : AppCompatActivity() {

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

        // Setup callbacks
        HorusSDK.setOnRecordingStarted {
            runOnUiThread { showRecordingUI() }
        }

        HorusSDK.setOnRecordingStopped { result ->
            runOnUiThread { showUploadingUI() }
        }

        HorusSDK.setOnUploadProgress { progress ->
            runOnUiThread { updateProgress(progress) }
        }

        HorusSDK.setOnUploadComplete { result ->
            runOnUiThread {
                Toast.makeText(this, "Recording uploaded!", Toast.LENGTH_SHORT).show()
            }
        }

        HorusSDK.setOnError { error ->
            runOnUiThread {
                Toast.makeText(this, "Error: ${error.message}", Toast.LENGTH_LONG).show()
            }
        }
    }

    fun startRecording() {
        // This shows a system dialog asking the user for permission
        HorusSDK.requestScreenCapture(
            activity = this,
            includeAudio = true
        )
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        // Let the SDK handle the permission result
        if (HorusSDK.onActivityResult(requestCode, resultCode, data)) {
            // Recording started successfully
            return
        }
    }

    fun stopRecording() {
        HorusSDK.stopRecording(
            description = "Bug in checkout flow",
            metadata = mapOf(
                "screen" to "checkout",
                "action" to "payment_failed"
            )
        )
    }
}

API Reference

HorusSDK

The main SDK singleton.

Initialization

kotlin
HorusSDK.init(context: Context, config: HorusConfig)

Initialize the SDK. Must be called before any other methods.

Recording Control

kotlin
// Request screen capture permission (shows system dialog)
HorusSDK.requestScreenCapture(activity: Activity, includeAudio: Boolean = true)

// Handle activity result (call from onActivityResult)
HorusSDK.onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean

// Stop recording and upload
HorusSDK.stopRecording(
    description: String? = null,
    metadata: Map<String, Any>? = null
)

// Cancel recording without uploading
HorusSDK.cancelRecording()

Properties

kotlin
HorusSDK.isInitialized(): Boolean  // Check if SDK is initialized
HorusSDK.isRecording(): Boolean    // Check if recording is in progress
HorusSDK.getRecordingDuration(): Double  // Get current duration in seconds

Callbacks

kotlin
HorusSDK.setOnRecordingStarted { /* Recording started */ }
HorusSDK.setOnRecordingStopped { result: RecordingResult -> /* Before upload */ }
HorusSDK.setOnUploadProgress { progress: Float -> /* 0.0 to 1.0 */ }
HorusSDK.setOnUploadComplete { result: UploadResult -> /* Upload done */ }
HorusSDK.setOnError { error: HorusException -> /* Handle error */ }

User Management

kotlin
// Update user info after initialization
HorusSDK.setUserInfo(UserInfo(email = "new@example.com"))

// Reset the SDK
HorusSDK.reset()

HorusConfig

Configuration options for the SDK.

PropertyTypeDefaultDescription
embedTokenString?nullHorus project embed token (starts with proj_)
recordingLinkIdString?nullLegacy: Direct recording link token
apiUrlStringhttps://tryhorus.ioHorus API URL
userInfoUserInfo?nullUser identification
customMetadataMap<String, Any>?nullCustom key-value pairs
captureAudioBooleantrueWhether to capture microphone audio
videoWidthInt720Video width in pixels
videoHeightInt1280Video height in pixels
videoBitrateInt2,000,000Video bitrate in bps
videoFpsInt30Video frames per second

UserInfo

kotlin
data class UserInfo(
    val email: String? = null,
    val name: String? = null,
    val userId: String? = null
)

RecordingResult

Returned after recording stops (before upload).

kotlin
data class RecordingResult(
    val videoFile: File,
    val audioFile: File?,
    val duration: Double,  // seconds
    val description: String?
)

UploadResult

Returned after successful upload.

kotlin
data class UploadResult(
    val recordingId: String,
    val streamVideoId: String?,
    val success: Boolean,
    val message: String?
)

HorusException

Error types thrown by the SDK.

kotlin
sealed class HorusException(message: String) : Exception(message) {
    class NotInitialized(message: String) : HorusException(message)
    class PermissionDenied(message: String) : HorusException(message)
    class RecordingFailed(message: String) : HorusException(message)
    class UploadFailed(message: String) : HorusException(message)
    class InvalidConfig(message: String) : HorusException(message)
    class NetworkError(message: String) : HorusException(message)
}

Recording UI Example

Show a recording indicator while recording:

kotlin
class RecordingOverlayView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

    private val durationText: TextView
    private var startTime: Long = 0
    private val handler = Handler(Looper.getMainLooper())

    init {
        inflate(context, R.layout.recording_overlay, this)
        durationText = findViewById(R.id.duration_text)
    }

    fun startTimer() {
        startTime = System.currentTimeMillis()
        updateDuration()
    }

    private fun updateDuration() {
        val elapsed = (System.currentTimeMillis() - startTime) / 1000
        val minutes = elapsed / 60
        val seconds = elapsed % 60
        durationText.text = String.format("%02d:%02d", minutes, seconds)

        handler.postDelayed({ updateDuration() }, 1000)
    }

    fun stopTimer() {
        handler.removeCallbacksAndMessages(null)
    }
}

ProGuard / R8 Configuration

If you're using ProGuard or R8, add these rules:

proguard
-keep class io.horus.sdk.** { *; }
-dontwarn io.horus.sdk.**

Troubleshooting

Screen appears black in recording

  1. Make sure you're not recording DRM-protected content
  2. Check that the MediaProjection permission was granted
  3. Verify the device supports screen capture

Audio not being recorded

  1. Request RECORD_AUDIO permission at runtime first
  2. Ensure captureAudio = true in config
  3. Check that no other app is using the microphone

Upload fails with network error

  1. Verify internet connectivity
  2. Check that the embedToken is valid
  3. Ensure the Horus API URL is reachable

Recording stops immediately

  1. Check for errors in the onError callback
  2. Verify the app has sufficient storage space
  3. Ensure the foreground service is running

Released under the MIT License.