Android IoT, Automotive, & Smart TV Customizations

How to Create a Multi-User Profile System in Your Custom Leanback Android TV Launcher

Google AdSense Native Placement - Horizontal Top-Post banner

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 SharedPreferences to 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 →
Google AdSense Inline Placement - Content Footer banner