ThemePicker: add support for custom themed icon pack [2/3]

Change-Id: Ib59201fea0939c9ebf2ebbc252e9259532dcbc72
Signed-off-by: jhonboy121 <alfredmathew05@gmail.com>
Signed-off-by: minaripenguin <minaripenguin@users.noreply.github.com>
Signed-off-by: PainKiller3 <ninadpatil100@gmail.com>
Signed-off-by: Dmitrii <bankersenator@gmail.com>
Signed-off-by: Jackeagle <jackeagle102@gmail.com>
diff --git a/Android.bp b/Android.bp
index cce9aed..9e5448e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -60,6 +60,9 @@
         "SettingsLibSettingsTheme",
         "SystemUI-statsd",
         "styleprotoslite",
+        "androidx.lifecycle_lifecycle-runtime-ktx",
+        "kotlinx_coroutines_android",
+        "kotlinx_coroutines",
     ],
 
     jni_libs: [
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index bc2103d..0af3cab 100755
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -50,6 +50,8 @@
     <uses-permission android:name="com.android.launcher3.permission.READ_SETTINGS" />
     <uses-permission android:name="com.android.launcher3.permission.WRITE_SETTINGS" />
 
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+
     <application
         tools:replace="android:icon,android:name"
         android:extractNativeLibs="false"
diff --git a/res/layout/themed_icon_pack_section_view.xml b/res/layout/themed_icon_pack_section_view.xml
new file mode 100644
index 0000000..573920b
--- /dev/null
+++ b/res/layout/themed_icon_pack_section_view.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2022 FlamingoOS Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.customization.picker.themedicon.ThemedIconPackSectionView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?selectableItemBackground"
+    android:clickable="true"
+    android:orientation="vertical"
+    android:paddingHorizontal="@dimen/section_horizontal_padding"
+    android:paddingVertical="@dimen/section_vertical_padding">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/themed_icon_custom_title"
+            style="@style/SectionTitleTextStyle" />
+
+        <TextView
+            android:id="@+id/themed_icon_pack_summary"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            style="@style/SectionSubtitleTextStyle"/>
+    </LinearLayout>
+</com.android.customization.picker.themedicon.ThemedIconPackSectionView>
diff --git a/res/layout/themed_icons_app_list_item.xml b/res/layout/themed_icons_app_list_item.xml
new file mode 100644
index 0000000..37979a8
--- /dev/null
+++ b/res/layout/themed_icons_app_list_item.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 FlamingoOS Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:background="?android:attr/selectableItemBackground"
+    android:clipChildren="false"
+    android:clipToPadding="false">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginVertical="4dp"
+        android:scaleType="centerInside"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent" />
+
+    <TextView
+        android:id="@+id/label"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/default_margin"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:textColor="?android:attr/textColorPrimary"
+        app:layout_constraintStart_toEndOf="@id/icon"
+        app:layout_constraintEnd_toStartOf="@id/radio_button"
+        app:layout_constraintTop_toTopOf="@id/icon"
+        app:layout_constraintBottom_toTopOf="@id/package_name" />
+
+    <TextView
+        android:id="@+id/package_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorSecondary"
+        app:layout_constraintStart_toStartOf="@id/label"
+        app:layout_constraintEnd_toEndOf="@id/label"
+        app:layout_constraintTop_toBottomOf="@id/label"
+        app:layout_constraintBottom_toBottomOf="@id/icon" />
+
+    <RadioButton
+        android:id="@+id/radio_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:focusable="false"
+        android:clickable="false"
+        app:layout_constraintBottom_toBottomOf="@id/icon"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="@id/icon" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/res/layout/themed_icons_app_list_layout.xml b/res/layout/themed_icons_app_list_layout.xml
new file mode 100644
index 0000000..be337be
--- /dev/null
+++ b/res/layout/themed_icons_app_list_layout.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2022 FlamingoOS Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <include layout="@layout/section_header" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/apps_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+     <androidx.core.widget.ContentLoadingProgressBar
+        android:id="@+id/loading_progress"
+        style="@android:style/Widget.DeviceDefault.ProgressBar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:indeterminate="true"
+        android:layout_gravity="center" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/bliss_strings.xml b/res/values/bliss_strings.xml
index 6eeb3b1..abeb26b 100644
--- a/res/values/bliss_strings.xml
+++ b/res/values/bliss_strings.xml
@@ -1,20 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
-     Copyright (C) 2018 The Android Open Source Project
 
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
+    Copyright (C) 2020-2022 The AwakenOS Project
 
-          http://www.apache.org/licenses/LICENSE-2.0
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
 
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
 -->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+<resources>
 
     <!-- Icon's section -->
     <string name="icon_preview_card_content_description">Icon Preview</string>
@@ -23,5 +24,7 @@
     <!-- Font's section -->
     <string name="font_title">System Fonts</string>
     <string name="font_preview_card_content_description">Font Preview</string>
-</resources>
 
+    <string name="system_icons">System icons</string>
+    <string name="themed_icon_custom_title">Custom themed icons</string>
+</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 0ad221e..a64838b 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -135,4 +135,7 @@
     <dimen name="color_seed_option_tile_padding">10dp</dimen>
     <dimen name="color_seed_option_tile_padding_selected">6dp</dimen>
     <dimen name="color_seed_chip_margin">14dp</dimen>
+
+    <!-- For the custom themed icons -->
+    <dimen name="default_margin">16dp</dimen>
 </resources>
diff --git a/src/com/android/customization/ResourceProxy.java b/src/com/android/customization/ResourceProxy.java
new file mode 100644
index 0000000..55cd908
--- /dev/null
+++ b/src/com/android/customization/ResourceProxy.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2022 FlamingoOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.customization;
+
+import com.android.wallpaper.R;
+
+/**
+ * Hack of the century. For some reason still unknown,
+ * aapt compiled resources are unresolved in kt files.
+ * So added proxy variables to work around this mess.
+ */
+public class ResourceProxy {
+    public static final class Id {
+        public static final int themed_icon_pack_summary = R.id.themed_icon_pack_summary;
+
+        public static final int apps_list = R.id.apps_list;
+        public static final int loading_progress = R.id.loading_progress;
+
+        public static final int icon = R.id.icon;
+        public static final int label = R.id.label;
+        public static final int package_name = R.id.package_name;
+        public static final int radio_button = R.id.radio_button;
+    }
+
+    public static final class String {
+        public static final int themed_icon_custom_title = R.string.themed_icon_custom_title;
+        public static final int system_icons = R.string.system_icons;
+    }
+
+    public static final class Layout {
+        public static final int themed_icon_pack_section_view = R.layout.themed_icon_pack_section_view;
+        public static final int themed_icons_app_list_layout = R.layout.themed_icons_app_list_layout;
+        public static final int themed_icons_app_list_item = R.layout.themed_icons_app_list_item;
+    }
+}
diff --git a/src/com/android/customization/model/themedicon/ThemedIconPackSectionController.kt b/src/com/android/customization/model/themedicon/ThemedIconPackSectionController.kt
new file mode 100644
index 0000000..bfc1d09
--- /dev/null
+++ b/src/com/android/customization/model/themedicon/ThemedIconPackSectionController.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2022 FlamingoOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.customization.model.themedicon
+
+import android.content.ContentValues
+import android.content.Context
+import android.content.pm.PackageManager.NameNotFoundException
+import android.database.Cursor
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+
+import androidx.lifecycle.LifecycleCoroutineScope
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+
+import com.android.customization.picker.themedicon.ThemedIconPackPickerFragment
+import com.android.customization.picker.themedicon.ThemedIconPackSectionView
+import com.android.customization.ResourceProxy
+import com.android.wallpaper.model.CustomizationSectionController
+import com.android.wallpaper.model.CustomizationSectionController.CustomizationSectionNavigationController
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+class ThemedIconPackSectionController(
+    private val context: Context,
+    private val navigationController: CustomizationSectionNavigationController,
+    private val themedIconSwitchProvider: ThemedIconSwitchProvider,
+    private val lifecycleOwner: LifecycleOwner,
+    savedInstanceState: Bundle?,
+) : CustomizationSectionController<ThemedIconPackSectionView> {
+
+    private val pm = context.packageManager
+    private val mutex = Mutex()
+
+    private val lifecycleScope: LifecycleCoroutineScope
+        get() = lifecycleOwner.lifecycleScope
+
+    private val themedIconUtils: ThemedIconUtils
+        get() = themedIconSwitchProvider.themedIconUtils
+
+    private var themedIconPackage: String? = null
+    private var summaryView: TextView? = null
+
+    init {
+        val pkg = savedInstanceState?.getString(
+            KEY_THEMED_ICON_PACK,
+            null /* defaultValue */
+        )
+        if (pkg == null) {
+            if (isAvailable(context)) {
+                fetchThemedIconPack()
+            }
+        } else {
+            themedIconPackage = pkg
+        }
+    }
+
+    private fun fetchThemedIconPack() {
+        val uri = themedIconUtils.getUriForPath(KEY_THEMED_ICON_PACK)
+        lifecycleScope.launchWhenCreated {
+            withContext(Dispatchers.Default) {
+                val cursor = context.contentResolver.query(
+                    uri,
+                    null, /* projection*/
+                    null, /* selection */
+                    null, /* selectionArgs */
+                    null, /* sortOrder */
+                ) ?: return@withContext
+                cursor.use {
+                    it.moveToNext()
+                    mutex.withLock {
+                        themedIconPackage = it.getString(it.getColumnIndex(KEY_NAME))
+                    }
+                }
+            }
+        }
+    }
+
+    override fun isAvailable(context: Context?) = themedIconSwitchProvider.isThemedIconAvailable()
+
+    override fun createView(context: Context): ThemedIconPackSectionView {
+        val view = LayoutInflater.from(context).inflate(
+            ResourceProxy.Layout.themed_icon_pack_section_view,
+            null /* root */
+        ) as ThemedIconPackSectionView
+        summaryView = view.findViewById(ResourceProxy.Id.themed_icon_pack_summary)
+        lifecycleScope.launch {
+            mutex.withLock {
+                updateSummary(themedIconPackage)
+            }
+        }
+        view.setOnClickListener {
+            val pickerFragment = ThemedIconPackPickerFragment()
+            pickerFragment.setOnAppSelectListener {
+                mutex.withLock {
+                    themedIconPackage = it
+                }
+                withContext(Dispatchers.Default) {
+                    savePackage(it)
+                }
+                // Toggling it on and off will trigger updates. Hacky but rather easy.
+                themedIconSwitchProvider.setThemedIconEnabled(false)
+                themedIconSwitchProvider.setThemedIconEnabled(it != null)
+            }
+            pickerFragment.setInitalCheckedApp(themedIconPackage)
+            navigationController.navigateTo(pickerFragment)
+        }
+        return view
+    }
+
+    override fun onSaveInstanceState(savedInstanceState: Bundle) {
+        lifecycleScope.launch {
+            mutex.withLock {
+                savedInstanceState.putString(KEY_THEMED_ICON_PACK, themedIconPackage)
+            }
+        }
+    }
+
+    private fun updateSummary(packageName: String?) {
+        summaryView?.text = if (packageName != null) {
+            val packageLabel = getLabelForPackageName(packageName)
+            if (packageLabel != null) {
+                "$packageLabel ($packageName)"
+            } else {
+                packageName
+            }
+        } else {
+            context.getString(ResourceProxy.String.system_icons)
+        }
+    }
+
+    private fun getLabelForPackageName(packageName: String): CharSequence? =
+        try {
+            pm.getApplicationInfo(packageName, 0 /* flags */).loadLabel(pm)
+        } catch (e: NameNotFoundException) {
+            null
+        }
+
+    private suspend fun savePackage(packageName: String?) {
+        val values = ContentValues().apply {
+            put(KEY_NAME, packageName)
+        }
+        val uri = themedIconUtils.getUriForPath(KEY_THEMED_ICON_PACK)
+        val result = context.contentResolver.update(
+            uri,
+            values,
+            null, /* where */
+            null, /* selectionArgs */
+        )
+        if (result != RESULT_SUCCESS) {
+            Log.e(TAG, "Failed to update themed icon pack")
+        }
+    }
+
+    companion object {
+        private const val TAG = "ThemedIconPackSectionController"
+
+        private const val KEY_THEMED_ICON_PACK = "themed_icon_pack"
+        private const val KEY_NAME = "name"
+
+        private const val RESULT_SUCCESS = 1
+    }
+}
diff --git a/src/com/android/customization/model/themedicon/ThemedIconSwitchProvider.java b/src/com/android/customization/model/themedicon/ThemedIconSwitchProvider.java
index 9acd319..d52b6f5 100644
--- a/src/com/android/customization/model/themedicon/ThemedIconSwitchProvider.java
+++ b/src/com/android/customization/model/themedicon/ThemedIconSwitchProvider.java
@@ -130,6 +130,10 @@
         });
     }
 
