Examples - solum-signage-system/solum-signage-example GitHub Wiki

Purpose

This section provides specific scenarios and code hints for using SoluM's Device API through the SoluM Signage Feature Examples Android application. These examples illustrate how to achieve common device operations typically needed for digital signage devices.


1. Display Attribute Change

Goal

To programmatically control visual display attributes such as brightness, contrast, and saturation of the SoluM Signage device's screen.

Scenario

When deploying the signage device or optimizing content visibility, you might need to adjust visual properties to match ambient lighting conditions, content type, or specific brand guidelines (e.g., setting optimal brightness for daylight, adjusting contrast for clarity, enhancing color vibrancy with saturation).

Understanding Display Operations and Multi-Display Control

Each device possesses a set of unique DisplayOperation objects, each corresponding to a controllable display attribute. These operations define what can be adjusted on which display(s).

Due to hardware and software considerations, the specific display attributes supported by a device, and the individual displays to which each attribute setting can be applied, may differ. Therefore, before attempting to change any display attribute, it is crucial to first determine whether the device actually supports that specific display attribute change, and identify which DisplayOperation is linked to the display(s) you wish to modify.

For example, let's consider a device that might support the following scenarios:

  • A DisplayOperation with DisplayAttribute.BRIGHTNESS associated with DisplayId {0, 1}.
  • A separate DisplayOperation with DisplayAttribute.CONTRAST associated with DisplayId {0}.

Consequently, this configuration leads to the following control behavior:

  • When you adjust the brightness using the DisplayOperation for DisplayAttribute.BRIGHTNESS (associated with DisplayId {0, 1}), it will control both Display 0 and Display 1 concurrently.
  • However, when you adjust the contrast using its DisplayOperation (associated with DisplayId {0}), it will apply only to Display 0.

Code Hint

1) Search for Supported DisplayAttributes on the Device

First, retrieve a list of display attributes that the current SoluM device actually supports. This is crucial as supported attributes can vary by device model or firmware.

import com.solum.display.manager.Display
import com.solum.display.manager.Display.DisplayOperation

val listOfSupportedDisplayOperations: List<DisplayOperation> = Display.getSupportedDisplayOperations(context)

2) Determine the Appropriate DisplayAttribute

Once you have the list, you need to identify the specific attribute you want to control (e.g., "Brightness", "Contrast", "Saturation").

val defaultScreenBrightnessOperation = listOfSupportedDisplayOperations.first{ displayOperation ->
    displayOperation.attribute == DisplayAttribute.BRIGHTNESS && displayOperation.displayId.contains(android.view.Display.DEFAULT_DISPLAY)
}

3) Understand the Current State of the DisplayAttribute

Before changing an attribute, it's often useful to know its current value.

val maximum = defaultScreenBrightnessOperation.maximum
val minimum = defaultScreenBrightnessOperation.minimum
val value = defaultScreenBrightnessOperation.value

4) Control the DisplayAttribute

Finally, set the desired value for the chosen attribute. Always check if the value is within the supported range (min/max values obtained in step 1).

defaultScreenBrightnessOperation.value = minimum + ((maximum - minimum) * 0.6).toInt()

How to Find in Project

You might find relevant code in a file like ChangeDisplayAttributeExample.kt


2. Find Audio Output Devices

Goal

To programmatically list and identify all available audio output devices connected to the SoluM Signage device, including general outputs and those specific to certain audio stream types (e.g., music).

Scenario

A digital signage application might need to determine what audio hardware is connected (e.g., HDMI, built-in speaker, Bluetooth) to verify capabilities, route audio content appropriately, or provide diagnostic information. This example demonstrates how to discover these outputs.

Code Hint

This example utilizes Android's AudioManager to retrieve information about connected audio output devices. It covers both a general discovery method and an API-level-specific method for querying devices capable of handling particular audio streams.

1) Get General Audio Output Devices

Use AudioManager.getDevices with the AudioManager.GET_DEVICES_OUTPUTS flag to obtain a list of all currently recognized audio output devices. This provides a broad overview of available hardware.

