[Android TV] インストールされているアプリ取得

ランチャー的なアプリを作る場合はインストールされているアプリを呼び出す機能が必要かと思います
実装はAndroid TVじゃなくても同様ですがAndroid TVの場合はバナーがありますのでバナーがある場合と無い場合の配慮がいります

YoutubeやGoogle Playがプリインストールされている場合の配慮
設定アプリはintent呼び出しできないようですがバナー取得のためターゲットに追加
UIはとりあえずグリットで表示でCardはimageを表示するためClassicCardを使用しました

Card長押しで順番入れ替えとかができるといい感じですが、入れ替えの実装とデータの管理方法がまだ未調査です…

data class InstalledApplication(
    val packageName: String,
    val name: String,
    val icon: Drawable
)

@SuppressLint("QueryPermissionsNeeded")
fun getInstalledAppList(
    context: Context
): List<InstalledApplication> {
    val packageManager = context.packageManager
    val flags = PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.MATCH_DISABLED_COMPONENTS
    val applicationInfo = packageManager.getInstalledApplications(flags)
    val myPackageName = context.packageName

    val target = listOf(
        "com.android.tv.settings",
        "com.android.vending", //Google Play Store
        "com.google.android.youtube.tv"
    )
    val list = mutableListOf<InstalledApplication>()
    applicationInfo.forEach { info ->
        val name = info.loadLabel(packageManager).toString()
        //Log.d(TAG, "$name(${info.packageName})")
        if (target.indexOf(info.packageName) >= 0 ||
            (info.packageName != myPackageName && (info.flags and ApplicationInfo.FLAG_SYSTEM) != ApplicationInfo.FLAG_SYSTEM)
        ) {
            var banner: Drawable? = null
            try {
                banner = info.loadBanner(packageManager)
            } catch (_: Exception) {}
            list += InstalledApplication(info.packageName, name, banner ?: info.loadIcon(packageManager))
        }
    }
    return list.toList()
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun InstalledAppListScreen(modifier: Modifier = Modifier) {
    val context = LocalContext.current
    val list = remember { mutableStateListOf<InstalledApplication>() }

    LaunchedEffect(Unit) {
        list.clear()
        list.addAll(getInstalledAppList(context))
    }

    val columnCount = 4
    val margin = 24
    val screenWidth = LocalConfiguration.current.screenWidthDp
    val width = (screenWidth - (margin * (columnCount + 1))) / columnCount
    val tvLazyGridState = rememberTvLazyGridState()
    TvLazyVerticalGrid(
        state = tvLazyGridState,
        modifier = modifier,
        columns = TvGridCells.Fixed(columnCount),
        contentPadding = PaddingValues(margin.dp),
        verticalArrangement = Arrangement.spacedBy(margin.dp)
    ) {
        itemsIndexed(list) {_, item ->
            Column {
                val image = remember {
                    item.icon.toBitmap(item.icon.intrinsicWidth, item.icon.intrinsicHeight, null)
                }
                // bannerのサイズは320*180
                ClassicCard(
                    onClick = {
                        val intent = context.packageManager.getLaunchIntentForPackage(item.packageName)
                        intent?.let {
                            context.startActivity(it)
                        }
                    },
                    image = {
                        val bitmap = item.icon.toBitmap(item.icon.intrinsicWidth, item.icon.intrinsicHeight, null)
                        Image(image.asImageBitmap(), null, Modifier.width(width.dp).aspectRatio(320f / 180), contentScale = ContentScale.Fit)
                    },
                    title = {},
                    subtitle = {},
                    description = {}
                )
                Spacer(Modifier.height(8.dp))
                //TODO: fontSizeはコアテキストは最低16.sp/その他は最低12.sp
                Text(item.name, Modifier.width(width.dp), fontSize = 16.sp, textAlign = TextAlign.Center)
            }
        }
    }
}

Android Studio Hedgehog 2023.1.1 RC 3 built on November 3, 2023