+    protected ThemedIconUtils getThemedIconUtils() {
+        return mThemedIconUtils;
+    }
+
     private void postMainThread(Runnable runnable) {
         new Handler(Looper.getMainLooper()).post(runnable);
     }
diff --git a/src/com/android/customization/module/DefaultCustomizationSections.java b/src/com/android/customization/module/DefaultCustomizationSections.java
index d538530..a34d51c 100644
--- a/src/com/android/customization/module/DefaultCustomizationSections.java
+++ b/src/com/android/customization/module/DefaultCustomizationSections.java
@@ -16,6 +16,7 @@
 import com.android.customization.model.iconpack.IconPackSectionController;
 import com.android.customization.model.mode.DarkModeSectionController;
 import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.customization.model.themedicon.ThemedIconPackSectionController;
 import com.android.customization.model.themedicon.ThemedIconSectionController;
 import com.android.customization.model.themedicon.ThemedIconSwitchProvider;
 import com.android.wallpaper.model.CustomizationSectionController;
@@ -40,7 +41,7 @@
             WallpaperPreviewNavigator wallpaperPreviewNavigator,
             CustomizationSectionNavigationController sectionNavigationController,
             @Nullable Bundle savedInstanceState) {
-        List<CustomizationSectionController<?>> sectionControllers = new ArrayList<>();
+        final List<CustomizationSectionController<?>> sectionControllers = new ArrayList<>();
 
         // Wallpaper section.
         sectionControllers.add(new WallpaperSectionController(
@@ -48,7 +49,7 @@
                 workspaceViewModel, sectionNavigationController, wallpaperPreviewNavigator,
                 savedInstanceState));
 
-        // Color section
+        // Theme color section.
         sectionControllers.add(new ColorSectionController(
                 activity, wallpaperColorsViewModel, lifecycleOwner, savedInstanceState));
 
@@ -61,6 +62,12 @@
                 ThemedIconSwitchProvider.getInstance(activity), workspaceViewModel,
                 savedInstanceState));
 