import android.content.Context
import android.media.AudioManager
import android.media.AudioDeviceInfo

// Assuming 'context' is available (e.g., from an Activity or Application)
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager

// Retrieve all connected audio output devices
val outputDevices: Array<AudioDeviceInfo> = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)

Log.d("AudioOutputDiscovery", "General Audio Output Devices Found:")
if (outputDevices.isNotEmpty()) {
    for (device in outputDevices) {
        // Log basic information: device type (converted to readable string) and product name
        // 'toAudioOutputTypeString()' is an extension function defined in the example file for readability.
        Log.d("AudioOutputDiscovery", "  Type: ${device.type.toAudioOutputTypeString()}, Name: ${device.productName}, ID: ${device.id}")
    }
} else {
    Log.d("AudioOutputDiscovery", "  No general audio output devices found.")
}

2) Get Audio Output Devices for a Specific Stream Type (API 33+)

For Android Tiramisu (API Level 33) and above, you can refine your search by querying for devices specifically capable of handling particular AudioAttributes (e.g., for playing music, alarms, or notifications). This is done using AudioManager.getAudioDevicesForAttributes.

import android.media.AudioAttributes
import android.os.Build
import androidx.annotation.RequiresApi

// This helper function (as seen in the example file) maps stream types to AudioAttributes
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun getOutputDeviceForStreamType(context: Context, streamType: Int = AudioManager.STREAM_MUSIC): List<AudioDeviceInfo> {
    val audioManager = context.getSystemService(AudioManager::class.java)

    val usage = when (streamType) {
        AudioManager.STREAM_MUSIC -> AudioAttributes.USAGE_MEDIA
        // ... other stream type mappings like ALARM, RING, VOICE_CALL
        else -> AudioAttributes.USAGE_UNKNOWN
    }
    val contentType = when (streamType) {
        AudioManager.STREAM_MUSIC -> AudioAttributes.CONTENT_TYPE_MUSIC
        // ... other content type mappings
        else -> AudioAttributes.CONTENT_TYPE_UNKNOWN
    }

    val audioAttributes = AudioAttributes.Builder()
        .setUsage(usage)
        .setContentType(contentType)
        .build()

    // Retrieve devices matching the specified audio attributes
    return audioManager.getAudioDevicesForAttributes(audioAttributes).toList()
}

// How this might be called in your application's logic:
// (Ensure this call is wrapped in an API level check if not using @RequiresApi at the call site)
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
     val mediaOutputDevices = getOutputDeviceForStreamType(context, AudioManager.STREAM_MUSIC)
     Log.d("AudioOutputDiscovery", "Media-specific Audio Output Devices (API 33+):")
     if (mediaOutputDevices.isNotEmpty()) {
         mediaOutputDevices.forEach { device ->
             Log.d("AudioOutputDiscovery", "  Type: ${device.type.toAudioOutputTypeString()}, Name: ${device.productName}")
         }
     } else {
         Log.d("AudioOutputDiscovery", "  No specific audio output devices found for media.")
     }
 }

How to Find in Project

You might find relevant code in a file like FindAudioOutputDeviceExample.kt


3. Controlling Device Volume

Goal

To programmatically control the audio volume of the SoluM Signage device for a specific audio stream (e.g., music), and understand how to manage associated system UI feedback.

Scenario

A digital signage application needs to dynamically adjust audio output levels for background music, video content, or announcements. This adjustment might be based on ambient noise, time of day, or remote management commands. Sometimes it's desirable to show the standard Android volume UI, while other times it should be suppressed.

Code Hint

This example demonstrates fetching current volume levels, setting new levels, and responding to user input (like keyboard arrows or a slider) to modify the volume of the STREAM_MUSIC audio stream.

1) Get AudioManager and Current Volume State

First, obtain an instance of AudioManager and retrieve the current, maximum, and minimum volume levels for the desired stream type (e.g., AudioManager.STREAM_MUSIC).

import android.content.Context
import android.media.AudioManager
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext

