Examples - solum-signage-system/solum-signage-example GitHub Wiki
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.
To programmatically control visual display attributes such as brightness, contrast, and saturation of the SoluM Signage device's screen.
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).
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
withDisplayAttribute.BRIGHTNESS
associated withDisplayId {0, 1}
. - A separate
DisplayOperation
withDisplayAttribute.CONTRAST
associated withDisplayId {0}
.
Consequently, this configuration leads to the following control behavior:
- When you adjust the brightness using the
DisplayOperation
forDisplayAttribute.BRIGHTNESS
(associated withDisplayId {0, 1}
), it will control both Display 0 and Display 1 concurrently. - However, when you adjust the contrast using its
DisplayOperation
(associated withDisplayId {0}
), it will apply only to Display 0.
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)
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)
}
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
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()
You might find relevant code in a file like ChangeDisplayAttributeExample.kt
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).
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.
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.
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.")
}
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.")
}
}
You might find relevant code in a file like FindAudioOutputDeviceExample.kt
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.
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.
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.
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) }
}
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)
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.
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.
To programmatically toggle the mute state (mute/unmute) of a specific audio stream (e.g., music) on the SoluM Signage device.
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.
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.
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)
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
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.
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.
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.
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 ownDisplay
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 aPresentation
, you can leverage the power of Jetpack Compose to build flexible UIs for secondary screens.
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.
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.")
}
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
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
}
}
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.
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).
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.
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.
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") }
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).
The full implementation for this example can be found in the project at: RotateScreenExample.kt
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.
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.
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.
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)}")
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
The full implementation for this example can be found in the project at: ChangeLanguageExample.kt
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.
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.
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.
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()
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)
The full implementation for this example can be found in the project at: ChangeTimeZoneExample.kt
To programmatically simulate physical keyboard key presses (e.g., volume up/down, mute) on the SoluM Signage device, typically for controlling system-level functionalities.
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.
This example utilizes the Input.injectInputEvent()
API to dispatch KeyEvent
objects to the system. This allows the application to mimic actual key presses.
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) }
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).
The full implementation for this example can be found in the project at: SimulateKeyboardInputExample.kt