diff --git a/app/src/main/java/eu/ottop/yamlauncher/AppActionMenu.kt b/app/src/main/java/eu/ottop/yamlauncher/AppActionMenu.kt index 50df8c2..0be33fe 100644 --- a/app/src/main/java/eu/ottop/yamlauncher/AppActionMenu.kt +++ b/app/src/main/java/eu/ottop/yamlauncher/AppActionMenu.kt @@ -17,6 +17,7 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.widget.AppCompatButton import eu.ottop.yamlauncher.databinding.ActivityAppMenuBinding +import eu.ottop.yamlauncher.databinding.ActivityMainBinding import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -26,9 +27,9 @@ class AppActionMenu { private val appUtils = AppUtils() fun setActionListeners( - activity: AppMenuActivity, + activity: MainActivity, uiScope: CoroutineScope, - binding: ActivityAppMenuBinding, + binding: ActivityMainBinding, textView: TextView, editLayout: LinearLayout, actionMenu: View, diff --git a/app/src/main/java/eu/ottop/yamlauncher/AppMenuActivity.kt b/app/src/main/java/eu/ottop/yamlauncher/AppMenuActivity.kt index 6f1f58c..aaf9d22 100644 --- a/app/src/main/java/eu/ottop/yamlauncher/AppMenuActivity.kt +++ b/app/src/main/java/eu/ottop/yamlauncher/AppMenuActivity.kt @@ -15,26 +15,24 @@ import android.widget.EditText import android.widget.LinearLayout import android.widget.TextView import android.widget.Toast +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity -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.ensureActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class AppMenuActivity : AppCompatActivity(), AppMenuAdapter.OnItemClickListener, AppMenuAdapter.OnShortcutListener, AppMenuAdapter.OnItemLongClickListener { - + /* private lateinit var binding: ActivityAppMenuBinding private lateinit var recyclerView: RecyclerView private lateinit var searchView: EditText private lateinit var adapter: AppMenuAdapter - private lateinit var filteredApps: MutableList>> private var job: Job? = null private var appActionMenu = AppActionMenu() private lateinit var launcherApps: LauncherApps @@ -79,14 +77,19 @@ class AppMenuActivity : AppCompatActivity(), AppMenuAdapter.OnItemClickListener, recyclerView = findViewById(R.id.recycler_view) recyclerView.scrollToPosition(0) installedApps = currentApps - filteredApps = mutableListOf() - filteredApps.addAll(installedApps) val newApps = mutableListOf>>() newApps.addAll(installedApps) adapter = AppMenuAdapter(this@AppMenuActivity, newApps, this, this,this, menuMode) recyclerView.adapter = adapter setupSearch() + + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finish() + + } + }) } override fun onItemClick(appInfo: LauncherActivityInfo, userHandle: UserHandle) { @@ -218,7 +221,6 @@ class AppMenuActivity : AppCompatActivity(), AppMenuAdapter.OnItemClickListener, private fun startTask() { job = CoroutineScope(Dispatchers.Default).launch { while (true) { - ensureActive() manualRefresh() delay(5000) } @@ -317,5 +319,32 @@ class AppMenuActivity : AppCompatActivity(), AppMenuAdapter.OnItemClickListener, data class Change(val type: ChangeType, val position: Int, val newPosition: Int = 0) enum class ChangeType { - INSERT, REMOVE, UPDATE + INSERT, REMOVE, UPDATE*/ + override fun onItemClick(appInfo: LauncherActivityInfo, userHandle: UserHandle) { + TODO("Not yet implemented") + } + + + + override fun onItemLongClick( + appInfo: LauncherActivityInfo, + userHandle: UserHandle, + userProfile: Int, + textView: TextView, + actionMenuLayout: LinearLayout, + editView: LinearLayout, + position: Int + ) { + TODO("Not yet implemented") + } + + override fun onShortcut( + appInfo: LauncherActivityInfo, + userHandle: UserHandle, + textView: TextView, + userProfile: Int, + shortcutView: TextView + ) { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/app/src/main/java/eu/ottop/yamlauncher/AppMenuAdapter.kt b/app/src/main/java/eu/ottop/yamlauncher/AppMenuAdapter.kt index 612b632..bb4df52 100644 --- a/app/src/main/java/eu/ottop/yamlauncher/AppMenuAdapter.kt +++ b/app/src/main/java/eu/ottop/yamlauncher/AppMenuAdapter.kt @@ -14,15 +14,17 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.widget.AppCompatButton import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.RecyclerView class AppMenuAdapter( - private val activity: AppMenuActivity, + private val activity: FragmentActivity, var apps: MutableList>>, private val itemClickListener: OnItemClickListener, private val shortcutListener: OnShortcutListener, private val itemLongClickListener: OnItemLongClickListener, - private val menuMode: String + private val menuMode: String = "app", + private val shortcutTextView: TextView? = null ) : RecyclerView.Adapter() { @@ -33,7 +35,7 @@ class AppMenuAdapter( } interface OnShortcutListener { - fun onShortcut(appInfo: LauncherActivityInfo, userHandle: UserHandle, textView: TextView, userProfile: Int) + fun onShortcut(appInfo: LauncherActivityInfo, userHandle: UserHandle, textView: TextView, userProfile: Int, shortcutView: TextView) } interface OnItemLongClickListener { @@ -62,7 +64,7 @@ class AppMenuAdapter( val position = bindingAdapterPosition val app = apps[position].first if (menuMode == "shortcut") { - shortcutListener.onShortcut(app, apps[position].second.first, textView, apps[position].second.second) + shortcutListener.onShortcut(app, apps[position].second.first, textView, apps[position].second.second, shortcutTextView!!) } else if (menuMode == "app") { itemClickListener.onItemClick(app, apps[position].second.first) diff --git a/app/src/main/java/eu/ottop/yamlauncher/MainActivity.kt b/app/src/main/java/eu/ottop/yamlauncher/MainActivity.kt index c78a326..c151741 100644 --- a/app/src/main/java/eu/ottop/yamlauncher/MainActivity.kt +++ b/app/src/main/java/eu/ottop/yamlauncher/MainActivity.kt @@ -5,27 +5,43 @@ import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps import android.os.Bundle import android.os.UserHandle +import android.text.Editable +import android.text.TextWatcher +import android.util.Log import android.view.GestureDetector import android.view.MotionEvent +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.EditText import android.widget.LinearLayout import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.res.ResourcesCompat import androidx.core.view.children +import androidx.recyclerview.widget.RecyclerView import eu.ottop.yamlauncher.databinding.ActivityMainBinding import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlin.math.abs -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), AppMenuAdapter.OnItemClickListener, AppMenuAdapter.OnShortcutListener, AppMenuAdapter.OnItemLongClickListener { private lateinit var binding: ActivityMainBinding private lateinit var gestureDetector: GestureDetector private lateinit var launcherApps: LauncherApps private lateinit var installedApps: List>> + private lateinit var recyclerView: RecyclerView + private lateinit var searchView: EditText + private lateinit var adapter: AppMenuAdapter + private var job: Job? = null + private var appActionMenu = AppActionMenu() + private val sharedPreferenceManager = SharedPreferenceManager() private val appUtils = AppUtils() @@ -36,9 +52,6 @@ class MainActivity : AppCompatActivity() { setSupportActionBar(null) launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - CoroutineScope(Dispatchers.Default).launch { - installedApps = appUtils.getInstalledApps(this@MainActivity) - } for (i in findViewById(R.id.shortcuts).children) { @@ -74,35 +87,122 @@ class MainActivity : AppCompatActivity() { } i.setOnLongClickListener { - AppMenuActivity.start(this@MainActivity, installedApps, "shortcut") { newText -> + binding.homeView.visibility = View.GONE - if (newText.first.second != 0) { - textView.setCompoundDrawablesWithIntrinsicBounds(ResourcesCompat.getDrawable(resources, R.drawable.ic_work_app, null),null,null,null) - } - else { - textView.setCompoundDrawablesWithIntrinsicBounds(ResourcesCompat.getDrawable(resources, R.drawable.ic_empty, null),null,null,null) - } - textView.text = newText.first.first - i.setOnClickListener { - val mainActivity = launcherApps.getActivityList(newText.second.first.applicationInfo.packageName, newText.second.second).firstOrNull() - if (mainActivity != null) { - launcherApps.startMainActivity(mainActivity.componentName, newText.second.second, null, null) - } else { - Toast.makeText(this, "Cannot launch app", Toast.LENGTH_SHORT).show() - } - } - sharedPreferenceManager.setShortcut(this, textView, newText.second.first.applicationInfo.packageName, newText.first.second) - } + val newApps = mutableListOf>>() + newApps.addAll(installedApps) + adapter = AppMenuAdapter(this@MainActivity, newApps, this@MainActivity, this@MainActivity, this@MainActivity, "shortcut", i) + recyclerView.adapter = adapter + binding.appView.visibility = View.VISIBLE return@setOnLongClickListener true - } - } + }} + gestureDetector = GestureDetector(this, GestureListener()) + + //Experimental + CoroutineScope(Dispatchers.Default).launch { + installedApps = appUtils.getInstalledApps(this@MainActivity) + + recyclerView = findViewById(R.id.recycler_view) + recyclerView.scrollToPosition(0) + + searchView = findViewById(R.id.searchView) + setupSearch() + } } + private fun setupSearch() { + binding.root.addOnLayoutChangeListener { _, _, top, _, bottom, _, oldTop, _, oldBottom -> + if (bottom - top > oldBottom - oldTop) { + searchView.clearFocus() + if (searchView.text.isNullOrEmpty()) { + job?.cancel() + startTask() + } + } + else { + job?.cancel() + } + } + searchView.addTextChangedListener(object : + TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + filterItems(searchView.text.toString()) + } + + override fun afterTextChanged(s: Editable?) { + } + + }) + } + + private fun filterItems(query: String?) { + CoroutineScope(Dispatchers.Default).launch { + val cleanQuery = query?.clean() + val newFilteredApps = mutableListOf>>() + val updatedApps = appUtils.getInstalledApps(this@MainActivity) + + if (cleanQuery.isNullOrEmpty()) { + manualRefresh() + newFilteredApps.addAll(installedApps) + } else { + updatedApps.forEach { + val cleanItemText = sharedPreferenceManager.getAppName(this@MainActivity, it.first.applicationInfo.packageName, it.second.second, it.first.applicationInfo.loadLabel(packageManager)).toString().clean() + if (cleanItemText.contains(cleanQuery, ignoreCase = true)) { + newFilteredApps.add(it) + } + } + } + + val changes = detectChanges(installedApps, newFilteredApps) + installedApps = newFilteredApps + withContext(Dispatchers.Main) { + applyChanges(changes, installedApps) + } + } + + } + + private fun String.clean(): String { + return this.replace("[^a-zA-Z0-9]".toRegex(), "") + } + + override fun onStop() { + super.onStop() + job?.cancel() + + } + + override fun onDestroy() { + super.onDestroy() + job?.cancel() + } + + override fun onStart() { + super.onStart() + startTask() + + // Keyboard is sometimes open when going back to the app, so close it. + val imm = + getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(binding.root.windowToken, 0) + } + + private fun startTask() { + job = CoroutineScope(Dispatchers.Default).launch { + while (true) { + manualRefresh() + delay(5000) + } + } + } override fun onTouchEvent(event: MotionEvent): Boolean { gestureDetector.onTouchEvent(event) @@ -134,9 +234,178 @@ class MainActivity : AppCompatActivity() { } fun openAppMenuActivity() { - AppMenuActivity.start(this, installedApps) { + //AppMenuActivity.start(this, installedApps) { + //} + binding.homeView.visibility = View.GONE + val newApps = mutableListOf>>() + newApps.addAll(installedApps) + adapter = AppMenuAdapter(this@MainActivity, newApps, this@MainActivity, this@MainActivity, this@MainActivity, "app") + recyclerView.adapter = adapter + binding.appView.visibility = View.VISIBLE + } + + 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 { + Toast.makeText(this, "Cannot launch app", Toast.LENGTH_SHORT).show() } } + override fun onShortcut( + appInfo: LauncherActivityInfo, + userHandle: UserHandle, + textView: TextView, + userProfile: Int, + shortcutView: TextView + ) { + if (userProfile != 0) { + shortcutView.setCompoundDrawablesWithIntrinsicBounds(ResourcesCompat.getDrawable(resources, R.drawable.ic_work_app, null),null,null,null) + } + else { + shortcutView.setCompoundDrawablesWithIntrinsicBounds(ResourcesCompat.getDrawable(resources, R.drawable.ic_empty, null),null,null,null) + } + shortcutView.text = textView.text.toString() + shortcutView.setOnClickListener { + val mainActivity = launcherApps.getActivityList(appInfo.applicationInfo.packageName, userHandle).firstOrNull() + if (mainActivity != null) { + launcherApps.startMainActivity(mainActivity.componentName, userHandle, null, null) + } else { + Toast.makeText(this, "Cannot launch app", Toast.LENGTH_SHORT).show() + } + } + sharedPreferenceManager.setShortcut(this, shortcutView, appInfo.applicationInfo.packageName, userProfile) + binding.appView.visibility = View.GONE + binding.homeView.visibility = View.VISIBLE + } + + + override fun onItemLongClick( + appInfo: LauncherActivityInfo, + userHandle: UserHandle, + userProfile: Int, + textView: TextView, + actionMenuLayout: LinearLayout, + editView: LinearLayout, + position: Int + ) { + textView.visibility = View.INVISIBLE + actionMenuLayout.visibility = View.VISIBLE + val mainActivity = + launcherApps.getActivityList(appInfo.applicationInfo.packageName, userHandle) + .firstOrNull() + appActionMenu.setActionListeners( + this@MainActivity, + CoroutineScope(Dispatchers.Main), + binding, + textView, + editView, + actionMenuLayout, + searchView, + appInfo.applicationInfo, + userHandle, + userProfile, + launcherApps, + mainActivity, + position + ) + } + + fun manualRefresh() { + CoroutineScope(Dispatchers.Default).launch { + try { + val updatedApps = appUtils.getInstalledApps(this@MainActivity) + val changes = detectChanges(installedApps, updatedApps) + installedApps = updatedApps + withContext(Dispatchers.Main) { + applyChanges(changes, installedApps) + } + } + catch (_: UninitializedPropertyAccessException) { + } + } + } + + private fun detectChanges(oldList: List>>, newList: List>>): List { + val changes = mutableListOf() + val removalChanges = mutableListOf() + val oldSet = oldList.map { Pair(it.first.applicationInfo.packageName, it.second.second) }.toSet() + val newSet = newList.map { Pair(it.first.applicationInfo.packageName, it.second.second) }.toSet() + + //Detect updates + oldList.forEachIndexed { index, oldItem -> + if (newSet.contains(Pair(oldItem.first.applicationInfo.packageName, oldItem.second.second))) { + val newIndex = newList.indexOfFirst { it.first.applicationInfo.packageName == oldItem.first.applicationInfo.packageName && it.second.second == oldItem.second.second } + if (oldItem.first.componentName != newList[newIndex].first.componentName) { + changes.add(Change(ChangeType.UPDATE, index)) + } + + } + } + + // Detect insertions + newList.forEachIndexed { index, newItem -> + if (!oldSet.contains(Pair(newItem.first.applicationInfo.packageName, newItem.second.second))) { + changes.add(Change(ChangeType.INSERT, index)) + } + } + + // Detect removals + oldList.forEachIndexed { index, oldItem -> + if (!newSet.contains(Pair(oldItem.first.applicationInfo.packageName, oldItem.second.second))) { + removalChanges.add(Change(ChangeType.REMOVE, index)) + } + } + + changes.addAll(removalChanges.reversed()) + + return changes + } + + private fun applyChanges(changes: List, updatedApps: List>>) { + changes.forEach { change -> + when (change.type) { + ChangeType.INSERT -> { + insertItem(change.position, updatedApps[change.position]) + } + ChangeType.REMOVE -> { + try { + removeItem(change.position) + } + catch (_: IndexOutOfBoundsException) { + } + } + ChangeType.UPDATE -> { + updateItem(change.position, updatedApps[change.position]) + } + } + } + } + + private fun insertItem(position: Int, app: Pair>) { + adapter.addApp(position, app) + adapter.notifyItemInserted(position) + } + private fun removeItem(position: Int) { + adapter.removeApp(position) + adapter.notifyItemRemoved(position) + } + + fun updateItem(position: Int, app: Pair>) { + adapter.updateApp(position, app) + adapter.notifyItemChanged(position) + } + + fun moveItem(position: Int, newPosition: Int) { + Log.d("Movestatus","MOVED") + adapter.moveApp(position, newPosition) + adapter.notifyItemMoved(position, newPosition) + } } +data class Change(val type: ChangeType, val position: Int, val newPosition: Int = 0) + +enum class ChangeType { + INSERT, REMOVE, UPDATE +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3d3ae5a..43fa827 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,37 +1,29 @@ - - - + android:layout_height="match_parent" + android:gravity="bottom" + android:orientation="vertical" + android:visibility="gone"> + + + android:textColor="#C1F3F3F3" + android:textSize="36sp" + android:visibility="gone" /> - + android:layout_height="0dp" + android:layout_weight="1" + android:clipToPadding="false" + android:fadingEdgeLength="20dp" + android:padding="0dp" + android:requiresFadingEdge="vertical" + android:scrollbars="none" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:stackFromEnd="true"> - + - + android:textSize="25sp" /> - - - - - - - + android:layout_height="match_parent" + android:layout_gravity="fill_vertical" + android:fitsSystemWindows="true" + android:gravity="fill_vertical" + android:orientation="vertical"> - + + + + + + + + + + + + + + + + + + + + + + + + + + +