core: home widgets api

This commit is contained in:
MSOB7YY
2024-02-14 01:34:23 +02:00
parent a6e767ebd2
commit 77f8f406cd
12 changed files with 651 additions and 4 deletions

View File

@@ -32,7 +32,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 33
compileSdkVersion 34
ndkVersion flutter.ndkVersion
splits {
@@ -131,5 +131,9 @@ repositories {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
}

View File

@@ -116,5 +116,23 @@
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<!-- Home Screen Widgets -->
<receiver android:name="SchwarzSechsPrototypeMkII"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/schwarz" />
</receiver>
<receiver android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver" android:exported="true">
<intent-filter>
<action android:name="es.antonborri.home_widget.action.BACKGROUND" />
</intent-filter>
</receiver>
<service android:name="es.antonborri.home_widget.HomeWidgetBackgroundService"
android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true"/>
</application>
</manifest>

View File

@@ -0,0 +1,21 @@
package com.msob7y.namida
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class SchwarzBroadcast : BroadcastReceiver() {
private val HOME_WIDGET_BACKGROUND_ACTION = "com.msob7y.namida.action.BACKGROUND"
override fun onReceive(context: Context, intentRecieved: Intent) {
val intent = Intent(context, NamidaMainActivity::class.java)
intent.data = intentRecieved.data
intent.action = HOME_WIDGET_BACKGROUND_ACTION
intent.addFlags(
Intent.FLAG_ACTIVITY_CLEAR_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK
)
context.startActivity(intent)
}
}

View File

@@ -0,0 +1,127 @@
package com.msob7y.namida
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.view.View
import android.widget.RemoteViews
import es.antonborri.home_widget.HomeWidgetBackgroundIntent
import es.antonborri.home_widget.HomeWidgetLaunchIntent
import es.antonborri.home_widget.HomeWidgetProvider
class SchwarzSechsPrototypeMkII : HomeWidgetProvider() {
private val HOME_WIDGET_BACKGROUND_ACTION = "com.msob7y.namida.action.BACKGROUND"
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
widgetData: SharedPreferences
) {
appWidgetIds.forEach { widgetId ->
val views =
RemoteViews(context.packageName, R.layout.mk2).apply {
// Open App on Widget Click
val pendingIntent =
HomeWidgetLaunchIntent.getActivity(context, NamidaMainActivity::class.java)
setOnClickPendingIntent(R.id.widget_container, pendingIntent)
// Swap Title Text by calling Dart Code in the Background
setTextViewText(R.id.widget_title, widgetData.getString("title", null) ?: "")
setClickIntent(this, context, R.id.previous, "previous")
setClickIntent(this, context, R.id.play_pause, "play_pause")
setClickIntent(this, context, R.id.next, "next")
val message = widgetData.getString("message", null)
setTextViewText(R.id.widget_message, message ?: "Unknown")
// Show Images saved with `renderFlutterWidget`
val image = widgetData.getString("dashIcon", null)
if (image != null) {
setImageViewBitmap(R.id.widget_img, BitmapFactory.decodeFile(image))
setViewVisibility(R.id.widget_img, View.VISIBLE)
} else {
setViewVisibility(R.id.widget_img, View.GONE)
}
// Detect App opened via Click inside Flutter
val pendingIntentWithData =
HomeWidgetLaunchIntent.getActivity(
context,
NamidaMainActivity::class.java,
Uri.parse("namidaWidget://message?message=$message")
)
setOnClickPendingIntent(R.id.widget_message, pendingIntentWithData)
}
appWidgetManager.updateAppWidget(widgetId, views)
}
}
private fun setClickIntent(views: RemoteViews, context: Context, viewId: Int, key: String) {
val backgroundIntent =
HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("namidaWidget://$key"))
views.setOnClickPendingIntent(viewId, backgroundIntent)
// val serviceName = ComponentName(context, NamidaMainActivity::class.java)
// val intent = Intent(HOME_WIDGET_BACKGROUND_ACTION)
// intent.component = serviceName
// intent.data = Uri.parse("namidaWidget://$key")
// val pendingIntent: PendingIntent
// if (Build.VERSION.SDK_INT >= 23) {
// pendingIntent =
// PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
// } else {
// pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
// }
// val pendingIntent = buildPendingIntent(context, HOME_WIDGET_BACKGROUND_ACTION, serviceName)
// views.setOnClickPendingIntent(viewId, pendingIntent)
// val intent = Intent(context.getApplicationContext(), NamidaMainActivity::class.java)
// intent.addFlags(
// Intent.FLAG_ACTIVITY_CLEAR_TASK or
// Intent.FLAG_ACTIVITY_CLEAR_TOP or
// Intent.FLAG_ACTIVITY_NEW_TASK
// )
// intent.data = Uri.parse("namidaWidget://$key")
// intent.action = HOME_WIDGET_BACKGROUND_ACTION
// context.startActivity(intent)
// val intent = Intent(context, NamidaMainActivity::class.java)
// intent.data = Uri.parse("namidaWidget://$key")
// intent.action = HOME_WIDGET_BACKGROUND_ACTION
// var flags = PendingIntent.FLAG_UPDATE_CURRENT
// if (Build.VERSION.SDK_INT >= 23) {
// flags = flags or PendingIntent.FLAG_IMMUTABLE
// }
// val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags)
// val pendingIntentActivity = PendingIntent.getActivity(context, 0, intent, flags)
// views.setOnClickPendingIntent(viewId, backgroundIntent)
}
protected fun buildPendingIntent(
context: Context,
action: String,
serviceName: ComponentName
): PendingIntent {
val intent = Intent(action)
intent.component = serviceName
return if (Build.VERSION.SDK_INT >= 23) {
PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#181818" />
<corners android:radius="16dp" />
</shape>

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:minWidth="40dp"
android:minHeight="40dp"
android:targetCellWidth="4"
android:targetCellHeight="2"
android:maxResizeWidth="250dp"
android:maxResizeHeight="120dp"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable|configuration_optional"
tools:ignore="ContentDescription"
>
<!-- Outer Row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/widget_background"
android:orientation="horizontal">
<ImageView
android:id="@+id/widget_img"
android:layout_width="64dp"
android:layout_height="64dp"
android:visibility="gone" />
<!-- <androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:cardCornerRadius="8dp">
<ImageView
android:id="@+id/widget_img"
android:layout_width="64dp"
android:layout_height="64dp"
android:visibility="gone" />
</androidx.cardview.widget.CardView> -->
<!-- Column Layout -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Text Column -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="8dp">
<!-- <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/widget_title"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:breakStrategy="balanced"
android:gravity="center_horizontal"
android:maxLines="1"
android:text=""
android:textSize="16sp"
app:autoSizeTextType="uniform"
tools:targetApi="o"
tools:text="Title" /> -->
<TextView
android:id="@+id/widget_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end"
android:layout_marginStart="2dp"
tools:text="Title" />
<TextView
android:id="@+id/widget_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:maxLines="1"
android:ellipsize="end"
android:layout_marginStart="2dp"
tools:text="Message" />
</LinearLayout>
<!-- Inner Row (Buttons) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:id="@+id/widget_container">
<ImageButton
android:id="@+id/previous"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@android:color/transparent"
tools:src="@drawable/audio_service_skip_previous" />
<ImageButton
android:id="@+id/play_pause"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@android:color/transparent"
tools:src="@drawable/audio_service_play_arrow" />
<ImageButton
android:id="@+id/next"
android:layout_width="24dp"
android:layout_height="24dp"
tools:src="@drawable/audio_service_skip_next" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/mk2"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>

View File

@@ -18,6 +18,7 @@ import 'package:namida/class/video.dart';
import 'package:namida/controller/connectivity.dart';
import 'package:namida/controller/current_color.dart';
import 'package:namida/controller/history_controller.dart';
import 'package:namida/controller/home_widgets_controller.dart';
import 'package:namida/controller/indexer_controller.dart';
import 'package:namida/controller/lyrics_controller.dart';
import 'package:namida/controller/miniplayer_controller.dart';
@@ -164,18 +165,25 @@ class NamidaAudioVideoHandler<Q extends Playable> extends BasicAudioHandler<Q> {
}
void _notificationUpdateItem({required Q item, required bool isItemFavourite, required int itemIndex, VideoInfo? videoInfo}) {
late MediaItem finalMI;
item._execute(
selectable: (finalItem) async {
mediaItem.add(finalItem.toMediaItem(currentIndex, currentQueue.length));
final mediaItemInfo = finalItem.toMediaItem(currentIndex, currentQueue.length);
finalMI = mediaItemInfo;
mediaItem.add(mediaItemInfo);
playbackState.add(transformEvent(PlaybackEvent(currentIndex: currentIndex), isItemFavourite, itemIndex));
},
youtubeID: (finalItem) async {
final info = videoInfo ?? YoutubeController.inst.getVideoInfo(finalItem.id);
final thumbnail = finalItem.getThumbnailSync();
mediaItem.add(finalItem.toMediaItem(info, thumbnail, currentIndex, currentQueue.length));
final mediaItemInfo = finalItem.toMediaItem(info, thumbnail, currentIndex, currentQueue.length);
finalMI = mediaItemInfo;
mediaItem.add(mediaItemInfo);
playbackState.add(transformEvent(PlaybackEvent(currentIndex: currentIndex), isItemFavourite, itemIndex));
},
);
printo('NAMIDAWIDGET: updating ${finalMI.displayTitle}');
NamidaWidgetController.updateData(title: finalMI.displayTitle ?? finalMI.title, subtitle: finalMI.displaySubtitle ?? finalMI.artist ?? '');
}
// =================================================================================

View File

@@ -0,0 +1,316 @@
import 'dart:async';
import 'dart:math';
import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:home_widget/home_widget.dart';
import 'package:namida/base/audio_handler.dart';
import 'package:namida/controller/player_controller.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/main.dart';
import 'package:workmanager/workmanager.dart';
/// Used for Background Updates using Workmanager Plugin
@pragma("vm:entry-point")
void namidaWidgetDispatcher() {
Workmanager().executeTask((taskName, inputData) {
final now = DateTime.now();
return Future.wait<bool?>([
HomeWidget.saveWidgetData(
'title',
'Updated from Background',
),
HomeWidget.saveWidgetData(
'message',
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}',
),
HomeWidget.updateWidget(
name: _widgetID1,
),
]).then((value) {
return !value.contains(false);
});
});
}
/// Called when Doing Background Work initiated from Widget
@pragma("vm:entry-point")
Future<void> interactiveCallback(Uri? data) async {
print('NAMIDAWIDGET: $data');
try {
// -- initialize main
} catch (e) {
print('NAMIDAWIDGET: $e');
}
print('NAMIDAWIDGET: initialized main');
if (data?.host == 'titleclicked') {
final greetings = [
'Hello',
'Hallo',
'Bonjour',
'Hola',
'Ciao',
'哈洛',
'안녕하세요',
'xin chào',
];
final selectedGreeting = greetings[Random().nextInt(greetings.length)];
await HomeWidget.setAppGroupId(_groupId);
await HomeWidget.saveWidgetData<String>('title', selectedGreeting);
await HomeWidget.updateWidget(
name: _widgetID1,
);
}
}
const _groupId = 'kurukuru';
const _widgetID1 = 'SchwarzSechsPrototypeMkII';
class NamidaWidgetController {
static initManager() {
Workmanager().initialize(namidaWidgetDispatcher, isInDebugMode: kDebugMode);
}
static initWidget() {
HomeWidget.setAppGroupId(_groupId);
HomeWidget.registerInteractivityCallback(interactiveCallback);
}
static onDepChange() {
_checkForWidgetLaunch();
HomeWidget.widgetClicked.listen(_launchedFromWidget);
}
static void _checkForWidgetLaunch() {
HomeWidget.initiallyLaunchedFromHomeWidget().then(_launchedFromWidget);
}
static void _launchedFromWidget(Uri? uri) {
if (uri != null) printo('NAMIDAWIDGET: App started from HomeScreenWidget, uri: $uri');
}
static Future updateData({
required String title,
required String subtitle,
}) async {
await _sendData(title: title, subtitle: subtitle);
await _updateWidget();
}
static Future _sendData({
required String title,
required String subtitle,
}) async {
try {
return Future.wait([
HomeWidget.saveWidgetData<String>('title', title),
HomeWidget.saveWidgetData<String>('message', subtitle),
HomeWidget.renderFlutterWidget(
const Icon(
Icons.abc,
size: 200,
),
logicalSize: const Size(200, 200),
key: 'dashIcon',
),
HomeWidget.renderFlutterWidget(
const Icon(
Icons.fork_left,
size: 24,
),
logicalSize: const Size(24, 24),
key: 'previous',
),
HomeWidget.renderFlutterWidget(
const Icon(
Icons.play_arrow,
size: 24,
),
logicalSize: const Size(24, 24),
key: 'play_pause',
),
HomeWidget.renderFlutterWidget(
const Icon(
Icons.fork_right,
size: 24,
),
logicalSize: const Size(24, 24),
key: 'next',
),
]);
} on PlatformException catch (exception) {
printo('NAMIDAWIDGET: Error Sending Data. $exception', isError: true);
}
}
static Future<bool?> _updateWidget() async {
try {
return HomeWidget.updateWidget(name: _widgetID1);
} on PlatformException catch (exception) {
printo('NAMIDAWIDGET: Error Updating Widget. $exception', isError: true);
return null;
}
}
}
// class MyApp extends StatefulWidget {
// const MyApp({Key? key}) : super(key: key);
// @override
// State<MyApp> createState() => _MyAppState();
// }
// class _MyAppState extends State<MyApp> {
// final TextEditingController _titleController = TextEditingController();
// final TextEditingController _messageController = TextEditingController();
// @override
// void initState() {
// super.initState();
// HomeWidget.setAppGroupId('YOUR_GROUP_ID');
// HomeWidget.registerInteractivityCallback(interactiveCallback);
// }
// @override
// void didChangeDependencies() {
// super.didChangeDependencies();
// _checkForWidgetLaunch();
// HomeWidget.widgetClicked.listen(_launchedFromWidget);
// }
// @override
// void dispose() {
// _titleController.dispose();
// _messageController.dispose();
// super.dispose();
// }
// Future _sendData() async {
// try {
// return Future.wait([
// HomeWidget.saveWidgetData<String>('title', _titleController.text),
// HomeWidget.saveWidgetData<String>('message', _messageController.text),
// HomeWidget.renderFlutterWidget(
// const Icon(
// Icons.flutter_dash,
// size: 200,
// ),
// logicalSize: const Size(200, 200),
// key: 'dashIcon',
// ),
// ]);
// } on PlatformException catch (exception) {
// debugPrint('Error Sending Data. $exception');
// }
// }
// Future _updateWidget() async {
// try {
// return HomeWidget.updateWidget(
// name: 'HomeWidgetExampleProvider',
// iOSName: 'HomeWidgetExample',
// );
// } on PlatformException catch (exception) {
// debugPrint('Error Updating Widget. $exception');
// }
// }
// Future _loadData() async {
// try {
// return Future.wait([
// HomeWidget.getWidgetData<String>('title', defaultValue: 'Default Title').then((value) => _titleController.text = value ?? ''),
// HomeWidget.getWidgetData<String>(
// 'message',
// defaultValue: 'Default Message',
// ).then((value) => _messageController.text = value ?? ''),
// ]);
// } on PlatformException catch (exception) {
// debugPrint('Error Getting Data. $exception');
// }
// }
// Future<void> _sendAndUpdate() async {
// await _sendData();
// await _updateWidget();
// }
// void _checkForWidgetLaunch() {
// HomeWidget.initiallyLaunchedFromHomeWidget().then(_launchedFromWidget);
// }
// void _launchedFromWidget(Uri? uri) {
// if (uri != null) {
// showDialog(
// context: context,
// builder: (buildContext) => AlertDialog(
// title: const Text('App started from HomeScreenWidget'),
// content: Text('Here is the URI: $uri'),
// ),
// );
// }
// }
// void _startBackgroundUpdate() {
// Workmanager().registerPeriodicTask(
// '1',
// 'widgetBackgroundUpdate',
// frequency: const Duration(minutes: 15),
// );
// }
// void _stopBackgroundUpdate() {
// Workmanager().cancelByUniqueName('1');
// }
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// appBar: AppBar(
// title: const Text('HomeWidget Example'),
// ),
// body: Center(
// child: Column(
// children: [
// TextField(
// decoration: const InputDecoration(
// hintText: 'Title',
// ),
// controller: _titleController,
// ),
// TextField(
// decoration: const InputDecoration(
// hintText: 'Body',
// ),
// controller: _messageController,
// ),
// ElevatedButton(
// onPressed: _sendAndUpdate,
// child: const Text('Send Data to Widget'),
// ),
// ElevatedButton(
// onPressed: _loadData,
// child: const Text('Load Data'),
// ),
// ElevatedButton(
// onPressed: _checkForWidgetLaunch,
// child: const Text('Check For Widget Launch'),
// ),
// if (Platform.isAndroid)
// ElevatedButton(
// onPressed: _startBackgroundUpdate,
// child: const Text('Update in background'),
// ),
// if (Platform.isAndroid)
// ElevatedButton(
// onPressed: _stopBackgroundUpdate,
// child: const Text('Stop updating in background'),
// ),
// ],
// ),
// ),
// );
// }
// }

View File

@@ -23,6 +23,7 @@ import 'package:namida/controller/connectivity.dart';
import 'package:namida/controller/current_color.dart';
import 'package:namida/controller/equalizer_settings.dart';
import 'package:namida/controller/folders_controller.dart';
import 'package:namida/controller/home_widgets_controller.dart';
import 'package:namida/controller/indexer_controller.dart';
import 'package:namida/controller/namida_channel.dart';
import 'package:namida/controller/navigator_controller.dart';
@@ -45,9 +46,10 @@ import 'package:namida/youtube/controller/youtube_controller.dart';
import 'package:namida/youtube/controller/youtube_playlist_controller.dart';
import 'package:namida/youtube/controller/youtube_subscriptions_controller.dart';
void main() async {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
Paint.enableDithering = true; // for smooth gradient effect.
NamidaWidgetController.initManager();
// -- x this makes some issues with GestureDetector
// GestureBinding.instance.resamplingEnabled = true; // for 120hz displays, should make scrolling smoother.
@@ -177,6 +179,7 @@ void main() async {
Folders.inst.onFirstLoad();
_initLifeCycle();
NamidaWidgetController.initWidget();
}
void _initLifeCycle() {
@@ -224,6 +227,10 @@ Future<void> _initializeIntenties() async {
}
Future<void> playFiles(List<SharedFile> files) async {
files.loop((e, index) {
printo('NAMIDAWIDGET: ${e.value} || ${e.realPath} || ${e.type}');
print('NAMIDAWIDGET: ${e.value} || ${e.realPath} || ${e.type}');
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
if (files.isNotEmpty) {
final paths = <String>[];

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:namida/controller/home_widgets_controller.dart';
import 'package:namida/controller/miniplayer_controller.dart';
import 'package:namida/controller/navigator_controller.dart';
import 'package:namida/controller/player_controller.dart';
@@ -54,6 +55,7 @@ class _MainPageWrapperState extends State<MainPageWrapper> {
@override
void didChangeDependencies() {
widget.onContextAvailable(context);
NamidaWidgetController.onDepChange();
super.didChangeDependencies();
}

View File

@@ -25,6 +25,7 @@ dependency_overrides:
ref: 4ff36f3eb1cac226051faeb079b9097a9f0bb0f9
archive: ^3.3.8
intl: ^0.18.0
device_info_plus: ^9.0.2
get:
git:
url: https://github.com/MSOB7YY/getx
@@ -77,6 +78,9 @@ dependencies:
system_info2: ^4.0.0
vibration: ^1.8.4
flutter_displaymode: ^0.6.0
home_widget: ^0.4.1
workmanager: ^0.5.2
android_intent_plus: ^4.0.3
# ---- Path/File Providers ----
path_provider: ^2.0.14