From f21f13ba6cda8d64276e43f5da293566c9e8b509 Mon Sep 17 00:00:00 2001 From: ottoptj Date: Thu, 9 May 2024 11:29:01 +0300 Subject: [PATCH] Refactoring everything into a recyclerview. Missing: UI Design, Renaming functionality --- app/build.gradle.kts | 1 + .../eu/ottop/yamlauncher/AppActionMenu.kt | 118 ++++++++++ .../eu/ottop/yamlauncher/AppMenuActivity.kt | 203 ++++++++++++++++-- .../yamlauncher/SharedPreferenceManager.kt | 48 +++++ app/src/main/res/layout/activity_app_menu.xml | 41 ++-- app/src/main/res/layout/app_action_menu.xml | 44 ++-- app/src/main/res/layout/app_item_layout.xml | 18 ++ app/src/main/res/layout/rename_view.xml | 20 ++ 8 files changed, 433 insertions(+), 60 deletions(-) create mode 100644 app/src/main/java/eu/ottop/yamlauncher/AppActionMenu.kt create mode 100644 app/src/main/java/eu/ottop/yamlauncher/SharedPreferenceManager.kt create mode 100644 app/src/main/res/layout/app_item_layout.xml create mode 100644 app/src/main/res/layout/rename_view.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 12f05cd..fd57a8b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") implementation("com.google.code.gson:gson:2.10") + implementation("androidx.recyclerview:recyclerview:1.3.2") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/java/eu/ottop/yamlauncher/AppActionMenu.kt b/app/src/main/java/eu/ottop/yamlauncher/AppActionMenu.kt new file mode 100644 index 0000000..c069e45 --- /dev/null +++ b/app/src/main/java/eu/ottop/yamlauncher/AppActionMenu.kt @@ -0,0 +1,118 @@ +package eu.ottop.yamlauncher + +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.SearchView +import android.widget.TextView +import eu.ottop.yamlauncher.databinding.ActivityAppMenuBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class AppActionMenu { + + private val sharedPreferenceManager = SharedPreferenceManager() + + fun setActionListeners( + activity: AppMenuActivity, + uiScope: CoroutineScope, + binding: ActivityAppMenuBinding, + textView: TextView, + editLayout: LinearLayout, + actionMenu: View, + searchView: SearchView, + appInfo: ApplicationInfo, + userHandle: UserHandle, + workProfile: Int, + launcherApps: LauncherApps, + mainActivity: LauncherActivityInfo? + ){ + + actionMenu.findViewById(R.id.info).setOnClickListener { + if (mainActivity != null) { + launcherApps.startAppDetailsActivity( + mainActivity.componentName, + userHandle, + null, + null + ) + } + + actionMenu.visibility = View.GONE + textView.visibility = View.VISIBLE + } + + actionMenu.findViewById(R.id.uninstall).setOnClickListener { + val intent = Intent(Intent.ACTION_DELETE) + intent.data = Uri.parse("package:${appInfo.packageName}") + intent.putExtra(Intent.EXTRA_USER, userHandle) + activity.startActivity(intent) + + actionMenu.visibility = View.GONE + textView.visibility = View.VISIBLE + } + + actionMenu.findViewById(R.id.rename).setOnClickListener { + textView.visibility = View.GONE + editLayout.visibility = View.VISIBLE + actionMenu.visibility = View.GONE + val editText = editLayout.findViewById(R.id.app_name) + searchView.visibility = View.GONE + editText.requestFocus() + + val handler = Handler(Looper.getMainLooper()) + handler.postDelayed({ + val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) + //binding.appList.scrollToDescendant(textView) + }, 100) + + binding.root.addOnLayoutChangeListener { + _, _, top, _, bottom, _, oldTop, _, oldBottom -> + if (bottom - top > oldBottom - oldTop) { + editLayout.clearFocus() + + editLayout.visibility = View.GONE + textView.visibility = View.VISIBLE + searchView.visibility = View.VISIBLE + } + } + + editText.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(editText.windowToken, 0) + sharedPreferenceManager.setAppName(activity, appInfo.packageName, workProfile, editText.text.toString()) + uiScope.launch { + //activity.refreshAppMenu() + } + + return@setOnEditorActionListener true + } + false + } + } + + actionMenu.findViewById(R.id.hide).setOnClickListener { + sharedPreferenceManager.setAppHidden(activity, appInfo.packageName, workProfile, true) + actionMenu.visibility = View.GONE + textView.visibility = View.GONE + } + + actionMenu.findViewById(R.id.close).setOnClickListener { + actionMenu.visibility = View.GONE + textView.visibility = View.VISIBLE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/ottop/yamlauncher/AppMenuActivity.kt b/app/src/main/java/eu/ottop/yamlauncher/AppMenuActivity.kt index add5dc1..39ef5ba 100644 --- a/app/src/main/java/eu/ottop/yamlauncher/AppMenuActivity.kt +++ b/app/src/main/java/eu/ottop/yamlauncher/AppMenuActivity.kt @@ -1,41 +1,208 @@ package eu.ottop.yamlauncher import android.content.Context -import android.content.Intent -import android.content.pm.ApplicationInfo import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps -import android.content.res.ColorStateList -import android.graphics.Color -import android.net.Uri import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.os.UserHandle -import android.view.Gravity import android.view.LayoutInflater import android.view.View -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.webkit.RenderProcessGoneDetail -import android.widget.EditText -import android.widget.ImageView +import android.view.ViewGroup import android.widget.LinearLayout -import android.widget.PopupWindow import android.widget.SearchView import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.core.graphics.drawable.toDrawable -import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import eu.ottop.yamlauncher.databinding.ActivityAppMenuBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -class AppMenuActivity : AppCompatActivity() { +class AppMenuActivity : AppCompatActivity(), AppMenuAdapter.OnItemClickListener, AppMenuAdapter.OnItemLongClickListener { + private lateinit var binding: ActivityAppMenuBinding + private lateinit var recyclerView: RecyclerView + private lateinit var searchView: SearchView + private lateinit var adapter: AppMenuAdapter + private lateinit var shownApps: List>> + private lateinit var job: Job + private var appActionMenu = AppActionMenu() + private lateinit var launcherApps: LauncherApps + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityAppMenuBinding.inflate(layoutInflater) + setContentView(binding.root) + setSupportActionBar(null) + launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + searchView = findViewById(R.id.searchView) + + recyclerView = findViewById(R.id.recycler_view) + recyclerView.layoutManager = LinearLayoutManager(this) + shownApps = getInstalledApps() + adapter = AppMenuAdapter(shownApps, this, this) + recyclerView.adapter = adapter + } + + override fun onItemClick(appInfo: LauncherActivityInfo, userHandle: UserHandle) { + val mainActivity = launcherApps.getActivityList(appInfo.applicationInfo.packageName, userHandle).firstOrNull() + if (mainActivity != null) { + launcherApps.startMainActivity(mainActivity.componentName, userHandle, null, null) + } else { + // Handle the case when launch intent is null (e.g., app cannot be launched) + Toast.makeText(this, "Cannot launch app", Toast.LENGTH_SHORT).show() + } + } + + override fun onItemLongClick( + appInfo: LauncherActivityInfo, + userHandle: UserHandle, + userProfile: Int, + textView: TextView, + actionMenuLayout: LinearLayout, + editView: LinearLayout + ) { + // Handle the long click action here, for example, show additional options or information about the app + textView.visibility = View.GONE + actionMenuLayout.visibility = View.VISIBLE + val mainActivity = launcherApps.getActivityList(appInfo.applicationInfo.packageName, userHandle).firstOrNull() + appActionMenu.setActionListeners(this@AppMenuActivity, CoroutineScope(Dispatchers.Main), binding, textView, editView, actionMenuLayout, searchView, appInfo.applicationInfo, userHandle, userProfile, launcherApps, mainActivity) + + } + + private fun getInstalledApps(): List>> { + val allApps = mutableListOf>>() + val launcherApps = getSystemService(LAUNCHER_APPS_SERVICE) as LauncherApps + for (i in launcherApps.profiles.indices) { + launcherApps.getActivityList(null, launcherApps.profiles[i]).forEach { app -> + allApps.add(Pair(app, Pair(launcherApps.profiles[i], i))) + } + } + return allApps.sortedBy { + it.first.applicationInfo.loadLabel(packageManager).toString().lowercase() + } + } + + override fun onStop() { + super.onStop() + job.cancel() + + } + + override fun onStart() { + super.onStart() + startTask() + } + + private fun startTask() { + job = CoroutineScope(Dispatchers.IO).launch { + while (true) { + if (!listsEqual(shownApps, getInstalledApps())) { + shownApps = getInstalledApps() + withContext(Dispatchers.Main) { + adapter.updateApps(shownApps) + } + } + delay(5000) + } + } + } + + private fun listsEqual( + list1: List>>, + list2: List>> + ): Boolean { + if (list1.size != list2.size) return false + + for (i in list1.indices) { + if (list1[i].first.componentName != list2[i].first.componentName || list1[i].second.first != list2[i].second.first) { + return false + } + } + + return true + } + +} + + class AppMenuAdapter(private var apps: List>>, private val itemClickListener: OnItemClickListener, private val itemLongClickListener: OnItemLongClickListener) : + RecyclerView.Adapter() { + + interface OnItemClickListener { + fun onItemClick(appInfo: LauncherActivityInfo, userHandle: UserHandle) + } + + interface OnItemLongClickListener { + fun onItemLongClick( + appInfo: LauncherActivityInfo, + userHandle: UserHandle, + userProfile: Int, + textView: TextView, + actionMenuLayout: LinearLayout, + editView: LinearLayout + ) + } + + inner class AppViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val textView: TextView = itemView.findViewById(R.id.app_name) + + init { + val actionMenuLayout = LayoutInflater.from(itemView.context) + .inflate(R.layout.app_action_menu, itemView.findViewById(R.id.list_item), false) as LinearLayout + val editView = LayoutInflater.from(itemView.context) + .inflate(R.layout.rename_view, itemView.findViewById(R.id.list_item), false) as LinearLayout + val parentLayout: ViewGroup = itemView.findViewById(R.id.list_item) + parentLayout.addView(actionMenuLayout) + parentLayout.addView(editView) + actionMenuLayout.visibility = View.GONE + editView.visibility = View.GONE + itemView.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + val app = apps[position].first + itemClickListener.onItemClick(app, apps[position].second.first) + } + } + itemView.setOnLongClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + val app = apps[position].first + itemLongClickListener.onItemLongClick(app, apps[position].second.first, apps[position].second.second, textView, actionMenuLayout, editView) + return@setOnLongClickListener true + } + false + } + } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.app_item_layout, parent, false) + return AppViewHolder(view) + } + + override fun onBindViewHolder(holder: AppViewHolder, position: Int) { + val app = apps[position] + val appInfo = app.first.activityInfo.applicationInfo + holder.textView.text = appInfo.loadLabel(holder.itemView.context.packageManager) + } + + override fun getItemCount(): Int { + return apps.size + } + + fun updateApps(newApps: List>>) { + apps = newApps + notifyDataSetChanged() + } + } + +/* private lateinit var binding: ActivityAppMenuBinding private lateinit var searchView: SearchView private lateinit var container: LinearLayout @@ -415,4 +582,4 @@ class AppMenuActivity : AppCompatActivity() { val key = "$packageName-$profile" return sharedPreferences.getString(key, appName.toString()) } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/app/src/main/java/eu/ottop/yamlauncher/SharedPreferenceManager.kt b/app/src/main/java/eu/ottop/yamlauncher/SharedPreferenceManager.kt new file mode 100644 index 0000000..7c08923 --- /dev/null +++ b/app/src/main/java/eu/ottop/yamlauncher/SharedPreferenceManager.kt @@ -0,0 +1,48 @@ +package eu.ottop.yamlauncher + +import android.content.Context +import androidx.appcompat.app.AppCompatActivity + +class SharedPreferenceManager { + + fun setAppHidden(cont: Context, packageName: String, profile: Int, hidden: Boolean) { + val editor = cont.getSharedPreferences("hidden_apps", AppCompatActivity.MODE_PRIVATE).edit() + val key = "$packageName-$profile" + editor.putBoolean(key, hidden) + editor.apply() + } + + fun isAppHidden(cont: Context, packageName: String, profile: Int): Boolean { + val sharedPref = cont.getSharedPreferences("hidden_apps", AppCompatActivity.MODE_PRIVATE) + val key = "$packageName-$profile" + return sharedPref.getBoolean(key, false) // Default to false (visible) + } + + fun setAppVisible(cont: Context, packageName: String, profile: Int) { + val editor = cont.getSharedPreferences("hidden_apps", AppCompatActivity.MODE_PRIVATE).edit() + val key = "$packageName-$profile" + editor.remove(key) + editor.apply() + } + + fun setAppName(cont: Context, packageName: String, profile: Int, newName: String) { + val editor = cont.getSharedPreferences("renamed_apps", AppCompatActivity.MODE_PRIVATE).edit() + val key = "$packageName-$profile" + editor.putString(key, newName) + editor.apply() + } + + fun getAppName(cont: Context, packageName: String, profile: Int, appName: CharSequence): CharSequence? { + val sharedPreferences = cont.getSharedPreferences("renamed_apps", AppCompatActivity.MODE_PRIVATE) + val key = "$packageName-$profile" + return sharedPreferences.getString(key, appName.toString()) + } + + fun resetAppName(cont: Context, packageName: String, profile: Int) { + val editor = cont.getSharedPreferences("renamed_apps", AppCompatActivity.MODE_PRIVATE).edit() + val key = "$packageName-$profile" + editor.remove(key) + editor.apply() + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_app_menu.xml b/app/src/main/res/layout/activity_app_menu.xml index 966643c..a5eaf2b 100644 --- a/app/src/main/res/layout/activity_app_menu.xml +++ b/app/src/main/res/layout/activity_app_menu.xml @@ -1,39 +1,21 @@ + android:layout_height="match_parent" + android:orientation="vertical"> - - + android:layout_weight="1" + android:clipToPadding="false" + android:padding="0dp" + android:scrollbars="none" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"> - - + - \ No newline at end of file + + diff --git a/app/src/main/res/layout/app_action_menu.xml b/app/src/main/res/layout/app_action_menu.xml index 5dd27c9..39ed877 100644 --- a/app/src/main/res/layout/app_action_menu.xml +++ b/app/src/main/res/layout/app_action_menu.xml @@ -1,51 +1,69 @@ + android:gravity="center"> + android:textColor="#F3F3F3FF" + app:drawableTopCompat="@android:drawable/ic_menu_info_details" /> + android:textColor="#F3F3F3FF" + app:drawableTopCompat="@android:drawable/ic_menu_delete" /> + android:textColor="#F3F3F3FF" + app:drawableTopCompat="@android:drawable/ic_menu_edit" /> + android:textColor="#F3F3F3FF" + app:drawableTopCompat="@android:drawable/ic_menu_view" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/app_item_layout.xml b/app/src/main/res/layout/app_item_layout.xml new file mode 100644 index 0000000..d434943 --- /dev/null +++ b/app/src/main/res/layout/app_item_layout.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/layout/rename_view.xml b/app/src/main/res/layout/rename_view.xml new file mode 100644 index 0000000..cb8c947 --- /dev/null +++ b/app/src/main/res/layout/rename_view.xml @@ -0,0 +1,20 @@ + + + + + +