Feat: Basic starter to add Widget support.

This commit is contained in:
HeCodes2Much
2024-05-22 16:43:09 +01:00
parent 364fe2bb7c
commit a3c4e6f0af
16 changed files with 382 additions and 10 deletions

13
.vscode/easycode.ignore vendored Normal file
View File

@@ -0,0 +1,13 @@
node_modules/
dist/
vendor/
cache/
.*/
*.min.*
*.test.*
*.spec.*
*.bundle.*
*.bundle-min.*
*.*.js
*.*.ts
*.log

View File

@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
AsterLauncher Copyright (C) 2024 neophtex
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

View File

@@ -51,6 +51,7 @@ android {
}
buildFeatures {
viewBinding = true
dataBinding = true
}
applicationVariants.all {

View File

@@ -13,6 +13,10 @@
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<uses-permission android:name="android.permission.BIND_APPWIDGET"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<queries>
<intent>

View File

@@ -4,6 +4,9 @@ object Constants {
const val PACKAGE_NAME = "app.easy.launcher"
const val PACKAGE_NAME_DEBUG = "$PACKAGE_NAME.debug"
const val WIDGETS_PREFS = "EasyLauncherWidgets.pref"
const val APP_WIDGETS_ID = "APP_WIDGETS_ID"
const val PREFS_FILENAME = "EasyLauncher.pref"
const val FIRST_LAUNCH = "FIRST_LAUNCH"
const val SHOW_DATE = "SHOW_DATE"
@@ -39,8 +42,6 @@ object Constants {
const val HOME_TIME_ALIGNMENT = "HOME_TIME_ALIGNMENT"
const val HOME_APP_ALIGNMENT = "HOME_APP_ALIGNMENT"
const val HOME_DAILY_WORD_ALIGNMENT = "HOME_DAILY_WORD_ALIGNMENT"
const val HOME_DRAW_ALIGNMENT = "HOME_DRAW_ALIGNMENT"
const val HOME_BATTERY_ALIGNMENT = "HOME_BATTERY_ALIGNMENT"
const val NOTIFICATION_SERVICE = "statusbar"
const val NOTIFICATION_MANAGER = "android.app.StatusBarManager"
@@ -61,5 +62,6 @@ object Constants {
const val URL_GOOGLE_PLAY_STORE = "https://play.google.com/store/search?c=apps&q"
const val APP_GOOGLE_PLAY_STORE = "market://search?c=apps&q"
const val APP_WIDGET_HOST_ID = 1024
const val REQUEST_CODE_ENABLE_ADMIN = 123
}

View File

@@ -101,11 +101,11 @@ class AppHelper @Inject constructor() {
fun dayNightMod(context: Context, view: View) {
when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> {
view.setBackgroundColor(context.resources.getColor(R.color.blackTrans50, context.theme))
view.setBackgroundColor(context.resources.getColor(R.color.blackTrans25, context.theme))
}
Configuration.UI_MODE_NIGHT_NO -> {
view.setBackgroundColor(context.resources.getColor(R.color.whiteTrans50, context.theme))
view.setBackgroundColor(context.resources.getColor(R.color.whiteTrans25, context.theme))
}
}
}

View File

@@ -78,7 +78,7 @@ class MainActivity : AppCompatActivity() {
}
private fun observeUI() {
binding.pager.currentItem = 0
binding.pager.currentItem = 1
preferenceViewModel.setShowStatusBar(preferenceHelper.showStatusBar)
preferenceViewModel.showStatusBarLiveData.observe(this) {
if (it) appHelper.showStatusBar(this.window)

View File

@@ -40,7 +40,7 @@ class DrawFragment : Fragment(), OnItemClickedListener.OnAppsClickedListener,
OnItemClickedListener.OnAppLongClickedListener,
OnItemClickedListener.BottomSheetDismissListener,
OnItemClickedListener.OnAppStateClickListener,
FingerprintHelper.Callback{
FingerprintHelper.Callback {
private var _binding: FragmentDrawBinding? = null
private val binding get() = _binding!!
@@ -181,14 +181,12 @@ class DrawFragment : Fragment(), OnItemClickedListener.OnAppsClickedListener,
override fun onResume() {
super.onResume()
observeDrawerApps()
binding.drawAdapter.scrollToPosition(0)
if (preferenceHelper.automaticKeyboard) binding.searchViewText.showKeyboard()
}
override fun onStart() {
super.onStart()
observeDrawerApps()
binding.drawAdapter.scrollToPosition(0)
}
override fun onStop() {

View File

@@ -6,12 +6,14 @@ import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.github.droidworksstudio.launcher.ui.drawer.DrawFragment
import com.github.droidworksstudio.launcher.ui.home.HomeFragment
import com.github.droidworksstudio.launcher.ui.widgetmanager.WidgetManagerFragment
class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
FragmentStateAdapter(fragmentManager, lifecycle) {
private val fragments: ArrayList<Fragment> = arrayListOf(
WidgetManagerFragment(),
HomeFragment(),
DrawFragment(),
)
@@ -23,5 +25,4 @@ class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
override fun createFragment(position: Int): Fragment {
return fragments[position]
}
}

View File

@@ -0,0 +1,58 @@
package com.github.droidworksstudio.launcher.ui.widgetmanager
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.recyclerview.widget.RecyclerView
import com.github.droidworksstudio.launcher.R
class WidgetAdapter(
private val context: Context,
private val appWidgetHost: AppWidgetHost,
private val onWidgetClick: (Int) -> Unit
) : RecyclerView.Adapter<WidgetAdapter.WidgetViewHolder>() {
private val widgetItems = mutableListOf<WidgetItem>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WidgetViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_widget, parent, false)
return WidgetViewHolder(itemView)
}
override fun onBindViewHolder(holder: WidgetViewHolder, position: Int) {
val widgetItem = widgetItems[position]
holder.bind(widgetItem)
}
override fun getItemCount(): Int = widgetItems.size
fun addWidget(widgetItem: WidgetItem) {
widgetItems.add(widgetItem)
notifyItemInserted(widgetItems.size - 1)
}
fun removeWidget(appWidgetId: Int) {
val position = widgetItems.indexOfFirst { it.appWidgetId == appWidgetId }
if (position != -1) {
widgetItems.removeAt(position)
notifyItemRemoved(position)
}
}
fun getWidgetIds(): List<Int> = widgetItems.map { it.appWidgetId }
inner class WidgetViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(widgetItem: WidgetItem) {
val hostView = appWidgetHost.createView(context, widgetItem.appWidgetId, widgetItem.appWidgetInfo)
itemView.findViewById<FrameLayout>(R.id.widget_frame).addView(hostView)
itemView.setOnClickListener {
onWidgetClick(widgetItem.appWidgetId)
}
}
}
}

View File

@@ -0,0 +1,5 @@
package com.github.droidworksstudio.launcher.ui.widgetmanager
import android.appwidget.AppWidgetProviderInfo
data class WidgetItem(val appWidgetId: Int, val appWidgetInfo: AppWidgetProviderInfo?)

View File

@@ -0,0 +1,182 @@
package com.github.droidworksstudio.launcher.ui.widgetmanager
import android.app.Activity
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.droidworksstudio.launcher.Constants
import com.github.droidworksstudio.launcher.databinding.FragmentWidgetManagerBinding
import com.github.droidworksstudio.launcher.helper.AppHelper
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* A simple [Fragment] subclass as the second destination in the navigation.
*/
@AndroidEntryPoint
class WidgetManagerFragment : Fragment(), WidgetOptionsDialogFragment.WidgetOptionsListener {
private var _binding: FragmentWidgetManagerBinding? = null
private val binding get() = _binding!!
private lateinit var pickWidgetLauncher: ActivityResultLauncher<Intent>
private lateinit var createWidgetLauncher: ActivityResultLauncher<Intent>
private lateinit var appWidgetManager: AppWidgetManager
private lateinit var appWidgetHost: AppWidgetHost
private lateinit var widgetAdapter: WidgetAdapter
@Inject
lateinit var appHelper: AppHelper
private lateinit var context: Context
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentWidgetManagerBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
appHelper.dayNightMod(requireContext(), binding.drawBackground)
super.onViewCreated(view, savedInstanceState)
context = requireContext()
appWidgetManager = AppWidgetManager.getInstance(requireContext())
appWidgetHost = AppWidgetHost(requireContext(), Constants.APP_WIDGET_HOST_ID)
widgetAdapter = WidgetAdapter(requireContext(), appWidgetHost) { appWidgetId ->
showWidgetOptionsDialog(appWidgetId)
}
binding.widgetContainer.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = widgetAdapter
}
// Load saved widgets
val savedWidgetIds = loadWidgetIds()
for (widgetId in savedWidgetIds) {
addWidget(widgetId)
}
// Initialize the ActivityResultLaunchers
pickWidgetLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
data?.let {
val appWidgetId = it.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (appWidgetId != -1) {
val appWidgetInfo = appWidgetManager.getAppWidgetInfo(appWidgetId)
if (appWidgetInfo?.configure != null) {
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
intent.component = appWidgetInfo.configure
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
createWidgetLauncher.launch(intent)
} else {
addWidget(appWidgetId)
}
}
}
} else if (result.resultCode == Activity.RESULT_CANCELED) {
val appWidgetId = result.data?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (appWidgetId != null && appWidgetId != -1) {
appWidgetHost.deleteAppWidgetId(appWidgetId)
}
}
}
createWidgetLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
data?.let {
val appWidgetId = it.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (appWidgetId != -1) {
addWidget(appWidgetId)
}
}
} else if (result.resultCode == Activity.RESULT_CANCELED) {
val appWidgetId = result.data?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (appWidgetId != null && appWidgetId != -1) {
appWidgetHost.deleteAppWidgetId(appWidgetId)
}
}
}
// Set long-click listener on the root view
binding.widgetParent.setOnLongClickListener {
selectWidget()
true // Indicate that the long-click event has been handled
}
}
private fun selectWidget() {
val appWidgetId = appWidgetHost.allocateAppWidgetId()
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
pickWidgetLauncher.launch(intent)
}
private fun addWidget(appWidgetId: Int) {
val appWidgetInfo = appWidgetManager.getAppWidgetInfo(appWidgetId)
appWidgetInfo?.let {
val widgetItem = WidgetItem(appWidgetId, it)
widgetAdapter.addWidget(widgetItem)
saveWidgetIds(widgetAdapter.getWidgetIds()) // Save the updated widget IDs
} ?: Log.e("WidgetManagerFragment", "Failed to get app widget info for ID: $appWidgetId")
}
override fun onRemoveWidget(appWidgetId: Int) {
appWidgetHost.deleteAppWidgetId(appWidgetId)
widgetAdapter.removeWidget(appWidgetId)
saveWidgetIds(widgetAdapter.getWidgetIds())
}
private fun showWidgetOptionsDialog(appWidgetId: Int) {
val dialog = WidgetOptionsDialogFragment.newInstance(appWidgetId)
dialog.show(childFragmentManager, "WidgetOptionsDialog")
}
override fun onOpenAppWidget(appWidgetId: Int) {
// Implement the logic to open the widget's configuration or app
// This is typically dependent on the widget and its associated app
}
private fun saveWidgetIds(widgetIds: List<Int>) {
val sharedPreferences = requireContext().getSharedPreferences(Constants.WIDGETS_PREFS, Activity.MODE_PRIVATE)
val editor = sharedPreferences.edit()
val widgetIdsSet = widgetIds.map { it.toString() }.toSet()
editor.putStringSet(Constants.APP_WIDGETS_ID, widgetIdsSet)
editor.apply()
}
private fun loadWidgetIds(): List<Int> {
val sharedPreferences = requireContext().getSharedPreferences(Constants.WIDGETS_PREFS, Activity.MODE_PRIVATE)
val widgetIdsSet = sharedPreferences.getStringSet(Constants.APP_WIDGETS_ID, emptySet())
return widgetIdsSet?.map { it.toInt() } ?: emptyList()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,47 @@
package com.github.droidworksstudio.launcher.ui.widgetmanager
import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.github.droidworksstudio.launcher.Constants
import com.github.droidworksstudio.launcher.R
class WidgetOptionsDialogFragment : DialogFragment() {
interface WidgetOptionsListener {
fun onRemoveWidget(appWidgetId: Int)
fun onOpenAppWidget(appWidgetId: Int)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val appWidgetId = arguments?.getInt(ARG_APP_WIDGET_ID) ?: return super.onCreateDialog(savedInstanceState)
val builder = AlertDialog.Builder(requireContext())
builder.setTitle("Widget Options")
.setItems(arrayOf("Remove Widget", "Open Widget")) { _, which ->
val listener = parentFragment as? WidgetOptionsListener
when (which) {
0 -> listener?.onRemoveWidget(appWidgetId)
1 -> listener?.onOpenAppWidget(appWidgetId)
}
}
return builder.create()
}
companion object {
private const val ARG_APP_WIDGET_ID = Constants.APP_WIDGETS_ID
fun newInstance(appWidgetId: Int): WidgetOptionsDialogFragment {
val fragment = WidgetOptionsDialogFragment()
val args = Bundle().apply {
putInt(ARG_APP_WIDGET_ID, appWidgetId)
}
fragment.arguments = args
return fragment
}
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/draw_background"
tools:context=".ui.widgetmanager.WidgetManagerFragment">
<FrameLayout
android:id="@+id/widget_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:longClickable="true"
android:orientation="vertical"
android:padding="16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/widget_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"/>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/widget_frame"
android:padding="8dp"/>

View File

@@ -0,0 +1,29 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/button_open_app"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Open App"
android:textColor="?attr/colorPrimary"
android:backgroundTint="?attr/colorAccent"
app:backgroundTint="?attr/colorAccent"
app:rippleColor="?attr/colorAccent" />
<Button
android:id="@+id/button_remove_widget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Remove Widget"
android:textColor="?attr/colorPrimary"
android:backgroundTint="?attr/colorAccent"
app:backgroundTint="?attr/colorAccent"
app:rippleColor="?attr/colorAccent" />
</LinearLayout>