diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1b10784..087ec28 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
tools:ignore="QueryAllPackagesPermission" />
+
+
@@ -80,6 +82,9 @@
+
-
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt
index 3c2e3bc..e0d1d00 100644
--- a/app/src/main/java/de/jrpie/android/launcher/Application.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt
@@ -25,6 +25,7 @@ import de.jrpie.android.launcher.preferences.resetPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlin.system.exitProcess
const val APP_WIDGET_HOST_ID = 42;
@@ -106,6 +107,11 @@ class Application : android.app.Application() {
// TODO Error: Invalid resource ID 0x00000000.
// DynamicColors.applyToActivitiesIfAvailable(this)
+ Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
+ sendCrashNotification(this@Application, throwable)
+ exitProcess(1)
+ }
+
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
torchManager = TorchManager(this)
@@ -114,8 +120,6 @@ class Application : android.app.Application() {
appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID)
appWidgetManager = AppWidgetManager.getInstance(this.applicationContext)
- appWidgetHost.startListening()
-
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
LauncherPreferences.init(preferences, this.resources)
@@ -157,6 +161,8 @@ class Application : android.app.Application() {
removeUnusedShortcuts(this)
}
loadApps()
+
+ createNotificationChannels(this)
}
fun getCustomAppNames(): HashMap {
@@ -170,10 +176,4 @@ class Application : android.app.Application() {
apps.postValue(getApps(packageManager, applicationContext))
}
}
-
- override fun onTerminate() {
- appWidgetHost.stopListening()
- super.onTerminate()
-
- }
}
diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt
index 9679ae5..b6df30b 100644
--- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt
@@ -6,9 +6,6 @@ import android.app.role.RoleManager
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
-import android.appwidget.AppWidgetManager
-import android.appwidget.AppWidgetProvider
-import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
@@ -227,3 +224,13 @@ fun copyToClipboard(context: Context, text: String) {
val clipData = ClipData.newPlainText("Debug Info", text)
clipboardManager.setPrimaryClip(clipData)
}
+
+fun writeEmail(context: Context, to: String, subject: String, text: String) {
+ val intent = Intent(Intent.ACTION_SENDTO)
+ intent.setData("mailto:".toUri())
+ intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
+ intent.putExtra(Intent.EXTRA_SUBJECT, subject)
+ intent.putExtra(Intent.EXTRA_TEXT, text)
+ context.startActivity(Intent.createChooser(intent, context.getString(R.string.send_email)))
+}
+
diff --git a/app/src/main/java/de/jrpie/android/launcher/Notifications.kt b/app/src/main/java/de/jrpie/android/launcher/Notifications.kt
new file mode 100644
index 0000000..0cf0efb
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/Notifications.kt
@@ -0,0 +1,87 @@
+package de.jrpie.android.launcher
+
+import android.app.Activity
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.util.Log
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import de.jrpie.android.launcher.ui.EXTRA_CRASH_LOG
+import de.jrpie.android.launcher.ui.ReportCrashActivity
+import java.io.PrintWriter
+import java.io.StringWriter
+import kotlin.random.Random
+
+private val NOTIFICATION_CHANNEL_CRASH = "launcher:crash"
+
+fun createNotificationChannels(context: Context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationChannel = NotificationChannel(
+ NOTIFICATION_CHANNEL_CRASH,
+ context.getString(R.string.notification_channel_crash),
+ NotificationManager.IMPORTANCE_HIGH
+ )
+
+ val notificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(notificationChannel)
+ }
+}
+
+fun requestNotificationPermission(activity: Activity) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ return
+ }
+
+ val permission =
+ (activity.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED)
+
+ if (!permission) {
+ ActivityCompat.requestPermissions(
+ activity,
+ arrayOf( android.Manifest.permission.POST_NOTIFICATIONS ),
+ 1
+ )
+ }
+}
+
+fun sendCrashNotification(context: Context, throwable: Throwable) {
+ val stringWriter = StringWriter()
+ val printWriter = PrintWriter(stringWriter)
+ throwable.printStackTrace(printWriter)
+
+ val intent = Intent(context, ReportCrashActivity::class.java)
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ intent.putExtra(EXTRA_CRASH_LOG, stringWriter.toString())
+
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ Random.nextInt(),
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_CRASH)
+ .setSmallIcon(R.drawable.baseline_bug_report_24)
+ .setContentTitle(context.getString(R.string.notification_crash_title))
+ .setContentText(context.getString(R.string.notification_crash_explanation))
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(false)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+
+ val notificationManager = NotificationManagerCompat.from(context)
+ try {
+ notificationManager.notify(
+ 0,
+ builder.build()
+ )
+ } catch (e: SecurityException) {
+ Log.e("Crash Notification", "Could not send notification")
+ }
+}
diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt
index d7829a6..84c4179 100644
--- a/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt
@@ -1,5 +1,6 @@
package de.jrpie.android.launcher.actions
+import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Rect
@@ -25,11 +26,18 @@ class WidgetPanelAction(val widgetPanelId: Int) : Action {
override fun invoke(context: Context, rect: Rect?): Boolean {
- if (WidgetPanel.byId(widgetPanelId) == null) {
+ if (context is WidgetPanelActivity) {
+ if (context.widgetPanelId == widgetPanelId) {
+ context.finish()
+ return true
+ }
+ }
+
+ if (WidgetPanel.byId(this.widgetPanelId) == null) {
Toast.makeText(context, R.string.alert_widget_panel_not_found, Toast.LENGTH_LONG).show()
} else {
context.startActivity(Intent(context, WidgetPanelActivity::class.java).also {
- it.putExtra(EXTRA_PANEL_ID, widgetPanelId)
+ it.putExtra(EXTRA_PANEL_ID, this.widgetPanelId)
})
}
return true
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt
index fd86d79..a4b1f43 100644
--- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt
@@ -2,7 +2,6 @@ package de.jrpie.android.launcher.preferences
import android.content.Context
import android.util.Log
-import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.apps.AbstractAppInfo
@@ -10,22 +9,25 @@ import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1
+import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion100
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion2
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion4
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown
+import de.jrpie.android.launcher.sendCrashNotification
import de.jrpie.android.launcher.ui.HomeActivity
import de.jrpie.android.launcher.widgets.ClockWidget
import de.jrpie.android.launcher.widgets.DebugInfoWidget
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.WidgetPosition
-import de.jrpie.android.launcher.widgets.deleteAllWidgets
+import de.jrpie.android.launcher.widgets.generateInternalId
+import de.jrpie.android.launcher.widgets.getAppWidgetHost
/* Current version of the structure of preferences.
* Increase when breaking changes are introduced and write an appropriate case in
* `migratePreferencesToNewVersion`
*/
-const val PREFERENCE_VERSION = 100
+const val PREFERENCE_VERSION = 101
const val UNKNOWN_PREFERENCE_VERSION = -1
private const val TAG = "Launcher - Preferences"
@@ -64,6 +66,10 @@ fun migratePreferencesToNewVersion(context: Context) {
migratePreferencesFromVersion4(context)
Log.i(TAG, "migration of preferences complete (4 -> ${PREFERENCE_VERSION}).")
}
+ 100 -> {
+ migratePreferencesFromVersion100(context)
+ Log.i(TAG, "migration of preferences complete (100 -> ${PREFERENCE_VERSION}).")
+ }
else -> {
Log.w(
@@ -76,6 +82,7 @@ fun migratePreferencesToNewVersion(context: Context) {
}
} catch (e: Exception) {
Log.e(TAG, "Unable to restore preferences:\n${e.stackTrace}")
+ sendCrashNotification(context, e)
resetPreferences(context)
}
}
@@ -84,12 +91,12 @@ fun resetPreferences(context: Context) {
Log.i(TAG, "Resetting preferences")
LauncherPreferences.clear()
LauncherPreferences.internal().versionCode(PREFERENCE_VERSION)
- deleteAllWidgets(context)
+ context.getAppWidgetHost().deleteHost()
LauncherPreferences.widgets().widgets(
setOf(
ClockWidget(
- (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(),
+ generateInternalId(),
WidgetPosition(1, 3, 10, 4),
WidgetPanel.HOME.id
)
@@ -101,7 +108,7 @@ fun resetPreferences(context: Context) {
LauncherPreferences.widgets().widgets().also {
it.add(
DebugInfoWidget(
- (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(),
+ generateInternalId(),
WidgetPosition(1, 1, 10, 4),
WidgetPanel.HOME.id
)
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt
new file mode 100644
index 0000000..43e4bc7
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt
@@ -0,0 +1,39 @@
+package de.jrpie.android.launcher.preferences.legacy
+
+import android.content.Context
+import de.jrpie.android.launcher.Application
+import de.jrpie.android.launcher.preferences.LauncherPreferences
+import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
+import de.jrpie.android.launcher.widgets.ClockWidget
+import de.jrpie.android.launcher.widgets.DebugInfoWidget
+import de.jrpie.android.launcher.widgets.generateInternalId
+import de.jrpie.android.launcher.widgets.updateWidget
+
+fun migratePreferencesFromVersion100(context: Context) {
+ assert(PREFERENCE_VERSION == 101)
+ assert(LauncherPreferences.internal().versionCode() == 100)
+
+ val widgets = LauncherPreferences.widgets().widgets() ?: setOf()
+ widgets.forEach { widget ->
+ when (widget) {
+ is ClockWidget -> {
+ val id = widget.id
+ val newId = generateInternalId()
+ (context.applicationContext as Application).appWidgetHost.deleteAppWidgetId(id)
+ widget.delete(context)
+ widget.id = newId
+ updateWidget(widget)
+ }
+ is DebugInfoWidget -> {
+ val id = widget.id
+ val newId = generateInternalId()
+ (context.applicationContext as Application).appWidgetHost.deleteAppWidgetId(id)
+ widget.delete(context)
+ widget.id = newId
+ updateWidget(widget)
+ }
+ else -> {}
+ }
+ }
+ LauncherPreferences.internal().versionCode(101)
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt
index a9ab3a1..b13978b 100644
--- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt
@@ -7,19 +7,20 @@ import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
import de.jrpie.android.launcher.widgets.ClockWidget
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.WidgetPosition
+import de.jrpie.android.launcher.widgets.generateInternalId
fun migratePreferencesFromVersion4(context: Context) {
- assert(PREFERENCE_VERSION == 100)
assert(LauncherPreferences.internal().versionCode() < 100)
LauncherPreferences.widgets().widgets(
setOf(
ClockWidget(
- (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(),
+ generateInternalId(),
WidgetPosition(1, 3, 10, 4),
WidgetPanel.HOME.id
)
)
)
LauncherPreferences.internal().versionCode(100)
+ migratePreferencesFromVersion100(context)
}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
index f501107..76bf443 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
@@ -1,16 +1,9 @@
package de.jrpie.android.launcher.ui
-import android.annotation.SuppressLint
-import android.app.Activity
import android.content.SharedPreferences
-import android.content.res.Configuration
import android.content.res.Resources
-import android.os.Build
import android.os.Bundle
-import android.view.KeyEvent
-import android.view.MotionEvent
import android.view.View
-import android.window.OnBackInvokedDispatcher
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
@@ -19,6 +12,7 @@ import de.jrpie.android.launcher.databinding.ActivityHomeBinding
import de.jrpie.android.launcher.openTutorial
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
+import de.jrpie.android.launcher.ui.util.LauncherGestureActivity
/**
* [HomeActivity] is the actual application Launcher,
@@ -32,10 +26,9 @@ import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
* - Setting global variables (preferences etc.)
* - Opening the [TutorialActivity] on new installations
*/
-class HomeActivity : UIObject, Activity() {
+class HomeActivity : UIObject, LauncherGestureActivity() {
private lateinit var binding: ActivityHomeBinding
- private var touchGestureDetector: TouchGestureDetector? = null
private var sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
@@ -54,35 +47,21 @@ class HomeActivity : UIObject, Activity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
+ super.onCreate(savedInstanceState)
super.onCreate()
-
// Initialise layout
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
- // Handle back key / gesture on Android 13+, cf. onKeyDown()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_OVERLAY
- ) {
- handleBack()
- }
- }
binding.buttonFallbackSettings.setOnClickListener {
LauncherAction.SETTINGS.invoke(this)
}
}
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
- touchGestureDetector?.updateScreenSize(windowManager)
- }
-
override fun onStart() {
- super.onStart()
+ super.onStart()
super.onStart()
// If the tutorial was not finished, start it
@@ -93,15 +72,6 @@ class HomeActivity : UIObject, Activity() {
LauncherPreferences.getSharedPreferences()
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
- (application as Application).appWidgetHost.startListening()
-
- }
-
-
-
- override fun onStop() {
- (application as Application).appWidgetHost.stopListening()
- super.onStop()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -112,7 +82,6 @@ class HomeActivity : UIObject, Activity() {
}
}
-
private fun updateSettingsFallbackButtonVisibility() {
// If µLauncher settings can not be reached from any action bound to an enabled gesture,
// show the fallback button.
@@ -131,81 +100,42 @@ class HomeActivity : UIObject, Activity() {
return modifyTheme(super.getTheme())
}
+ override fun onPause() {
+ try {
+ (application as Application).appWidgetHost.stopListening()
+ } catch (e: Exception) {
+ // Throws a NullPointerException on Android 12 an earlier, see #172
+ e.printStackTrace()
+ }
+ super.onPause()
+ }
+
override fun onResume() {
super.onResume()
-
- /* This should be initialized in onCreate()
- However on some devices there seems to be a bug where the touchGestureDetector
- is not working properly after resuming the app.
- Reinitializing the touchGestureDetector every time the app is resumed might help to fix that.
- (see issue #138)
- */
- touchGestureDetector = TouchGestureDetector(
- this, 0, 0,
- LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
- ).also {
- it.updateScreenSize(windowManager)
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- binding.root.setOnApplyWindowInsetsListener { _, windowInsets ->
- @Suppress("deprecation") // required to support API 29
- val insets = windowInsets.systemGestureInsets
- touchGestureDetector?.setSystemGestureInsets(insets)
-
- windowInsets
- }
- }
updateSettingsFallbackButtonVisibility()
binding.homeWidgetContainer.updateWidgets(this@HomeActivity,
LauncherPreferences.widgets().widgets()
)
+
+ (application as Application).appWidgetHost.startListening()
}
+
override fun onDestroy() {
LauncherPreferences.getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
super.onDestroy()
}
- @SuppressLint("GestureBackNavigation")
- override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
- when (keyCode) {
- KeyEvent.KEYCODE_BACK -> {
- // Only used pre Android 13, cf. onBackInvokedDispatcher
- handleBack()
- }
-
- KeyEvent.KEYCODE_VOLUME_UP -> {
- if (Action.forGesture(Gesture.VOLUME_UP) == LauncherAction.VOLUME_UP) {
- // Let the OS handle the key event. This works better with some custom ROMs
- // and apps like Samsung Sound Assistant.
- return false
- }
- Gesture.VOLUME_UP(this)
- }
-
- KeyEvent.KEYCODE_VOLUME_DOWN -> {
- if (Action.forGesture(Gesture.VOLUME_DOWN) == LauncherAction.VOLUME_DOWN) {
- // see above
- return false
- }
- Gesture.VOLUME_DOWN(this)
- }
- }
- return true
- }
-
- override fun onTouchEvent(event: MotionEvent): Boolean {
- touchGestureDetector?.onTouchEvent(event)
- return true
- }
-
- private fun handleBack() {
+ override fun handleBack() {
Gesture.BACK(this)
}
+ override fun getRootView(): View {
+ return binding.root
+ }
+
override fun isHomeScreen(): Boolean {
return true
}
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt
new file mode 100644
index 0000000..5d95769
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt
@@ -0,0 +1,57 @@
+package de.jrpie.android.launcher.ui
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import de.jrpie.android.launcher.R
+import de.jrpie.android.launcher.copyToClipboard
+import de.jrpie.android.launcher.databinding.ActivityReportCrashBinding
+import de.jrpie.android.launcher.getDeviceInfo
+import de.jrpie.android.launcher.openInBrowser
+import de.jrpie.android.launcher.writeEmail
+
+const val EXTRA_CRASH_LOG = "crashLog"
+
+class ReportCrashActivity : AppCompatActivity() {
+ // We don't know what caused the crash, so this Activity should use as little functionality as possible.
+ // In particular it is not a UIObject (and hence looks quite ugly)
+ private lateinit var binding: ActivityReportCrashBinding
+ private var report: String? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Initialise layout
+ binding = ActivityReportCrashBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ setTitle(R.string.report_crash_title)
+ setSupportActionBar(binding.reportCrashAppbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(false)
+
+ report = intent.getStringExtra(EXTRA_CRASH_LOG)
+
+ binding.reportCrashButtonCopy.setOnClickListener {
+ copyToClipboard(this,
+ "Device Info:\n${getDeviceInfo()}\n\nCrash Log:\n${report}")
+ }
+
+ binding.reportCrashButtonMail.setOnClickListener {
+ writeEmail(
+ this,
+ getString(R.string.settings_meta_report_bug_mail),
+ "Crash in μLauncher",
+ "Hi!\nUnfortunately, μLauncher crashed:\n" +
+ "\nDevice Info\n\n${getDeviceInfo()}\n\n" +
+ "\nCrash Log\n\n${report}\n" +
+ "\nAdditional Information\n\n" +
+ "[Please add additional information: What did you do when the crash happened? Do you know how to trigger it? ... ]"
+ )
+ }
+ binding.reportCrashButtonReport.setOnClickListener {
+ openInBrowser(
+ getString(R.string.settings_meta_report_bug_link),
+ this
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt
index e066da2..1a0e802 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt
@@ -1,39 +1,18 @@
package de.jrpie.android.launcher.ui.settings.meta
-import android.content.Context
+import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Warning
-import androidx.compose.material3.*
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.DialogProperties
+import android.widget.Button
+import android.widget.TextView
import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.copyToClipboard
+import de.jrpie.android.launcher.databinding.SettingsMetaBinding
import de.jrpie.android.launcher.getDeviceInfo
import de.jrpie.android.launcher.openInBrowser
import de.jrpie.android.launcher.openTutorial
@@ -51,21 +30,13 @@ import de.jrpie.android.launcher.ui.UIObject
*/
class SettingsFragmentMeta : Fragment(), UIObject {
+ private lateinit var binding: SettingsMetaBinding
override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
+ inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- return ComposeView(requireContext()).apply {
- setContent {
- MaterialTheme {
- SettingsMetaScreen(
- context = requireContext(),
- onResetConfirmed = { requireActivity().finish() }
- )
- }
- }
- }
+ binding = SettingsMetaBinding.inflate(inflater, container, false)
+ return binding.root
}
override fun onStart() {
@@ -74,215 +45,102 @@ class SettingsFragmentMeta : Fragment(), UIObject {
}
override fun setOnClicks() {
- // No longer needed as click handlers are defined in Compose
- }
-}
-// Data class to represent a settings action
-private data class SettingsAction(
- val textResId: Int,
- val onClick: (Context) -> Unit
-)
-
-// Composable for the settings meta screen
-@Composable
-fun SettingsMetaScreen(
- context: Context,
- onResetConfirmed: () -> Unit
-) {
- val openAlertDialog = remember { mutableStateOf(false) }
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(16.dp)
- ) {
- SettingsButtonList(context, openAlertDialog)
- if (openAlertDialog.value) {
- AlertDialogResetSettings(
- onDismissRequest = { openAlertDialog.value = false },
- onConfirmation = {
- openAlertDialog.value = false
- resetPreferences(context)
- onResetConfirmed()
- },
- dialogTitle = stringResource(R.string.settings_meta_reset),
- dialogText = stringResource(R.string.settings_meta_reset_confirm),
- icon = Icons.Default.Warning
- )
- }
- }
-}
-
-@Preview
-@Composable
-fun SettingsMetaScreenPreview() {
- SettingsMetaScreen(
- context = LocalContext.current,
- onResetConfirmed = {}
- )
-}
-
-// Composable for the scrollable button list and version number
-@Composable
-private fun SettingsButtonList(
- context: Context,
- openAlertDialog: MutableState
-) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- val actions = listOf(
- SettingsAction(R.string.settings_meta_show_tutorial) { openTutorial(it) },
- SettingsAction(R.string.settings_meta_reset) { openAlertDialog.value = true },
- SettingsAction(R.string.settings_meta_view_code) {
- openInBrowser(it.getString(R.string.settings_meta_link_github), it)
- },
- SettingsAction(R.string.settings_meta_report_bug) {
- openInBrowser(it.getString(R.string.settings_meta_report_bug_link), it)
- },
- SettingsAction(R.string.settings_meta_join_chat) {
- openInBrowser(it.getString(R.string.settings_meta_chat_url), it)
- },
- SettingsAction(R.string.settings_meta_fork_contact) {
- openInBrowser(it.getString(R.string.settings_meta_fork_contact_url), it)
- },
- SettingsAction(R.string.settings_meta_donate) {
- openInBrowser(it.getString(R.string.settings_meta_donate_url), it)
- },
- SettingsAction(R.string.settings_meta_privacy) {
- openInBrowser(it.getString(R.string.settings_meta_privacy_url), it)
- },
- SettingsAction(R.string.settings_meta_licenses) {
- it.startActivity(Intent(it, LegalInfoActivity::class.java))
- }
- )
-
- actions.forEachIndexed { index, action ->
- SettingsButton(
- text = stringResource(action.textResId),
- onClick = { action.onClick(context) }
- )
- if (index == 1 || index == 3 || index == 6) {
- SettingsButtonSpacer()
+ fun bindURL(view: View, urlRes: Int) {
+ view.setOnClickListener {
+ openInBrowser(
+ getString(urlRes),
+ requireContext()
+ )
}
}
- // Version number at the bottom of buttons
- Text(
- text = BuildConfig.VERSION_NAME,
- style = MaterialTheme.typography.bodyMedium,
- fontWeight = FontWeight.Bold,
- textAlign = TextAlign.End,
- color = colorResource(R.color.finnmglasTheme_text_color),
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp, end = 8.dp)
- .clickable {
- val deviceInfo = getDeviceInfo()
- copyToClipboard(context, deviceInfo)
+
+ binding.settingsMetaButtonViewTutorial.setOnClickListener {
+ openTutorial(requireContext())
+ }
+
+ // prompting for settings-reset confirmation
+ binding.settingsMetaButtonResetSettings.setOnClickListener {
+ AlertDialog.Builder(this.requireContext(), R.style.AlertDialogCustom)
+ .setTitle(getString(R.string.settings_meta_reset))
+ .setMessage(getString(R.string.settings_meta_reset_confirm))
+ .setPositiveButton(
+ android.R.string.ok
+ ) { _, _ ->
+ resetPreferences(this.requireContext())
+ requireActivity().finish()
}
- )
- Spacer(modifier = Modifier.height(48.dp))
+ .setNegativeButton(android.R.string.cancel, null)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .show()
+ }
+
+
+ // view code
+ bindURL(binding.settingsMetaButtonViewCode, R.string.settings_meta_link_github)
+
+ // view documentation
+ bindURL(binding.settingsMetaButtonViewDocs, R.string.settings_meta_link_docs)
+
+ // report a bug
+ binding.settingsMetaButtonReportBug.setOnClickListener {
+ val deviceInfo = getDeviceInfo()
+ AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
+ setView(R.layout.dialog_report_bug)
+ setTitle(R.string.dialog_report_bug_title)
+ setPositiveButton(R.string.dialog_report_bug_create_report) { _, _ ->
+ openInBrowser(
+ getString(R.string.settings_meta_report_bug_link),
+ requireContext()
+ )
+ }
+ setNegativeButton(R.string.dialog_cancel) { _, _ -> }
+ }.create().also { it.show() }.apply {
+ val info = findViewById(R.id.dialog_report_bug_device_info)
+ val buttonClipboard = findViewById