Skip to content

Commit 612d887

Browse files
feat(widgets): Implement Home, FAB, and Word of the Day widgets
This commit introduces three new home screen widgets: - **Home Apps Widget:** Displays a configurable list of user-selected applications. - Added `HomeAppsWidgetProvider.kt` to manage widget updates and layout. - Created `HomeAppUpdateReceiver.kt` to handle app launch clicks from the widget. - Added layouts `widget_home_apps.xml` and `item_home_app.xml`. - Added `widget_home_apps_info.xml` for widget metadata. - **FAB (Floating Action Button) Widget:** Provides quick access to common actions like phone, messages, camera, photos, browser, settings, and launching the main app. - Added `FabWidget.kt` and `FabClickReceiver.kt` for widget logic and click handling. - Created layout `widget_fab.xml`. - Added `widget_fab_info.xml` for widget metadata. - **Word of the Day Widget:** Displays a daily word. - Added `WordOfTheDayWidget.kt` for widget display. - Implemented `WordOfTheDayAlarm.kt` and `WordOfTheDayUpdateReceiver.kt` to schedule and handle daily updates at midnight. - Created layout `widget_word_of_the_day.xml`. - Added `word_of_the_day_widget_info.xml` for widget metadata. Changes also include: - Updated `AndroidManifest.xml` to declare new widget providers, receivers, services, and necessary permissions (`USE_EXACT_ALARM`, `SCHEDULE_EXACT_ALARM`, `BIND_REMOTEVIEWS`). - Modified icon drawables (`ic_settings.xml`, `ic_photos.xml`, `ic_messages.xml`, `ic_camera.xml`, `ic_browser.xml`, `ic_phone.xml`) to use a fixed white fill color (`#ffffff`) instead of `?attr/primaryColor`. - Added `startApp` function to `AppReloader.kt` to allow programmatic app restart. - Updated `ContextExtensions.kt` to add `FLAG_ACTIVITY_NEW_TASK` to intents for opening various system apps (Dialer, Alarm, Camera, Photos, Settings, Browser, Battery Manager) to ensure they can be launched from non-Activity contexts like widgets. - Added new string resources for widget names in `nontranslatable.xml`. Signed-off-by: CreativeCodeCat <wayne6324@gmail.com>
1 parent 36052db commit 612d887

23 files changed

+614
-26
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,19 @@
2323
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
2424
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
2525
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
26+
<uses-permission
27+
android:name="android.permission.USE_EXACT_ALARM"
28+
tools:ignore="ExactAlarmPolicy" />
29+
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
2630
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
2731
<uses-permission android:name="android.permission.READ_CONTACTS" />
2832
<uses-permission android:name="android.permission.VIBRATE" />
2933
<uses-permission
3034
android:name="android.permission.BIND_APPWIDGET"
3135
tools:ignore="ProtectedPermissions" />
36+
<uses-permission
37+
android:name="android.permission.BIND_REMOTEVIEWS"
38+
tools:ignore="ProtectedPermissions" />
3239
<uses-permission
3340
android:name="android.permission.FOREGROUND_SERVICE"
3441
tools:ignore="ForegroundServicesPolicy" />
@@ -130,5 +137,59 @@
130137
android:resource="@xml/accessibility_service_config" />
131138
</service>
132139

140+
<service
141+
android:name=".services.HomeAppsService"
142+
android:permission="android.permission.BIND_REMOTEVIEWS" />
143+
144+
<receiver
145+
android:name=".ui.widgets.wordoftheday.WordOfTheDayWidget"
146+
android:exported="false"
147+
android:label="@string/widget_wotd_name">
148+
<intent-filter>
149+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
150+
</intent-filter>
151+
<meta-data
152+
android:name="android.appwidget.provider"
153+
android:resource="@xml/word_of_the_day_widget_info" />
154+
</receiver>
155+
156+
<receiver
157+
android:name=".ui.widgets.wordoftheday.WordOfTheDayUpdateReceiver"
158+
android:exported="false"
159+
tools:ignore="Instantiatable" />
160+
161+
<receiver
162+
android:name=".ui.widgets.fab.FabWidget"
163+
android:exported="false"
164+
android:label="@string/widget_fab_name">
165+
<intent-filter>
166+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
167+
</intent-filter>
168+
<meta-data
169+
android:name="android.appwidget.provider"
170+
android:resource="@xml/widget_fab_info" />
171+
</receiver>
172+
173+
<receiver
174+
android:name=".ui.widgets.fab.FabClickReceiver"
175+
android:exported="false" />
176+
177+
<receiver
178+
android:name=".ui.widgets.home.HomeAppsWidgetProvider"
179+
android:exported="true"
180+
android:label="@string/widget_home_name">
181+
<intent-filter>
182+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
183+
</intent-filter>
184+
<meta-data
185+
android:name="android.appwidget.provider"
186+
android:resource="@xml/widget_home_apps_info" />
187+
</receiver>
188+
189+
<receiver
190+
android:name=".ui.widgets.home.HomeAppUpdateReceiver"
191+
android:exported="false" />
192+
193+
133194
</application>
134195
</manifest>

