Introduction: Elevating Personalization on Android TV
Android TV, especially when deployed in custom environments like smart home hubs, automotive infotainment systems, or dedicated media centers, often benefits immensely from a multi-user profile system. Unlike a typical smartphone, a TV is a shared device. Implementing profiles allows each user to have their own personalized experience – from favorite apps and watchlists to specific settings and content recommendations. This tutorial will guide you through building a robust multi-user profile system for your custom Leanback Android TV launcher, ensuring a tailored experience for every member.
We’ll leverage the Android Leanback support library for UI components, focusing on clear data management and a seamless user interface for profile selection and switching. By the end, you’ll have a foundational system that can be expanded to include more complex features.
Core Concepts: User Management and Leanback Integration
Understanding User Profiles
At its heart, a user profile system involves managing distinct sets of data associated with individual users. For an Android TV launcher, this typically includes:
- User ID (unique identifier)
- User Name (display name)
- Profile Picture/Avatar (optional)
- Custom settings (e.g., theme, language preferences)
- Associated data (e.g., watch history, favorite apps, recommendations)
We’ll need a mechanism to store, retrieve, and switch between these profiles securely and efficiently.
Leanback UI Considerations
The Leanback library provides a great foundation for TV-optimized UIs. For a profile selection screen, we can adapt existing components like BrowseFragment or create a custom fragment with a RecyclerView to display profile cards. The goal is to make profile selection intuitive and remote-friendly.
Implementing the Multi-User System
1. Designing the User Profile Data Model
First, let’s define a data class for our user profiles. We’ll use Kotlin’s data class for simplicity and flexibility, and integrate a mechanism for serialization to store it as a string (e.g., JSON).
package com.example.leanbacklauncher.model
data class UserProfile(
val id: String, // Unique ID for the profile
var name: String,
var avatarUrl: String? = null, // URL or local path to avatar image
// Add other user-specific settings here
// val preferredLanguage: String = "en-US"
)
// Example: Using Gson for serialization/deserialization
import com.google.gson.Gson
fun UserProfile.toJson(): String = Gson().toJson(this)
fun String.toUserProfile(): UserProfile = Gson().fromJson(this, UserProfile::class.java)
2. Building the Profile Manager
The ProfileManager will be a singleton responsible for managing the collection of user profiles, persisting them, and handling the currently active profile. We’ll use SharedPreferences for simplicity, but for more complex scenarios, consider Room Database.
package com.example.leanbacklauncher.manager
import android.content.Context
import android.content.SharedPreferences
import com.example.leanbacklauncher.model.UserProfile
import com.example.leanbacklauncher.model.toUserProfile
import com.example.leanbacklauncher.model.toJson
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.util.UUID
class ProfileManager private constructor(context: Context) {
private val PREFS_NAME = "leanback_profile_prefs"
private val KEY_PROFILES = "key_profiles"
private val KEY_CURRENT_PROFILE_ID = "key_current_profile_id"
private val sharedPrefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val gson = Gson()
private var _userProfiles: MutableList<UserProfile> = loadProfiles()
private var _currentProfile: UserProfile? = loadCurrentProfile()
companion object {
@Volatile
private var INSTANCE: ProfileManager? = null
fun getInstance(context: Context): ProfileManager {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: ProfileManager(context.applicationContext).also { INSTANCE = it }
}
}
}
val userProfiles: List<UserProfile>
get() = _userProfiles
val currentProfile: UserProfile?
get() = _currentProfile
fun addProfile(name: String, avatarUrl: String? = null): UserProfile {
val newProfile = UserProfile(id = UUID.randomUUID().toString(), name = name, avatarUrl = avatarUrl)
_userProfiles.add(newProfile)
saveProfiles()
return newProfile
}
fun removeProfile(profileId: String) {
_userProfiles.removeIf { it.id == profileId }
if (_currentProfile?.id == profileId) {
_currentProfile = null
sharedPrefs.edit().remove(KEY_CURRENT_PROFILE_ID).apply()
}
saveProfiles()
}
fun selectProfile(profileId: String): Boolean {
val selected = _userProfiles.find { it.id == profileId }
if (selected != null) {
_currentProfile = selected
sharedPrefs.edit().putString(KEY_CURRENT_PROFILE_ID, profileId).apply()
return true
}
return false
}
fun updateProfile(updatedProfile: UserProfile) {
val index = _userProfiles.indexOfFirst { it.id == updatedProfile.id }
if (index != -1) {
_userProfiles[index] = updatedProfile
if (_currentProfile?.id == updatedProfile.id) {
_currentProfile = updatedProfile // Update current if it's the one modified
sharedPrefs.edit().putString(KEY_CURRENT_PROFILE_ID, updatedProfile.id).apply()
}
saveProfiles()
}
}
private fun saveProfiles() {
val json = gson.toJson(_userProfiles)
sharedPrefs.edit().putString(KEY_PROFILES, json).apply()
}
private fun loadProfiles(): MutableList<UserProfile> {
val json = sharedPrefs.getString(KEY_PROFILES, null)
return if (json != null) {
val type = object : TypeToken<MutableList<UserProfile>>() {}.type
gson.fromJson(json, type)
} else {
mutableListOf()
}
}
private fun loadCurrentProfile(): UserProfile? {
val currentProfileId = sharedPrefs.getString(KEY_CURRENT_PROFILE_ID, null)
return _userProfiles.find { it.id == currentProfileId }
}
}
3. Creating the Profile Selection UI
We’ll create a simple Fragment for selecting profiles. For a true Leanback experience, you might integrate this into a custom BrowseFragment row or a dedicated GuidedStepFragment. Here, we’ll demonstrate a basic custom fragment using a RecyclerView.
package com.example.leanbacklauncher.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.leanbacklauncher.R // Assume you have R.layout.fragment_profile_selection and R.id.profile_recycler_view, R.id.profile_name_text
import com.example.leanbacklauncher.manager.ProfileManager
import com.example.leanbacklauncher.model.UserProfile
class ProfileSelectionFragment : Fragment() {
private lateinit var profileManager: ProfileManager
private lateinit var profileAdapter: ProfileAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
context?.let { profileManager = ProfileManager.getInstance(it) }
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_profile_selection, container, false)
val recyclerView: RecyclerView = view.findViewById(R.id.profile_recycler_view)
recyclerView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
profileAdapter = ProfileAdapter(profileManager.userProfiles) { selectedProfile ->
profileManager.selectProfile(selectedProfile.id)
// Navigate to main launcher UI or refresh content
(activity as? ProfileSelectionListener)?.onProfileSelected(selectedProfile)
}
recyclerView.adapter = profileAdapter
// Optionally add a button for creating new profiles
view.findViewById<View>(R.id.add_profile_button)?.setOnClickListener {
// Implement dialog or new fragment to get new profile name
val newProfile = profileManager.addProfile("New User ${profileManager.userProfiles.size + 1}")
profileAdapter.updateProfiles(profileManager.userProfiles)
profileManager.selectProfile(newProfile.id)
(activity as? ProfileSelectionListener)?.onProfileSelected(newProfile)
}
return view
}
interface ProfileSelectionListener {
fun onProfileSelected(profile: UserProfile)
}
// Inner Adapter for the RecyclerView
private class ProfileAdapter(var profiles: List<UserProfile>, private val onProfileClicked: (UserProfile) -> Unit) :
RecyclerView.Adapter<ProfileAdapter.ProfileViewHolder>() {
class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val profileName: TextView = view.findViewById(R.id.profile_name_text)
// Add ImageView for avatar here
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_profile, parent, false) // Assume R.layout.item_profile
return ProfileViewHolder(view)
}
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
val profile = profiles[position]
holder.profileName.text = profile.name
// Load avatar image here (e.g., using Glide/Picasso)
holder.itemView.setOnClickListener { onProfileClicked(profile) }
}
override fun getItemCount(): Int = profiles.size
fun updateProfiles(newProfiles: List<UserProfile>) {
this.profiles = newProfiles
notifyDataSetChanged()
}
}
}
In your main launcher activity, you’d show this ProfileSelectionFragment when the app starts if no profile is selected, or provide an option to access it from the main UI. Once a profile is selected, you’d update your main launcher’s content based on ProfileManager.getInstance(context).currentProfile.
Example of `activity` handling `onProfileSelected`:
class MainActivity : AppCompatActivity(), ProfileSelectionFragment.ProfileSelectionListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val profileManager = ProfileManager.getInstance(this)
if (profileManager.currentProfile == null && profileManager.userProfiles.isEmpty()) {
// Create a default profile if none exists
profileManager.addProfile("Guest User")
profileManager.selectProfile(profileManager.userProfiles.first().id)
}
if (profileManager.currentProfile == null) {
// Show profile selection if no current profile is set
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, ProfileSelectionFragment())
.commit()
} else {
// Load main launcher content for the current user
loadLauncherContent(profileManager.currentProfile!!)
}
}
override fun onProfileSelected(profile: UserProfile) {
// Once a profile is selected, remove the selection fragment and load main launcher content
supportFragmentManager.popBackStack()
loadLauncherContent(profile)
}
private fun loadLauncherContent(profile: UserProfile) {
// Your logic to load/refresh UI components based on the selected profile
Log.d("MainActivity", "Loading content for profile: ${profile.name}")
// Example: Update a welcome message, load user-specific recommendations
// supportFragmentManager.beginTransaction()
// .replace(R.id.fragment_container, MainLeanbackBrowseFragment())
// .commit()
}
}
4. Integrating Profile Switching in the Launcher
Provide an accessible way for users to switch profiles from the main launcher UI. This could be:
- An icon in the top-left corner (common for Leanback).
- An option within a settings menu.
- A long-press action on the remote’s home button (if customizing system-level behavior).
When the user chooses to switch, simply launch the ProfileSelectionFragment again.
Important Considerations
As you build out your system, keep these in mind:
- Error Handling: Implement robust error handling for file I/O, JSON parsing, etc.
- Security: For sensitive data, consider encryption. For multi-user environments, protect against unauthorized profile modifications.
- Scalability: For many profiles or complex data, upgrade from
SharedPreferencesto a Room database or a custom backend service. - UI/UX Polish: Ensure smooth animations, clear focus management, and intuitive navigation for the Leanback environment.
- Default Profile: Always ensure there’s a fallback or default profile (e.g.,
Android Mobile Specs & Compare Directory
Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!
Compare Devices Specs →