@Composable // Context for Compose example
fun VolumeControlSetup(initialContext: Context) {
    val context = LocalContext.current // For Compose environment
    val audioManager = remember { context.getSystemService(AudioManager::class.java) }

    // State for the current volume level, initialized with the current music stream volume
    var volumeLevel by remember { mutableIntStateOf(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)) }
    // State for the maximum volume level for the music stream
    val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) }
    // State for the minimum volume level for the music stream (usually 0)
    val minVolume = remember { audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC) }
}

2) Set Device Volume for a Stream

To change the volume, use AudioManager.setStreamVolume(). This method takes the stream type, the new volume level, and a flags parameter to control UI feedback.

import android.media.AudioManager

/**
 * Sets the volume for a specific audio stream.
 * @param audioManager The AudioManager instance.
 * @param volumeLevel The desired volume level (must be within min/max range).
 */
private fun setVolume(audioManager: AudioManager, volumeLevel: Int) {
    var flags = 0 // Initialize flags to 0 (no special behavior)

    // Example: condition to show the system volume UI, default in this example is true
    val showSystemUi = true
    if (showSystemUi) {
        flags = flags or AudioManager.FLAG_SHOW_UI // Add flag to show the system volume UI
    }

    // Set the volume for the music stream
    audioManager.setStreamVolume(
        AudioManager.STREAM_MUSIC, // Stream type (e.g., music, ring, alarm, etc.)
        volumeLevel,               // Volume level to set
        flags                      // Additional flags to control UI/sound feedback
    )
}

// How it might be called in your app's logic (e.g., from a Slider's onValueChange or a button click):
// val newVolume = (currentVolume + 1).coerceIn(minVolume..maxVolume) // Example of incrementing and clamping
// setVolume(audioManager, newVolume)

3) Understanding flags in setStreamVolume

The flags parameter in AudioManager.setStreamVolume() allows for fine-grained control over the user experience when the volume changes. You can combine multiple flags using the bitwise OR (|) operator.

  • AudioManager.FLAG_SHOW_UI (Default in example: true)

    • Purpose: Displays the system's standard volume UI (e.g., a volume slider overlay) when the volume is changed.
    • Use Case: Ideal when users need visual feedback of volume adjustment. Set to 0 or omit if you want to silently adjust volume (e.g., for background processes or specific signage scenarios where UI should not pop up).
  • AudioManager.FLAG_PLAY_SOUND

    • Purpose: Plays a sound effect (e.g., a "tick" sound) as the volume is adjusted.
    • Use Case: Provides auditory feedback.
  • AudioManager.FLAG_BLUETOOTH_ABS_VOLUME

    • Purpose: Used to set volume for Bluetooth A2DP devices using absolute volume control.

By setting flags = 0 (or omitting all flags), you can perform silent volume adjustments without any visual or auditory feedback to the user.

How to Find in Project

The full implementation for this example can be found in the project at: ChangeVolumeExample

For comprehensive documentation on AudioManager and its methods like setStreamVolume, getStreamVolume, and getStreamMaxVolume, refer to the official Android Developers documentation for the AudioManager class.


4. Controlling Audio Mute Status

Goal

To programmatically toggle the mute state (mute/unmute) of a specific audio stream (e.g., music) on the SoluM Signage device.

Scenario

A digital signage application might require a quick way to silence content audio for announcements, during scheduled maintenance, or in response to remote commands. This example demonstrates how to switch between muted and unmuted states.

Code Hint

This example utilizes AudioManager to check and change the mute status of the AudioManager.STREAM_MUSIC stream, often displaying the current state and providing a toggle button.

1) Get AudioManager and Current Mute State

First, obtain an instance of AudioManager and initialize a state variable with the current mute status of the desired audio stream.

import android.content.Context
import android.media.AudioManager
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext

@Composable
fun MuteExampleComposable(context: Context) { // Assuming context is passed or LocalContext is used
    val audioManager = remember { context.getSystemService(AudioManager::class.java) }

    // State to hold the current mute state for STREAM_MUSIC
    var isMuted by remember { mutableStateOf(isMuted(audioManager, AudioManager.STREAM_MUSIC)) }

    // UI elements like Text(if (isMuted) "Media Muted" else "Media Unmuted")
    // And a Button(onClick = { setMuteStatus(audioManager, !isMuted); isMuted = !isMuted })
}