app/src/main/java/com/github/droidworksstudio/common/ContextExtensions.kt

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,11 @@ fun Context.launchCalendar() {
106106

107107
fun Context.openDialerApp() {
108108
try {
109-
val sendIntent = Intent(Intent.ACTION_DIAL)
109+
val sendIntent = Intent(Intent.ACTION_DIAL).apply {
110+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
111+
}
110112
this.startActivity(sendIntent)
111-
} catch (e: java.lang.Exception) {
113+
} catch (e: Exception) {
112114
AppLogger.d("openDialerApp", e.toString())
113115
}
114116
CrashHandler.logUserAction("Dialer App Launched")
@@ -129,8 +131,10 @@ fun Context.openTextMessagesApp() {
129131

130132
fun Context.openAlarmApp() {
131133
try {
132-
val intent = Intent(AlarmClock.ACTION_SHOW_ALARMS)
133-
this.startActivity(intent)
134+
val sendIntent = Intent(AlarmClock.ACTION_SHOW_ALARMS).apply {
135+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
136+
}
137+
this.startActivity(sendIntent)
134138
} catch (e: java.lang.Exception) {
135139
AppLogger.d("openAlarmApp", e.toString())
136140
}
@@ -139,7 +143,9 @@ fun Context.openAlarmApp() {
139143

140144
fun Context.openCameraApp() {
141145
try {
142-
val sendIntent = Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
146+
val sendIntent = Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA).apply {
147+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
148+
}
143149
this.startActivity(sendIntent)
144150
} catch (e: java.lang.Exception) {
145151
AppLogger.d("openCameraApp", e.toString())
@@ -149,11 +155,11 @@ fun Context.openCameraApp() {
149155

150156
fun Context.openPhotosApp() {
151157
try {
152-
val intent = Intent(Intent.ACTION_VIEW).apply {
158+
val sendIntent = Intent(Intent.ACTION_VIEW).apply {
153159
type = "image/*"
154-
flags = Intent.FLAG_ACTIVITY_NEW_TASK
160+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
155161
}
156-
this.startActivity(intent)
162+
this.startActivity(sendIntent)
157163
} catch (e: Exception) {
158164
AppLogger.d("openPhotosApp", e.toString())
159165
}
@@ -162,10 +168,10 @@ fun Context.openPhotosApp() {
162168

163169
fun Context.openDeviceSettings() {
164170
try {
165-
val intent = Intent(Settings.ACTION_SETTINGS).apply {
166-
flags = Intent.FLAG_ACTIVITY_NEW_TASK
171+
val sendIntent = Intent(Settings.ACTION_SETTINGS).apply {
172+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
167173
}
168-
this.startActivity(intent)
174+
this.startActivity(sendIntent)
169175
} catch (e: Exception) {
170176
AppLogger.d("openDeviceSettings", e.toString())
171177
}
@@ -178,7 +184,7 @@ fun Context.openWebBrowser() {
178184
if (defaultBrowserPackage != null) {
179185
val launchIntent = packageManager.getLaunchIntentForPackage(defaultBrowserPackage)
180186
if (launchIntent != null) {
181-
launchIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
187+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
182188
startActivity(launchIntent)
183189
} else {
184190
AppLogger.d("openDefaultBrowserApp", "No launch intent for package $defaultBrowserPackage")
@@ -194,16 +200,18 @@ fun Context.openWebBrowser() {
194200

195201

196202
fun Context.getDefaultBrowserPackageName(): String? {
197-
val intent = Intent(Intent.ACTION_VIEW, "https://".toUri())
198-
val resolveInfo = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
203+
val sendIntent = Intent(Intent.ACTION_VIEW, "https://".toUri())
204+
val resolveInfo = packageManager.resolveActivity(sendIntent, PackageManager.MATCH_DEFAULT_ONLY)
199205
return resolveInfo?.activityInfo?.packageName
200206
}
201207

202208

203209
fun Context.openBatteryManager() {
204210
try {
205-
val intent = Intent(Intent.ACTION_POWER_USAGE_SUMMARY)
206-
this.startActivity(intent)
211+
val sendIntent = Intent(Intent.ACTION_POWER_USAGE_SUMMARY).apply {
212+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
213+
}
214+
this.startActivity(sendIntent)
207215
} catch (_: ActivityNotFoundException) {
208216
showLongToast("Battery manager settings are not available on this device.")
209217
}

app/src/main/java/com/github/droidworksstudio/mlauncher/helper/utils/AppReloader.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import android.content.Intent
55
import androidx.lifecycle.ProcessLifecycleOwner
66
import androidx.lifecycle.lifecycleScope
7+
import com.github.droidworksstudio.common.AppLogger
78
import kotlinx.coroutines.CoroutineScope
89
import kotlinx.coroutines.Dispatchers
910
import kotlinx.coroutines.delay
@@ -25,5 +26,18 @@ object AppReloader {
2526
Runtime.getRuntime().exit(0) // Forcefully terminates the current process
2627
}
2728
}
29+
30+
fun startApp(context: Context) {
31+
try {
32+
val packageManager = context.packageManager
33+
val intent = packageManager.getLaunchIntentForPackage(context.packageName)
34+
intent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
35+
if (intent != null) {
36+
context.startActivity(intent)
37+
}
38+
} catch (e: Exception) {
39+
AppLogger.d("startApp", e.toString())
40+
}
41+
}
2842
}
2943

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package com.github.droidworksstudio.mlauncher.ui.widgets.fab
2+
3+
import android.app.PendingIntent
4+
import android.appwidget.AppWidgetManager
5+
import android.appwidget.AppWidgetProvider
6+
import android.content.BroadcastReceiver
7+
import android.content.Context
8+
import android.content.Intent
9+
import android.view.View
10+
import android.widget.RemoteViews
11+
import com.github.droidworksstudio.common.AppLogger
12+
import com.github.droidworksstudio.common.ColorManager
13+
import com.github.droidworksstudio.common.openCameraApp
14+
import com.github.droidworksstudio.common.openDeviceSettings
15+
import com.github.droidworksstudio.common.openDialerApp
16+
import com.github.droidworksstudio.common.openPhotosApp
17+
import com.github.droidworksstudio.common.openTextMessagesApp
18+
import com.github.droidworksstudio.common.openWebBrowser
19+
import com.github.droidworksstudio.mlauncher.R
20+
import com.github.droidworksstudio.mlauncher.data.Prefs
21+
import com.github.droidworksstudio.mlauncher.helper.utils.AppReloader
22+
23+
class FabWidget : AppWidgetProvider() {
24+
25+
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
26+
val prefs = Prefs(context)
27+
28+
// Default flags as a String
29+
val defaultFlagsString = "0000011"
30+
31+
// Get flags (already List<Boolean>)
32+
val fabFlags: List<Boolean> = prefs.getMenuFlags("HOME_BUTTON_FLAGS", defaultFlagsString)
33+
34+
val fabIds = listOf(
35+
R.id.fabPhone,
36+
R.id.fabMessages,
37+
R.id.fabCamera,
38+
R.id.fabPhotos,
39+
R.id.fabBrowser,
40+
R.id.fabSettings,
41+
R.id.fabAction
42+
)
43+
44+
// Generate colors
45+
val colors = ColorManager.getRandomHueColors(prefs.shortcutIconsColor, fabIds.size)
46+
47+
for (appWidgetId in appWidgetIds) {
48+
val views = RemoteViews(context.packageName, R.layout.widget_fab)
49+
50+
for (index in fabIds.indices) {
51+
val viewId = fabIds[index]
52+
val isVisible = fabFlags.getOrNull(index) ?: false
53+
views.setViewVisibility(viewId, if (isVisible) View.VISIBLE else View.GONE)
54+
55+
// Set color filter using when
56+
when (viewId) {
57+
R.id.fabPhone,
58+
R.id.fabMessages,
59+
R.id.fabCamera,
60+
R.id.fabPhotos,
61+
R.id.fabBrowser,
62+
R.id.fabSettings -> {
63+
val color = colors.getOrNull(index) ?: prefs.shortcutIconsColor
64+
views.setInt(viewId, "setColorFilter", if (prefs.iconRainbowColors) color else prefs.shortcutIconsColor)
65+
}
66+
67+
R.id.fabAction -> {
68+
// No color filter for fabAction
69+
}
70+
}
71+
}
72+
73+
// Assign PendingIntents only for visible buttons
74+
if (fabFlags.getOrNull(0) == true) views.setOnClickPendingIntent(
75+
R.id.fabPhone,
76+
getBroadcastPendingIntent(context, "FAB_PHONE")
77+
)
78+
if (fabFlags.getOrNull(1) == true) views.setOnClickPendingIntent(
79+
R.id.fabMessages,
80+
getBroadcastPendingIntent(context, "FAB_MESSAGES")
81+
)
82+
83+
if (fabFlags.getOrNull(2) == true) views.setOnClickPendingIntent(
84+
R.id.fabCamera,
85+
getBroadcastPendingIntent(context, "FAB_CAMERA")
86+
)
87+
88+
if (fabFlags.getOrNull(3) == true) views.setOnClickPendingIntent(
89+
R.id.fabPhotos,
90+
getBroadcastPendingIntent(context, "FAB_PHOTOS")
91+
)
92+
93+
if (fabFlags.getOrNull(4) == true) views.setOnClickPendingIntent(
94+
R.id.fabBrowser,
95+
getBroadcastPendingIntent(context, "FAB_BROWSER")
96+
)
97+
98+
if (fabFlags.getOrNull(5) == true) views.setOnClickPendingIntent(
99+
R.id.fabSettings,
100+
getBroadcastPendingIntent(context, "FAB_SETTINGS")
101+
)
102+
if (fabFlags.getOrNull(6) == true) views.setOnClickPendingIntent(
103+
R.id.fabAction,
104+
getBroadcastPendingIntent(context, "FAB_ACTION")
105+
)
106+
107+
appWidgetManager.updateAppWidget(appWidgetId, views)
108+
}
109+
}
110+
111+
112+
fun getBroadcastPendingIntent(context: Context, action: String): PendingIntent {
113+
val intent = Intent(context, FabClickReceiver::class.java).apply {
114+
this.action = action
115+
}
116+
return PendingIntent.getBroadcast(context, action.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
117+
}
118+
}
119+
120+
class FabClickReceiver : BroadcastReceiver() {
121+
override fun onReceive(context: Context, intent: Intent) {
122+
val action = intent.action
123+
124+
AppLogger.d("FabClickReceiver", "onReceive: $action")
125+
when (action) {
126+
"FAB_PHONE" -> context.openDialerApp()
127+
"FAB_MESSAGES" -> context.openTextMessagesApp()
128+
"FAB_CAMERA" -> context.openCameraApp()
129+
"FAB_PHOTOS" -> context.openPhotosApp()
130+
"FAB_BROWSER" -> context.openWebBrowser()
131+
"FAB_SETTINGS" -> context.openDeviceSettings()
132+
"FAB_ACTION" -> AppReloader.startApp(context)
133+
// handle other buttons...
134+
}
135+
}
136+
}
137+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.github.droidworksstudio.mlauncher.ui.widgets.home
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.widget.Toast
7+
import com.github.droidworksstudio.common.AppLogger
8+
9+
class HomeAppUpdateReceiver : BroadcastReceiver() {
10+
11+
override fun onReceive(context: Context, intent: Intent) {
12+
AppLogger.d("HomeAppUpdateReceiver", "Received intent: action=${intent.action}, extras=${intent.extras}")
13+
14+
if (intent.action == "HOME_APP_CLICK") {
15+
val extras = intent.extras
16+
if (extras == null) {
17+
AppLogger.d("HomeAppUpdateReceiver", "No extras found in intent")
18+
return
19+
}
20+
val packageName = extras.getString("PACKAGE_NAME")
21+
AppLogger.d("HomeAppUpdateReceiver", "PACKAGE_NAME extra: $packageName")
22+
23+
if (packageName.isNullOrEmpty()) {
24+
AppLogger.d("HomeAppUpdateReceiver", "No package name provided, aborting launch")
25+
return
26+
}
27+
28+
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
29+
if (launchIntent != null) {
30+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
31+
context.startActivity(launchIntent)
32+
AppLogger.d("HomeAppUpdateReceiver", "Launched app: $packageName")
33+
} else {
34+
AppLogger.d("HomeAppUpdateReceiver", "Cannot find app: $packageName")
35+
Toast.makeText(context, "Cannot find app: $packageName", Toast.LENGTH_SHORT).show()
36+
}
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)