+        // Custom themed icon pack section.
+        sectionControllers.add(new ThemedIconPackSectionController(
+                activity, sectionNavigationController,
+                ThemedIconSwitchProvider.getInstance(activity),
+                lifecycleOwner, savedInstanceState));
+
         // App grid section.
         sectionControllers.add(new GridSectionController(
                 GridOptionsManager.getInstance(activity), sectionNavigationController));
diff --git a/src/com/android/customization/picker/themedicon/ThemedIconPackPickerFragment.kt b/src/com/android/customization/picker/themedicon/ThemedIconPackPickerFragment.kt
new file mode 100644
index 0000000..01efe79
--- /dev/null
+++ b/src/com/android/customization/picker/themedicon/ThemedIconPackPickerFragment.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2022 FlamingoOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.customization.picker.themedicon
+
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsets
+import android.widget.ImageView
+import android.widget.RadioButton
+import android.widget.TextView
+
+import androidx.core.widget.ContentLoadingProgressBar
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.customization.ResourceProxy
+import com.android.wallpaper.picker.AppbarFragment
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class ThemedIconPackPickerFragment : AppbarFragment() {
+
+    private lateinit var pm: PackageManager
+    private lateinit var adapter: AppListAdapter
+
+    private var recyclerView: RecyclerView? = null
+    private var progressBar: ContentLoadingProgressBar? = null
+    private var appSelectListener: suspend CoroutineScope.(String?) -> Unit = {}
+    private var initialCheckedApp: String? = null
+
+    fun setOnAppSelectListener(listener: suspend CoroutineScope.(String?) -> Unit) {
+        appSelectListener = listener
+    }
+
+    fun setInitalCheckedApp(checkedApp: String?) {
+        initialCheckedApp = checkedApp
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        pm = requireContext().packageManager
+    }
+
+    override fun getDefaultTitle(): CharSequence = getString(ResourceProxy.String.themed_icon_custom_title)
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? = inflater.inflate(
+        ResourceProxy.Layout.themed_icons_app_list_layout,
+        container,
+        false /* attachToParent */
+    )
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        setUpToolbar(view)
+
+        // For nav bar edge-to-edge effect.
+        view.setOnApplyWindowInsetsListener { v, windowInsets ->
+            val systemBarsInsets = windowInsets.getInsets(WindowInsets.Type.systemBars())
+            v.setPadding(
+                v.paddingLeft,
+                systemBarsInsets.top,
+                v.paddingRight,
+                systemBarsInsets.bottom
+            )
+            return@setOnApplyWindowInsetsListener WindowInsets.CONSUMED
+        }
+
+        adapter = AppListAdapter(requireActivity().layoutInflater).also {
+            it.setCheckedItem(initialCheckedApp)
+            it.setOnItemSelectListener {
+                lifecycleScope.launch {
+                    appSelectListener(it)
+                }
+            }
+        }
+
+        recyclerView = view.findViewById<RecyclerView>(ResourceProxy.Id.apps_list)?.also {
+            it.layoutManager = LinearLayoutManager(context)
+            it.adapter = adapter
+        }
+
+        progressBar = view.findViewById(ResourceProxy.Id.loading_progress)
+        progressBar?.show()
+        lifecycleScope.launch {
+            refreshList()
+        }
+    }
+
+    private suspend fun refreshList() {
+        val list = withContext(Dispatchers.Default) {
+            pm.getInstalledPackages(PackageManager.MATCH_ALL).filter {
+                !it.applicationInfo.isSystemApp() && hasIconResourceMap(it)
+            }.map {
+                AppInfo(
+                    it.packageName,
+                    it.applicationInfo.loadLabel(pm).toString(),
+                    it.applicationInfo.loadIcon(pm),
+                )
+            }.sortedBy {
+                it.label
+            }.toMutableList()
+        }
+        list.add(
+            0 /* index */,
+            AppInfo(
+                null /* packageName */,
+                getString(ResourceProxy.String.system_icons),
+                pm.getDefaultActivityIcon()
+            )
+        )
+
+        adapter.submitList(list.toList())
+        progressBar?.hide()
+    }
+
+    private fun hasIconResourceMap(packageInfo: PackageInfo): Boolean {
+        val res = try {
+            pm.getResourcesForApplication(packageInfo.applicationInfo)
+        } catch (e: PackageManager.NameNotFoundException) {
+            null
+        } ?: return false
+
+        val resID = res.getIdentifier(
+            "grayscale_icon_map",
+            "xml",
+            packageInfo.packageName
+        )
+        return resID != 0
+    }
+
+    private class AppListAdapter(
+        private val layoutInflater: LayoutInflater
+    ) : ListAdapter<AppInfo, AppListViewHolder>(itemCallback) {
+
+        // Package name of the checked item
+        private var checkedItem: String? = null
+        // Index of the checked item
+        private var checkedItemIndex = -1
+        private var itemSelectListener: (String?) -> Unit = {}
+
+        /**
+         * @param item package name of the checked item
+         */
+        fun setCheckedItem(item: String?) {
+            checkedItem = item
+            updateCheckedItemIndex(currentList)
+        }
+
+        private fun updateCheckedItemIndex(list: List<AppInfo>?) {
+            if (list == null) {
+                checkedItemIndex = -1
+                return
+            }
+            checkedItemIndex = if (checkedItem == null) {
+                0
+            } else list.indexOfFirst {
+                it.packageName == checkedItem
+            }
+        }
+
+        /**
+         * Lambda parameter will be package name of the checked item
+         */
+        fun setOnItemSelectListener(listener: (String?) -> Unit) {
+            itemSelectListener = listener
+        }
+
+        override fun onCreateViewHolder(
+            parent: ViewGroup,
+            viewType: Int
+        ) = AppListViewHolder(
+                layoutInflater.inflate(
+                    ResourceProxy.Layout.themed_icons_app_list_item,
+                    parent,
+                    false /* attachToParent */
+                )
+            )
+
+        override fun onBindViewHolder(holder: AppListViewHolder, position: Int) {
+            val item = getItem(position)
+
+            holder.label.text = item.label
+            holder.packageName.visibility = if (item.packageName == null) View.GONE else View.VISIBLE
+            holder.packageName.text = item.packageName
+            holder.icon.setImageDrawable(item.icon)
+            holder.radioButton.isChecked = checkedItemIndex == position
+
+            holder.itemView.setOnClickListener {
+                if (checkedItemIndex == position) return@setOnClickListener
+
+                checkedItem = item.packageName
+                val tmpIndex = checkedItemIndex
+                checkedItemIndex = position
+
+                if (tmpIndex != -1) notifyItemChanged(tmpIndex)
+                notifyItemChanged(checkedItemIndex)
+
+                itemSelectListener(item.packageName)
+            }
+        }
+
+        override fun submitList(list: List<AppInfo>?) {
+            updateCheckedItemIndex(list)
+            super.submitList(list)
+        }
+
+        companion object {
+            private val itemCallback = object : DiffUtil.ItemCallback<AppInfo>() {
+                override fun areItemsTheSame(oldInfo: AppInfo, newInfo: AppInfo) =
+                    oldInfo.packageName == newInfo.packageName
+
+                override fun areContentsTheSame(oldInfo: AppInfo, newInfo: AppInfo) =
+                    oldInfo == newInfo
+            }
+        }
+    }
+
+    private class AppListViewHolder(
+        itemView: View
+    ) : RecyclerView.ViewHolder(itemView) {
+
+        val icon: ImageView = itemView.findViewById(ResourceProxy.Id.icon)
+        val label: TextView = itemView.findViewById(ResourceProxy.Id.label)
+        val packageName: TextView = itemView.findViewById(ResourceProxy.Id.package_name)
+        val radioButton: RadioButton = itemView.findViewById(ResourceProxy.Id.radio_button)
+    }
+
+    private data class AppInfo(
+        val packageName: String?,
+        val label: String,
+        val icon: Drawable,
+    )
+}
diff --git a/src/com/android/customization/picker/themedicon/ThemedIconPackSectionView.kt b/src/com/android/customization/picker/themedicon/ThemedIconPackSectionView.kt
new file mode 100644
index 0000000..e2491a8
--- /dev/null
+++ b/src/com/android/customization/picker/themedicon/ThemedIconPackSectionView.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 FlamingoOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.customization.picker.themedicon
+
+import android.content.Context
+import android.util.AttributeSet
+
+import com.android.wallpaper.picker.SectionView
+
+class ThemedIconPackSectionView(
+    context: Context,
+    attrs: AttributeSet?
+) : SectionView(context, attrs)
\ No newline at end of file