// Helper function to check if a stream is muted
private fun isMuted(audioManager: AudioManager, streamType: Int = AudioManager.STREAM_MUSIC): Boolean =
    audioManager.isStreamMute(streamType)

2) Set Audio Mute Status

To toggle the mute state, use AudioManager.adjustStreamVolume(). This method allows you to specifically mute or unmute a stream.

import android.media.AudioManager

/**
 * Sets the mute status for a specific audio stream.
 * @param audioManager The AudioManager instance.
 * @param mute True to mute the stream, false to unmute.
 * @param streamType The audio stream type (defaults to STREAM_MUSIC).
 */
private fun setMuteStatus(audioManager: AudioManager, mute: Boolean, streamType: Int = AudioManager.STREAM_MUSIC) {
    var flags = 0 // Initialize flags. See 'Controlling Device Volume' example for detailed flag explanation.

    // Typically, you'd show UI feedback for user-initiated mute/unmute
    val showSystemUi = true
    if (showSystemUi) {
        flags = flags or AudioManager.FLAG_SHOW_UI
    }

    // Use ADJUST_MUTE to mute or ADJUST_UNMUTE to unmute the stream
    audioManager.adjustStreamVolume(
        streamType,
        if (mute) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE,
        flags
    )
}

// How it might be called (e.g., from a toggle button):
// setMuteStatus(audioManager, true) // Mute the media stream
// setMuteStatus(audioManager, false) // Unmute the media stream

How to Find in Project

The full implementation for this example can be found in the project at: MuteExample.kt

Search for AudioManager.isStreamMute and AudioManager.adjustStreamVolume (with ADJUST_MUTE/ADJUST_UNMUTE) to see how mute functionality is managed.


5. Dual Screen Management

Goal

To demonstrate how to detect multiple physical displays connected to the SoluM Signage device and render distinct Jetpack Compose UI content on a secondary display using Android's Presentation API.

Scenario

SoluM has various digital signage systems with multiple screens. This example illustrates the fundamental approach to managing and rendering unique content across such a multi-display environment.

Understanding Android Presentation

The android.app.Presentation class is a specialized type of Dialog that allows an application to present unique content on a non-primary physical display. It's crucial for multi-screen applications, particularly in contexts like digital signage, where different information needs to be shown on separate screens simultaneously.

Key aspects of Presentation:

  • Secondary Display Target: It's designed to specifically render UI on a secondary display (e.g., HDMI-connected monitor, wireless display).
  • Separate Context and Theme: A Presentation instance has its own Display context, meaning it will correctly inherit the theme and resources appropriate for that specific display.
  • Lifecycle Management: Although it's a Dialog, its lifecycle often needs to be carefully managed in sync with the primary Activity or Fragment that creates it, especially when hosting dynamic content like Jetpack Compose.
  • Hosting Composables: By using ComposeView within a Presentation, you can leverage the power of Jetpack Compose to build flexible UIs for secondary screens.

Code Hint

This example uses Android's DisplayManager to find available displays and then utilizes the Presentation class (a specialized Dialog) to show content on a non-primary display. It also demonstrates how to integrate Jetpack Compose into this Presentation by managing its lifecycle.

1) Detect Displays and Identify Primary/Secondary

First, get the DisplayManager system service to enumerate all connected physical displays. Then, identify the primary display and the secondary display(s).

import android.content.Context
import android.hardware.display.DisplayManager
import android.view.Display // android.view.Display

// Assuming 'context' is available
val displayManager = context.getSystemService(Context.class.java) as DisplayManager

// Get all displays connected to the device
val allDisplays: Array<Display> = displayManager.displays

// Helper functions (as seen in the example file) to find specific displays
private fun getPrimaryDisplay(displays: Array<Display>): Display =
    displays.first { it.displayId == Display.DEFAULT_DISPLAY }

private fun getSecondaryDisplay(displays: Array<Display>): Display =
    displays.first { it.displayId != Display.DEFAULT_DISPLAY } // Finds the first non-default display

// Usage:
val primaryDisplay = getPrimaryDisplay(allDisplays)
Log.d("DualScreenExample", "Primary Display ID: ${primaryDisplay.displayId}")
if (allDisplays.size > 1) {
    val secondaryDisplay = getSecondaryDisplay(allDisplays)
    Log.d("DualScreenExample", "Secondary Display ID: ${secondaryDisplay.displayId}")
} else {
    Log.d("DualScreenExample", "Only one display found.")
}

2) Create and Show a Presentation on the Secondary Display

The [Presentation class](https://developer.android.com/reference/android/app/Presentation) is key for displaying content on secondary screens. You extend it to define your custom content.

import android.app.Presentation
import android.os.Bundle
import android.view.Display
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.lifecycle.setViewTreeSavedStateRegistryOwner
import androidx.savedstate.SavedStateRegistryOwner
import androidx.lifecycle.ViewModelStoreOwner
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.runtime.Composable

// Custom Presentation subclass to host Compose content
class MyPresentation(
    private val lifecycleOwner: LifecycleOwner,
    activityContext: Context,
    display: Display
) : Presentation(activityContext, display) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val savedStateRegistryOwner = lifecycleOwner as SavedStateRegistryOwner

        val composeView = ComposeView(context).apply { // 'context' here is Presentation's context
            // Important: Set lifecycle, ViewModel, and SavedState owners for Compose compatibility
            setViewTreeLifecycleOwner(lifecycleOwner)
            setViewTreeViewModelStoreOwner(lifecycleOwner as? ViewModelStoreOwner)
            setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)

            setContent {
                // Your app's theme and Composable content for the secondary screen
                // Example: SolumSignageExampleTheme { PresentationContent() }
            }
        }
        setContentView(composeView) // Set the ComposeView as the content of the Presentation
    }

    // @Composable private fun PresentationContent() { /* UI for secondary screen */ }
}

// How to create and show:
val secondaryDisplay = getSecondaryDisplay(allDisplays) // From step 1
val presentation = MyPresentation(lifecycleOwner, context, secondaryDisplay)
presentation.show() // Display the content on the secondary screen

3) Manage Presentation Lifecycle

The Presentation's visibility should typically align with the primary screen's lifecycle to prevent leaks or unexpected behavior. Use DisposableEffect and LifecycleEventObserver in Compose.

import androidx.compose.runtime.DisposableEffect
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner

// Inside a Composable that manages the Presentation (e.g., DualScreenExample composable)
val lifecycleOwner = LocalLifecycleOwner.current
var presentationForSecondaryDisplay: MyPresentation? = null // Declared outside DisposableEffect, can be managed by remember { mutableStateOf(...) }

DisposableEffect(lifecycleOwner) {
    // Assuming 'displays.size > 1' and 'getSecondaryDisplay' are handled
    // Create the Presentation when the Composable enters composition
    presentationForSecondaryDisplay = MyPresentation(
        lifecycleOwner,
        context, // 'context' from LocalContext
        getSecondaryDisplay(displays) // assuming 'displays' is available
    ).apply {
        setOnDismissListener {
            // Handle dismissal (e.g., secondary display disconnected), navigate back
            navController.navigateUp()
        }
    }
    presentationForSecondaryDisplay?.show() // Show the presentation

    // Observe primary screen's lifecycle events (ON_START, ON_STOP)
    val observer = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_START) {
            presentationForSecondaryDisplay?.show() // Show when primary screen starts
        } else if (event == Lifecycle.Event.ON_STOP) {
            presentationForSecondaryDisplay?.dismiss() // Dismiss when primary screen stops
        }
    }
    lifecycleOwner.lifecycle.addObserver(observer)

    // Clean up when the Composable leaves composition
    onDispose {
        lifecycleOwner.lifecycle.removeObserver(observer)
        presentationForSecondaryDisplay?.dismiss() // Ensure presentation is dismissed
    }
}

How to Find in Project

The full implementation for this example can be found in the project at: DualScreenExample.kt

Also, look for the definition of the custom MyPresentation class and how DisplayManager is used to find displays.


6. Controlling Screen Rotation

Goal

To programmatically change the orientation (e.g., portrait or landscape) of the SoluM Signage device's primary display to specific angles (0, 90, 180, or 270 degrees).

Scenario

Digital signage content is often designed for a particular screen orientation (e.g., a tall advertisement for portrait mode, or a wide video for landscape mode). This example demonstrates how to enforce a specific screen rotation.

Code Hint

This example uses the Display.changeOrientation() API included in the SoluM API library to set the screen rotation. It leverages standard Android Surface.ROTATION_* constants to define the target angle.

1) Implement Rotation Logic

To change the screen's orientation, you will call the Display.changeOrientation() method. This method typically requires the Context, the Display ID of the target screen (such as android.view.Display.DEFAULT_DISPLAY for the primary display), and the desired rotation constant.

import android.content.Context
import android.view.Surface // For Surface.ROTATION_* constants
import android.view.Display as AndroidDisplaywith android.view.Display
import com.solum.display.manager.Display

// This function could be part of your Composable or a helper
fun rotateScreen(context: Context, rotationValue: Int) {
    // Call the SDK function to change the screen orientation.
    // android.view.Display.DEFAULT_DISPLAY refers to the primary device display.
    Display.changeOrientation(context, AndroidDisplay.DEFAULT_DISPLAY, rotationValue)
    Log.i("ScreenRotationControl", "Attempted to set screen rotation to: $rotationValue")
}

// How it might be used in your UI (e.g., attached to buttons):
Button(onClick = { rotateScreen(context, Surface.ROTATION_0) }) { Text("0") }
Button(onClick = { rotateScreen(context, Surface.ROTATION_90) }) { Text("90") }
Button(onClick = { rotateScreen(context, Surface.ROTATION_180) }) { Text("180") }
Button(onClick = { rotateScreen(context, Surface.ROTATION_270) }) { Text("270") }

2) Understanding Rotation Constants

The Surface.ROTATION_* constants define standard screen orientations:

  • Surface.ROTATION_0: The display's natural orientation (usually portrait for phones, landscape for tablets/signage). Corresponds to 0 degrees.
  • Surface.ROTATION_90: Rotated 90 degrees clockwise from its natural orientation.
  • Surface.ROTATION_180: Rotated 180 degrees clockwise.
  • Surface.ROTATION_270: Rotated 270 degrees clockwise (or 90 degrees counter-clockwise).

How to Find in Project

The full implementation for this example can be found in the project at: RotateScreenExample.kt


7. Changing Device Language

Goal

To programmatically change the device's system language (locale) to one of the supported languages, affecting the application's displayed text and potentially the device's entire UI.

Scenario

Digital signage devices may operate in diverse linguistic environments. This example demonstrates how to allow users or administrators to switch the device's language on-site, facilitating easier navigation, content display for various audiences, or specific regional deployments.

Code Hint

This example utilizes standard Android Locale APIs combined with a SoluM SDK method to manage and set the device's language. It involves getting the current locale, presenting a list of supported languages, and applying the selected language via the SDK.

1) Get Current Device Locale

Retrieve the current primary locale of the device's configuration. This is usually the language the device is currently operating in.

import android.content.Context
import android.os.Build
import java.util.Locale

// Assuming 'context' is available
private fun getCurrentDeviceLocale(context: Context): Locale {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // For Android N (API 24) and above, get the primary locale from the locale list.
        context.resources.configuration.locales[0]
    } else {
        // For older versions, get the deprecated single locale.
        @Suppress("DEPRECATION")
        context.resources.configuration.locale
    }
}

// How it might be used in your Composable:
var currentSelectedLocale by remember { mutableStateOf(getCurrentDeviceLocale(context)) }
Text("Current language: ${currentSelectedLocale.getDisplayName(currentSelectedLocale)}")

2) Set Device Locale Using SDK

Use the specific SoluM SDK method (e.g., com.solum.display.manager.Locale.setLocale) to apply the chosen locale to the device. Note that changing the device locale often requires an Activity or application restart to take full effect across the system and other apps.

import android.content.Context
import java.util.Locale

/**
 * Sets the device's locale using the provided SDK method.
 * This function assumes `com.solum.display.manager.Locale.setLocale` is the correct
 * way to change the application's or device's locale through your specific SDK.
 *
 * @param context The application context.
 * @param locale The Locale to set.
 */
private fun setDeviceLocaleWithSDK(context: Context, locale: Locale) {
    // Call the SDK's method to set the locale.
    // The actual behavior (app-specific vs. system-wide, restart requirement) depends on this SDK call.
    com.solum.display.manager.Locale.setLocale(context, locale.toLanguageTag())
    // After this call, you might need to restart the Activity or app for changes to apply fully.
}

// How it might be called when a language is selected in the UI:
// setDeviceLocaleWithSDK(context, selectedLocale)
// currentSelectedLocale = selectedLocale // Update UI state

How to Find in Project

The full implementation for this example can be found in the project at: ChangeLanguageExample.kt


8. Changing Device Time Zone

Goal

To programmatically change the device's system time zone and observe its effect on time display, ensuring content scheduling and logging reflect local time correctly.

Scenario

Digital signage devices are often deployed across various geographical locations, requiring accurate local timekeeping for scheduled content playback, data logging, or displaying local time information. This example demonstrates how to configure the device's time zone.

Code Hint

This example utilizes Android's TimeZone and Calendar classes, along with a SoluM SDK method, to manage and set the device's time zone. It includes displaying the current time zone, listing available time zones, and applying a selected time zone.

1) Get Current Time Zone and List Available Time Zones

First, retrieve the device's currently configured time zone. Then, get a list of all available time zone IDs that the device supports.

import android.icu.util.TimeZone
import android.content.Context
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext

@Composable
fun TimeZoneControlSetup(initialContext: Context) { // Example for usage in a Composable
    val context = LocalContext.current
    // State for the currently selected time zone ID, initialized with the device's current time zone.
    var currentSelectedTimeZoneId by remember { mutableStateOf(TimeZone.getDefault().id) }
    // List of all available time zone IDs (e.g., "Asia/Seoul", "America/New_York")
    val timeZoneIdList = remember { TimeZone.getAvailableIDs() }

    // Your UI would typically list timeZoneIdList (e.g., in a LazyColumn)
    // and display currentSelectedTimeZoneId.
}

// Helper function to get the device's current default time zone
private fun getCurrentDeviceTimeZone(): TimeZone = TimeZone.getDefault()

2) Set Device Time Zone Using SDK

To change the time zone, use the specific SoluM SDK method (e.g., com.solum.display.manager.TimeZone.setTimeZone). Selecting a time zone from the timeZoneIdList will provide the necessary ID string.

import android.content.Context

/**
 * Sets the device's or application's time zone using a specific SDK method.
 *
 * This function is a wrapper around an assumed SDK call: `com.solum.display.manager.TimeZone.setTimeZone`.
 * The actual behavior of this SDK call (e.g., whether it affects the entire system or just the app,
 * and whether a restart or specific permissions are needed) depends on the SDK's implementation.
 *
 * @param context The application context, often required by SDK methods.
 * @param timeZoneId The ID string of the time zone to set (e.g., "America/New_York", "Europe/London").
 */
private fun setDeviceTimeZoneWithSDK(context: Context, timeZoneId: String) {
    // Call the proprietary SDK method to set the time zone.
    com.solum.display.manager.TimeZone.setTimeZone(context, timeZoneId)
}

// How it might be called when a time zone is selected in the UI:
 setDeviceTimeZoneWithSDK(context, selectedTimeZoneId)

How to Find in Project

The full implementation for this example can be found in the project at: ChangeTimeZoneExample.kt


8. Simulating Keyboard Input

Goal

To programmatically simulate physical keyboard key presses (e.g., volume up/down, mute) on the SoluM Signage device, typically for controlling system-level functionalities.

Scenario

For kiosk applications, unattended signage, or automated testing, simulating hardware key presses can be a powerful tool. This example demonstrates how to inject system-wide key events programmatically, allowing for remote control or triggering specific system behaviors without physical interaction.

Code Hint

This example utilizes the Input.injectInputEvent() API to dispatch KeyEvent objects to the system. This allows the application to mimic actual key presses.

1) Understand Key Event Injection

Injecting input events requires creating a KeyEvent object for both the ACTION_DOWN (key pressed) and ACTION_UP (key released) events to simulate a full key press. SystemClock.uptimeMillis() is used to get accurate event times.

import android.content.Context
import android.os.SystemClock
import android.view.InputDevice
import android.view.KeyCharacterMap
import android.view.KeyEvent
import com.solum.display.manager.Input // Assuming this is SoluM's Input class

/**
 * Injects a single key press event (down and then up) into the system.
 * This effectively simulates a physical button press.
 * @param context The application context.
 * @param keyCode The key code to simulate (e.g., KeyEvent.KEYCODE_VOLUME_UP).
 */
private fun injectSingleKeyEvent(context: Context, keyCode: Int) {
    val now = SystemClock.uptimeMillis() // Get current uptime in milliseconds

    // Create a KeyEvent for the key press (ACTION_DOWN)
    Input.injectInputEvent(context, KeyEvent(
        now, // downTime: The time (in uptimeMillis) when this event occurred.
        now, // eventTime: The time (in uptimeMillis) when this event occurred.
        KeyEvent.ACTION_DOWN, // Action: Key is pressed down.
        keyCode, // KeyCode: The key to inject (e.g., KEYCODE_VOLUME_UP).
        0,       // repeatCount: Number of repeated key events. 0 for initial press.
        0,       // metaState: State of meta keys (e.g., SHIFT, ALT).
        KeyCharacterMap.VIRTUAL_KEYBOARD, // deviceId: ID of the virtual keyboard.
        0,       // scancode: Hardware scancode.
        0,       // flags: Additional key event flags.
        InputDevice.SOURCE_UNKNOWN // source: Source of the input device.
    ))

    // Create a KeyEvent for the key release (ACTION_UP)
    Input.injectInputEvent(context, KeyEvent(
        now, // downTime
        now, // eventTime
        KeyEvent.ACTION_UP, // Action: Key is released.
        keyCode,
        0,
        0,
        KeyCharacterMap.VIRTUAL_KEYBOARD,
        0,
        0,
        InputDevice.SOURCE_UNKNOWN
    ))
    Log.i("KeyboardInput", "Injected key event: $keyCode (DOWN then UP)")
}

// How it might be called in your UI (e.g., from buttons):
coroutineScope.launch { injectSingleKeyEvent(context, KeyEvent.KEYCODE_VOLUME_UP) }
coroutineScope.launch { injectSingleKeyEvent(context, KeyEvent.KEYCODE_VOLUME_DOWN) }
coroutineScope.launch { injectSingleKeyEvent(context, KeyEvent.in.KEYCODE_VOLUME_MUTE) }

2) Commonly Simulated KeyCodes

For a comprehensive list of all possible key codes, refer to the official Android Developers documentation for the KeyEvent class. This example primarily demonstrates common volume control key presses:

  • KeyEvent.KEYCODE_VOLUME_UP: Simulates pressing the Volume Up button.
  • KeyEvent.KEYCODE_VOLUME_DOWN: Simulates pressing the Volume Down button.
  • KeyEvent.KEYCODE_VOLUME_MUTE: Simulates pressing a Mute button.
  • Other potential KeyCodes: KEYCODE_HOME, KEYCODE_BACK, KEYCODE_POWER (often requires higher privileges).

How to Find in Project

The full implementation for this example can be found in the project at: SimulateKeyboardInputExample.kt

⚠️ **GitHub.com Fallback** ⚠️