Merge branch 'master' into migrate-build-files-to-kotlin

This commit is contained in:
Luke Wass 2025-05-27 16:57:08 -05:00
commit 8882583f77
54 changed files with 1622 additions and 565 deletions

View file

@ -8,6 +8,7 @@
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" />
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".Application"
@ -19,6 +20,7 @@
android:supportsRtl="true"
android:theme="@style/launcherBaseTheme"
tools:ignore="UnusedAttribute">
<activity
android:name=".ui.widgets.manage.ManageWidgetPanelsActivity"
android:exported="false" />
@ -80,6 +82,9 @@
<activity
android:name=".ui.LegalInfoActivity"
android:exported="false" />
<activity
android:name=".ui.ReportCrashActivity"
android:exported="false" />
<receiver
android:name=".actions.lock.LauncherDeviceAdmin"
@ -110,5 +115,4 @@
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>

View file

@ -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<AbstractAppInfo, String> {
@ -170,10 +176,4 @@ class Application : android.app.Application() {
apps.postValue(getApps(packageManager, applicationContext))
}
}
override fun onTerminate() {
appWidgetHost.stopListening()
super.onTerminate()
}
}

View file

@ -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)))
}

View file

@ -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")
}
}

View file

@ -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

View file

@ -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
)

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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<Activity>.onCreate(savedInstanceState)
super<LauncherGestureActivity>.onCreate(savedInstanceState)
super<UIObject>.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<Activity>.onStart()
super<LauncherGestureActivity>.onStart()
super<UIObject>.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
}

View file

@ -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
)
}
}
}

View file

@ -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<Boolean>
) {
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<TextView>(R.id.dialog_report_bug_device_info)
val buttonClipboard = findViewById<Button>(R.id.dialog_report_bug_button_clipboard)
val buttonSecurity = findViewById<Button>(R.id.dialog_report_bug_button_security)
info.text = deviceInfo
buttonClipboard.setOnClickListener {
copyToClipboard(requireContext(), deviceInfo)
}
info.setOnClickListener {
copyToClipboard(requireContext(), deviceInfo)
}
buttonSecurity.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_report_vulnerability_link),
requireContext()
)
}
}
}
// join chat
bindURL(binding.settingsMetaButtonJoinChat, R.string.settings_meta_chat_url)
// contact developer
// bindURL(binding.settingsMetaButtonContact, R.string.settings_meta_contact_url)
// contact fork developer
bindURL(binding.settingsMetaButtonForkContact, R.string.settings_meta_fork_contact_url)
// donate
bindURL(binding.settingsMetaButtonDonate, R.string.settings_meta_donate_url)
// privacy policy
bindURL(binding.settingsMetaButtonPrivacy, R.string.settings_meta_privacy_url)
// legal info
binding.settingsMetaButtonLicenses.setOnClickListener {
startActivity(Intent(this.context, LegalInfoActivity::class.java))
}
// version
binding.settingsMetaTextVersion.text = BuildConfig.VERSION_NAME
binding.settingsMetaTextVersion.setOnClickListener {
val deviceInfo = getDeviceInfo()
copyToClipboard(requireContext(), deviceInfo)
}
}
}
@Preview
@Composable
fun SettingsButtonListPreview() {
SettingsButtonList(
context = LocalContext.current,
openAlertDialog = remember { mutableStateOf(false) })
}
// Composable for a settings button
@Composable
fun SettingsButton(
text: String,
onClick: () -> Unit
) {
Button(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(8.dp),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.buttonColors(
containerColor = colorResource(R.color.finnmglasTheme_accent_color),
contentColor = colorResource(R.color.finnmglasTheme_text_color)
)
) {
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.align(Alignment.CenterVertically),
color = colorResource(R.color.finnmglasTheme_text_color)
)
}
}
@Preview
@Composable
fun SettingsButtonPreview() {
SettingsButton(text = "Here's a button preview", onClick = {})
}
// Composable for spacing between buttons
@Composable
fun SettingsButtonSpacer() {
Spacer(modifier = Modifier.height(16.dp))
}
// Composable for the reset settings alert dialog
@Composable
fun AlertDialogResetSettings(
onDismissRequest: () -> Unit,
onConfirmation: () -> Unit,
dialogTitle: String,
dialogText: String,
icon: ImageVector
) {
AlertDialog(
icon = {
Icon(
imageVector = icon,
contentDescription = stringResource(android.R.string.dialog_alert_title),
tint = MaterialTheme.colorScheme.error
)
},
title = {
Text(text = dialogTitle)
},
text = {
Text(text = dialogText)
},
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onConfirmation) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(android.R.string.cancel))
}
},
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
)
}
@Preview
@Composable
fun AlertDialogResetSettingsPreview() {
AlertDialogResetSettings(
onDismissRequest = {},
onConfirmation = {},
dialogTitle = "Reset settings",
dialogText = "Are you sure you want to reset all settings?",
icon = Icons.Default.Warning
)
}

View file

@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.BuildConfig.VERSION_CODE
import de.jrpie.android.launcher.databinding.Tutorial5FinishBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.requestNotificationPermission
import de.jrpie.android.launcher.setDefaultHomeScreen
import de.jrpie.android.launcher.ui.UIObject
@ -31,8 +32,10 @@ class TutorialFragment5Finish : Fragment(), UIObject {
override fun onStart() {
super<Fragment>.onStart()
super<UIObject>.onStart()
requestNotificationPermission(requireActivity())
}
override fun setOnClicks() {
super.setOnClicks()
binding.tutorialFinishButtonStart.setOnClickListener { finishTutorial() }
@ -44,6 +47,7 @@ class TutorialFragment5Finish : Fragment(), UIObject {
LauncherPreferences.internal().startedTime(System.currentTimeMillis() / 1000L)
}
context?.let { setDefaultHomeScreen(it, checkDefault = true) }
activity?.finish()
}
}

View file

@ -0,0 +1,104 @@
package de.jrpie.android.launcher.ui.util
import android.annotation.SuppressLint
import android.app.Activity
import android.content.res.Configuration
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.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.TouchGestureDetector
/**
* An activity with a [TouchGestureDetector] as well as handling of volume and back keys set up.
*/
abstract class LauncherGestureActivity: Activity() {
protected var touchGestureDetector: TouchGestureDetector? = null
override fun onTouchEvent(event: MotionEvent): Boolean {
touchGestureDetector?.onTouchEvent(event)
return true
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Handle back key / gesture on Android 13+, cf. onKeyDown()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_OVERLAY
) {
handleBack()
}
}
}
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) {
getRootView()?.setOnApplyWindowInsetsListener { _, windowInsets ->
@Suppress("deprecation") // required to support API 29
val insets = windowInsets.systemGestureInsets
touchGestureDetector?.setSystemGestureInsets(insets)
windowInsets
}
}
}
@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 onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
touchGestureDetector?.updateScreenSize(windowManager)
}
protected abstract fun getRootView(): View?
protected abstract fun handleBack()
}

View file

@ -8,9 +8,11 @@ import androidx.core.view.isVisible
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.databinding.WidgetClockBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.widgets.WidgetPanel
import java.util.Locale
class ClockView(context: Context, attrs: AttributeSet? = null, val appWidgetId: Int): ConstraintLayout(context, attrs) {
class ClockView(context: Context, attrs: AttributeSet? = null, val appWidgetId: Int, val panelId: Int): ConstraintLayout(context, attrs) {
constructor(context: Context, attrs: AttributeSet?): this(context, attrs, WidgetPanel.HOME.id, -1)
val binding: WidgetClockBinding = WidgetClockBinding.inflate(LayoutInflater.from(context), this, true)
init {
@ -57,7 +59,7 @@ class ClockView(context: Context, attrs: AttributeSet? = null, val appWidgetId:
binding.clockUpperView.format12Hour = upperFormat
}
fun setOnClicks() {
private fun setOnClicks() {
binding.clockUpperView.setOnClickListener {
if (LauncherPreferences.clock().flipDateTime()) {
Gesture.TIME(context)
@ -74,7 +76,4 @@ class ClockView(context: Context, attrs: AttributeSet? = null, val appWidgetId:
}
}
}
}

View file

@ -1,26 +1,31 @@
package de.jrpie.android.launcher.ui.widgets
import android.app.Activity
import android.content.res.Resources
import android.os.Bundle
import android.view.View
import androidx.core.view.ViewCompat
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.databinding.ActivityWidgetPanelBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.ui.util.LauncherGestureActivity
import de.jrpie.android.launcher.ui.widgets.manage.EXTRA_PANEL_ID
import de.jrpie.android.launcher.widgets.WidgetPanel
class WidgetPanelActivity : Activity(), UIObject {
lateinit var binding: ActivityWidgetPanelBinding
private var widgetPanelId: Int = WidgetPanel.HOME.id
class WidgetPanelActivity : LauncherGestureActivity(), UIObject {
var binding: ActivityWidgetPanelBinding? = null
var widgetPanelId: Int = WidgetPanel.HOME.id
override fun onCreate(savedInstanceState: Bundle?) {
super<Activity>.onCreate(savedInstanceState)
super<LauncherGestureActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id)
val binding = ActivityWidgetPanelBinding.inflate(layoutInflater)
setContentView(binding.root)
widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id)
// The widget container should extend below the status and navigation bars,
// so let's set an empty WindowInsetsListener to prevent it from being moved.
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
@ -55,10 +60,33 @@ class WidgetPanelActivity : Activity(), UIObject {
}
override fun onStart() {
super<Activity>.onStart()
super<LauncherGestureActivity>.onStart()
super<UIObject>.onStart()
}
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()
(application as Application).appWidgetHost.startListening()
}
override fun getRootView(): View? {
return binding?.root
}
override fun handleBack() {
finish()
}
override fun isHomeScreen(): Boolean {
return true
}

View file

@ -9,6 +9,7 @@ import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.databinding.ActivityManageWidgetPanelsBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences

View file

@ -92,13 +92,24 @@ class ManageWidgetsActivity : UIObject, Activity() {
}
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()
(application as Application).appWidgetHost.startListening()
binding.manageWidgetsContainer.updateWidgets(
this,
LauncherPreferences.widgets().widgets()
)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@ -124,10 +135,6 @@ class ManageWidgetsActivity : UIObject, Activity() {
val appWidgetHost = (application as Application).appWidgetHost
startActivityForResult(
Intent(this, SelectWidgetActivity::class.java).also {
it.putExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetHost.allocateAppWidgetId()
)
it.putExtra(
EXTRA_PANEL_ID,
panelId
@ -140,13 +147,17 @@ class ManageWidgetsActivity : UIObject, Activity() {
private fun createWidget(data: Intent) {
Log.i("Launcher", "creating widget")
val appWidgetManager = (application as Application).appWidgetManager
val appWidgetHost = (application as Application).appWidgetHost
val appWidgetId = data.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return
val provider = appWidgetManager.getAppWidgetInfo(appWidgetId)
val display = windowManager.defaultDisplay
val widgetInfo = appWidgetManager.getAppWidgetInfo(appWidgetId)
if (widgetInfo == null) {
Log.w("Launcher", "can't access widget")
appWidgetHost.deleteAppWidgetId(appWidgetId)
return
}
val position = WidgetPosition.findFreeSpace(
WidgetPanel.byId(panelId),
@ -154,7 +165,7 @@ class ManageWidgetsActivity : UIObject, Activity() {
max(3, (GRID_SIZE * (widgetInfo.minHeight) / display.height.toFloat()).roundToInt())
)
val widget = AppWidget(appWidgetId, position, panelId, provider)
val widget = AppWidget(appWidgetId, position, panelId, widgetInfo)
LauncherPreferences.widgets().widgets(
(LauncherPreferences.widgets().widgets() ?: HashSet()).also {
it.add(widget)

View file

@ -14,6 +14,7 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.databinding.ActivitySelectWidgetBinding
import de.jrpie.android.launcher.ui.UIObject
@ -24,7 +25,7 @@ import de.jrpie.android.launcher.widgets.LauncherWidgetProvider
import de.jrpie.android.launcher.widgets.WidgetPanel
import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission
import de.jrpie.android.launcher.widgets.getAppWidgetHost
import de.jrpie.android.launcher.widgets.generateInternalId
import de.jrpie.android.launcher.widgets.getAppWidgetProviders
import de.jrpie.android.launcher.widgets.updateWidget
@ -38,12 +39,13 @@ private const val REQUEST_WIDGET_PERMISSION = 29
*/
class SelectWidgetActivity : AppCompatActivity(), UIObject {
lateinit var binding: ActivitySelectWidgetBinding
var widgetId: Int = -1
var widgetPanelId: Int = WidgetPanel.HOME.id
private fun tryBindWidget(info: LauncherWidgetProvider) {
when (info) {
is LauncherAppWidgetProvider -> {
val widgetId =
(applicationContext as Application).appWidgetHost.allocateAppWidgetId()
if (bindAppWidgetOrRequestPermission(
this,
info.info,
@ -62,7 +64,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject {
}
}
is LauncherClockWidgetProvider -> {
updateWidget(ClockWidget(widgetId, WidgetPosition(0, 4, 12, 3), widgetPanelId))
updateWidget(ClockWidget(generateInternalId(), WidgetPosition(0, 4, 12, 3), widgetPanelId))
finish()
}
}
@ -81,11 +83,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject {
setContentView(binding.root)
widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
widgetPanelId = intent.getIntExtra(EXTRA_PANEL_ID, WidgetPanel.HOME.id)
if (widgetId == -1) {
widgetId = getAppWidgetHost().allocateAppWidgetId()
}
val viewManager = LinearLayoutManager(this)
val viewAdapter = SelectWidgetRecyclerAdapter()

View file

@ -113,7 +113,7 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe
height
)
selectedWidgetOverlayView = view
selectedWidgetView = widgetViewById[view.widgetId] ?: return true
selectedWidgetView = widgetViewById[view.widgetId]
startWidgetPosition = position
val positionInView = start.minus(Point(position.left, position.top))

View file

@ -41,7 +41,7 @@ class AppWidget(
id,
position,
panelId,
false,
panelId != WidgetPanel.HOME.id,
widgetProviderInfo.provider.packageName,
widgetProviderInfo.provider.className,
widgetProviderInfo.profile.hashCode()
@ -78,6 +78,10 @@ class AppWidget(
override fun createView(activity: Activity): AppWidgetHostView? {
val providerInfo = activity.getAppWidgetManager().getAppWidgetInfo(id) ?: return null
/* TODO: if providerInfo is null, the corresponding app was probably uninstalled.
There does not seem to be a way to recover the widget when the app is installed again,
hence it should be deleted. */
val view = activity.getAppWidgetHost()
.createView(activity, this.id, providerInfo)

View file

@ -12,14 +12,14 @@ import kotlinx.serialization.Serializable
@Serializable
@SerialName("widget:clock")
class ClockWidget(
override val id: Int,
override var id: Int,
override var position: WidgetPosition,
override val panelId: Int,
override var allowInteraction: Boolean = true
) : Widget() {
override fun createView(activity: Activity): View? {
return ClockView(activity, null, id)
override fun createView(activity: Activity): View {
return ClockView(activity, null, id, panelId)
}
override fun findView(views: Sequence<View>): ClockView? {

View file

@ -12,7 +12,7 @@ import kotlinx.serialization.Serializable
@Serializable
@SerialName("widget:debuginfo")
class DebugInfoWidget(
override val id: Int,
override var id: Int,
override var position: WidgetPosition,
override val panelId: Int,
override var allowInteraction: Boolean = true

View file

@ -27,7 +27,9 @@ sealed class Widget {
abstract fun configure(activity: Activity, requestCode: Int)
fun delete(context: Context) {
context.getAppWidgetHost().deleteAppWidgetId(id)
if (id >= 0) {
context.getAppWidgetHost().deleteAppWidgetId(id)
}
LauncherPreferences.widgets().widgets(
LauncherPreferences.widgets().widgets()?.also {

View file

@ -13,12 +13,8 @@ import android.os.UserManager
import android.util.Log
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlin.math.min
fun deleteAllWidgets(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.getAppWidgetHost().appWidgetIds.forEach { AppWidget(it).delete(context) }
}
}
/**
* Tries to bind [providerInfo] to the id [id].
@ -29,12 +25,9 @@ fun deleteAllWidgets(context: Context) {
*
* @return true iff the app widget was bound successfully.
*/
fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, id: Int, requestCode: Int? = null): Boolean {
val appWidgetId = if(id == -1) {
activity.getAppWidgetHost().allocateAppWidgetId()
} else { id }
fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, appWidgetId: Int, requestCode: Int? = null): Boolean {
Log.i("Launcher", "Binding new widget ${appWidgetId}")
Log.i("Launcher", "Binding new widget $appWidgetId")
if (!activity.getAppWidgetManager().bindAppWidgetIdIfAllowed(
appWidgetId,
providerInfo.provider
@ -79,6 +72,13 @@ fun updateWidget(widget: Widget) {
)
}
// TODO: this needs to be improved
fun generateInternalId(): Int {
val minId = min(-5,(LauncherPreferences.widgets().widgets() ?: setOf()).minOfOrNull { it.id } ?: 0)
return minId -1
}
fun updateWidgetPanel(widgetPanel: WidgetPanel) {
LauncherPreferences.widgets().customPanels(
(LauncherPreferences.widgets().customPanels() ?: setOf())
@ -92,4 +92,4 @@ fun Context.getAppWidgetHost(): AppWidgetHost {
}
fun Context.getAppWidgetManager(): AppWidgetManager {
return (this.applicationContext as Application).appWidgetManager
}
}

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
</vector>

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ui.ReportCrashActivity">
<com.google.android.material.appbar.AppBarLayout
android:background="@null"
app:elevation="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/report_crash_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<de.jrpie.android.launcher.ui.util.HtmlTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/crash_info" />
<Button
android:id="@+id/report_crash_button_copy"
android:text="@string/report_crash_button_copy"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Space
android:layout_width="match_parent"
android:layout_height="20dp" />
<Button
android:id="@+id/report_crash_button_mail"
android:text="@string/report_crash_button_mail"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/report_crash_button_report"
android:text="@string/report_crash_button_report"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -6,55 +6,96 @@
android:id="@+id/list_apps_row_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15sp">
android:layout_marginHorizontal="30sp">
<ImageView
android:id="@+id/list_widgets_row_icon"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:src="@mipmap/ic_launcher_round"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/list_widgets_row_name"
android:layout_width="0dp"
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10sp"
android:layout_marginEnd="10sp"
android:gravity="start"
android:text=""
android:textSize="20sp"
tools:text="some widget"
app:layout_constraintStart_toEndOf="@id/list_widgets_row_icon"
app:cardBackgroundColor="?cardBackgroundColor"
app:cardElevation="8dp"
app:cardCornerRadius="12dp"
app:cardUseCompatPadding="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/list_widgets_row_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10sp"
android:gravity="start"
android:text=""
android:textSize="12sp"
app:layout_constraintStart_toStartOf="@+id/list_widgets_row_name"
app:layout_constraintTop_toBottomOf="@+id/list_widgets_row_name"
tools:text="a longer description of the widget" />
<ImageView
android:id="@+id/list_widgets_row_preview"
android:layout_width="0dp"
android:maxHeight="100dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_height="100dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/list_widgets_row_description"
tools:src="@mipmap/ic_launcher_round"
tools:ignore="ContentDescription" />
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_margin="10dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/icon_title_description_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/list_widgets_row_icon"
android:layout_width="40dp"
android:layout_height="40dp"
tools:src="@mipmap/ic_launcher_round"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/list_widgets_row_name"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginStart="10sp"
android:layout_marginEnd="10sp"
android:text=""
style="@style/TextAppearance.Material3.HeadlineSmall"
tools:text="Some Widget"
app:layout_constraintStart_toEndOf="@id/list_widgets_row_icon"
app:layout_constraintTop_toTopOf="@id/list_widgets_row_icon"
app:layout_constraintBottom_toBottomOf="@id/list_widgets_row_icon"/>
<TextView
android:id="@+id/list_widgets_row_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text=""
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/list_widgets_row_name"
app:layout_constraintTop_toBottomOf="@+id/list_widgets_row_name"
tools:text="A longer description of the widget that may take up multiple lines." />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
app:layout_constraintTop_toBottomOf="@id/icon_title_description_container">
<ImageView
android:id="@+id/list_widgets_row_preview"
android:layout_width="match_parent"
android:layout_height="100dp"
android:maxHeight="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/list_widgets_row_icon"
tools:src="@mipmap/ic_launcher_round"
tools:ignore="ContentDescription,NotSibling" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -41,6 +41,13 @@
android:text="@string/settings_meta_view_code"
android:textAllCaps="false" />
<Button
android:id="@+id/settings_meta_button_view_docs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_meta_view_docs"
android:textAllCaps="false" />
<Button
android:id="@+id/settings_meta_button_report_bug"
android:layout_width="match_parent"

View file

@ -33,17 +33,27 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tutorial_setup_title" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/tutorial_setup_actions_rview_fragment"
android:name="de.jrpie.android.launcher.ui.settings.actions.SettingsFragmentActionsRecycler"
android:layout_width="0dp"
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="16dp"
app:cardElevation="8dp"
app:cardBackgroundColor="?cardBackgroundColor"
app:cardCornerRadius="12dp"
app:cardUseCompatPadding="true"
app:layout_constraintTop_toBottomOf="@id/tutorial_setup_subtitle"
app:layout_constraintBottom_toTopOf="@id/tutorial_setup_text_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tutorial_setup_subtitle" />
app:layout_constraintEnd_toEndOf="parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/tutorial_setup_actions_rview_fragment"
android:name="de.jrpie.android.launcher.ui.settings.actions.SettingsFragmentActionsRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginVertical="4dp"
android:layout_marginStart="10dp"/>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/tutorial_setup_text_bottom"

View file

@ -97,9 +97,9 @@
<string name="settings_display_screen_timeout_disabled">Bildschirm nicht ausschalten</string>
<string name="settings_display_rotate_screen">Bildschirm drehen</string>
<string name="settings_launcher_section_functionality">Funktionalität</string>
<string name="settings_enabled_gestures_double_swipe">Doppelte Wischaktionen</string>
<string name="settings_enabled_gestures_double_swipe">Doppelte Wischgesten</string>
<string name="settings_enabled_gestures_double_swipe_summary">Mit zwei Fingern wischen</string>
<string name="settings_enabled_gestures_edge_swipe">Kantenaktionen</string>
<string name="settings_enabled_gestures_edge_swipe">Kantengesten</string>
<string name="settings_enabled_gestures_edge_swipe_edge_width">Kantenbreite</string>
<string name="settings_functionality_auto_launch">Suchergebnis starten</string>
<string name="settings_functionality_search_web_summary">Beim Durchsuchen der Apps Enter drücken, um stattdessen im Internet zu suchen.</string>

View file

@ -93,9 +93,9 @@
<string name="settings_clock_show_seconds">Mostra secondi</string>
<string name="settings_launcher_section_date_time"><![CDATA[Data e ora]]></string>
<string name="settings_enabled_gestures_double_swipe_summary">Scorri con due dita</string>
<string name="settings_enabled_gestures_double_swipe">Azioni a due dita</string>
<string name="settings_enabled_gestures_double_swipe">Scorrimento a due dita</string>
<string name="settings_functionality_auto_launch">Apri il risultato della ricerca</string>
<string name="settings_enabled_gestures_edge_swipe">Azioni sui bordi dello schermo</string>
<string name="settings_enabled_gestures_edge_swipe">Scorrimento sui bordi dello schermo</string>
<string name="settings_enabled_gestures_edge_swipe_summary">Scorri sui bordi dello schermo</string>
<string name="settings_enabled_gestures_edge_swipe_edge_width">Larghezza bordo</string>
<string name="settings_meta_view_code">Codice sorgente</string>
@ -287,4 +287,29 @@
<string name="tutorial_app_list_text">Puoi cercare rapidamente tra tutte le app nella lista app.\n\nScorri su per la lista o associa ad un gesto diverso.</string>
<string name="alert_recent_apps_failed">Errore: impossibile mostrare le app recenti. (Se hai appena aggiornato l\'app, prova a disabilitare e riabilitare il servizio di accessibilità dalle impostazioni del telefono)</string>
<string name="settings_functionality_auto_close_keyboard">Chiudi la tastiera durante lo scorrimento</string>
<string name="settings_widgets_widgets">Gestione widget</string>
<string name="widget_menu_remove">Rimuovi</string>
<string name="settings_widgets_custom_panels">Gestione pannelli widget</string>
<string name="select_widget_title">Scegli widget</string>
<string name="widget_menu_configure">Configura</string>
<string name="widget_menu_enable_interaction">Abilita interazione</string>
<string name="widget_menu_disable_interaction">Disabilita interazione</string>
<string name="widget_clock_label">Orologio</string>
<string name="widget_clock_description">Orologio predefinito di μLauncher</string>
<string name="manage_widget_panels_delete">Elimina</string>
<string name="manage_widget_panels_rename">Rinomina</string>
<string name="widget_panel_default_name">Pannello widget #%1$d</string>
<string name="dialog_ok">Ok</string>
<string name="widget_panels_title">Pannelli widget</string>
<plurals name="widget_panel_number_of_widgets">
<item quantity="one">Contiene %1$d widget.</item>
<item quantity="many">Contiene %1$d widget.</item>
<item quantity="other">Contiene %1$d widget.</item>
</plurals>
<string name="dialog_create_widget_panel_title">Crea nuovo pannello widget</string>
<string name="list_other_open_widget_panel">Apri pannello widget</string>
<string name="alert_widget_panel_not_found">Questo pannello widget non esiste più.</string>
<string name="settings_launcher_section_widgets">Widget</string>
<string name="dialog_select_widget_panel_title">Seleziona pannello widget</string>
<string name="dialog_select_widget_panel_info_no_panels"><![CDATA[Nessun pannello widget trovato. Puoi creare pannelli widget da Impostazioni > Launcher > Gestione Pannelli Widget.]]></string>
</resources>

View file

@ -1,16 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
-
- Home - Pradžia
-
-->
<string name="alert_cant_open_title">Nepavyksta paleisti programėlės</string>
<string name="alert_cant_open_message">Norite pakeisti nustatymus?</string>
<string name="toast_cant_open_message">Atidarykite nustatymus norėdami pasirinkti šio gesto veiksmą</string>
<!--
-
- Settings
-
-->
<string name="settings_title">Nustatymai</string>
<string name="settings_tab_actions">Veiksmai</string>
<string name="settings_tab_launcher">Paleidimo programėlė</string>
<string name="settings_tab_meta">Apie</string>
<!--
-
- Settings : Apps
-
-->
<string name="settings_gesture_back">Atgal</string>
<string name="settings_gesture_description_back">Grįžimo mygtukas / grįžimo gestas</string>
<string name="settings_gesture_up">Aukštyn</string>
<string name="settings_gesture_description_up">Perbraukimas aukštyn</string>
<string name="settings_gesture_tap_up">Bakstelėkite + aukštyn</string>
<string name="alert_cant_open_title">Nepavyksta paleisti programėlės</string>
<string name="toast_cant_open_message">Atidarykite nustatymus norėdami pasirinkti šio gesto veiksmą</string>
<string name="settings_gesture_up">Aukštyn</string>
<string name="settings_gesture_description_tap_up">Bakstelėjimas ir perbraukimas aukštyn</string>
<string name="settings_gesture_double_up">Dvigubai aukštyn</string>
<string name="settings_gesture_description_double_up">Perbraukite aukštyn dviem pirštais</string>
<string name="settings_gesture_down">Žemyn</string>
<string name="settings_gesture_description_down">Perbraukite žemyn</string>
<string name="settings_gesture_tap_down">Bakstelėkite + žemyn</string>
<string name="settings_gesture_description_tap_down">Bakstelėkite ir perbraukite žemyn</string>
<string name="settings_gesture_double_down">Dvigubai žemyn</string>
<string name="settings_gesture_description_double_down">Perbraukite dviem pirštais</string>
<string name="settings_gesture_left">Kairėje</string>
<string name="settings_gesture_description_left">Perbraukite į kairę</string>
<string name="settings_gesture_tap_left">Bakstelėkite + kairę</string>
<string name="settings_gesture_description_tap_left">Bakstelėkite ir perbraukite į kairę</string>
<string name="settings_gesture_double_left">Dvigubai kairėje</string>
<string name="settings_gesture_description_double_left">Du pirštais perbraukite kairėn</string>
<string name="settings_gesture_right">Dešinė</string>
<string name="settings_gesture_description_right">Perbraukite į dešinę</string>
<string name="settings_gesture_tap_right">Bakstelėkite + dešinė</string>
<string name="settings_gesture_description_tap_right">Bakstelėkite ir perbraukite į dešinę</string>
<string name="settings_gesture_double_right">Dviguba dešinė</string>
<string name="settings_gesture_description_double_right">Perbraukite į dešinę dviem pirštais</string>
<string name="settings_gesture_right_top_edge">Dešinė (viršuje)</string>
<string name="settings_gesture_description_right_top_edge">Perbraukite tiesiai ekrano viršuje</string>
<string name="settings_gesture_right_bottom_edge">Dešinė (apačia)</string>
<string name="settings_gesture_description_right_bottom_edge">Perbraukite tiesiai ekrano apačioje</string>
<string name="settings_gesture_left_bottom_edge">Kairė (apačia)</string>
<string name="settings_gesture_description_left_bottom_edge">Perbraukite į kairę ekrano apačioje</string>
<string name="settings_gesture_left_top_edge">Kairė (viršuje)</string>
<string name="settings_gesture_description_left_top_edge">Perbraukite kairėn ekrano viršuje</string>
<string name="settings_gesture_up_left_edge">Aukštyn (kairysis kraštas)</string>
<string name="settings_gesture_description_up_left_edge">Perbraukite aukštyn kairiajame ekrano krašte</string>
<string name="settings_gesture_up_right_edge">Aukštyn (dešinysis kraštas)</string>
<string name="settings_gesture_description_up_right_edge">Perbraukite aukštyn dešiniajame ekrano krašte</string>
<string name="settings_gesture_down_left_edge">Žemyn (kairysis kraštas)</string>
<string name="settings_gesture_description_down_left_edge">Perbraukite žemyn kairiajame ekrano krašte</string>
<string name="settings_gesture_down_right_edge">Žemyn (dešinysis kraštas)</string>
<string name="settings_gesture_description_down_right_edge">Žemyn (dešinysis kraštas)</string>
<string name="settings_gesture_vol_up">Garsumo didinimo klavišas</string>
<string name="settings_gesture_description_vol_up">Paspauskite mygtuką „Volume Up“</string>
<string name="settings_gesture_vol_down">Volume žemyn klavišas</string>
<string name="settings_gesture_description_vol_down">Paspauskite mygtuką „Volume Down“</string>
<string name="settings_gesture_double_click">Dukart spustelėkite</string>
<string name="settings_gesture_description_double_click">Dukart spustelėkite tuščią sritį</string>
<string name="settings_gesture_long_click">Ilgas spustelėjimas</string>
<string name="settings_gesture_description_long_click">Ilgai spustelėkite tuščią sritį</string>
<string name="settings_gesture_date">Data</string>
<string name="settings_gesture_description_date">Spustelėkite datą</string>
<string name="settings_gesture_time">Laikas</string>
<string name="settings_gesture_description_time">Spustelėkite laiką</string>
<string name="settings_widgets_widgets">Tvarkykite valdiklius</string>
<string name="settings_widgets_custom_panels">Tvarkykite valdiklio skydelius</string>
<string name="settings_apps_choose">Pasirinkite programą</string>
<string name="settings_apps_install">Įdiekite programas</string>
<string name="settings_apps_toast_store_not_found">Parduotuvėje nerasta</string>
<!--
-
- Settings : Launcher
-
-->
<string name="settings_launcher_section_appearance">Išvaizda</string>
<string name="settings_theme_color_theme">Spalvos tema</string>
<string name="settings_theme_color_theme_item_default">Numatytasis</string>
<string name="settings_theme_color_theme_item_dark">Tamsu</string>
<string name="settings_theme_color_theme_item_light">Šviesa</string>
<string name="settings_theme_color_theme_item_dynamic">Dinaminis</string>
<string name="settings_theme_text_shadow">Teksto šešėlis</string>
<string name="settings_theme_background">Fonas (programų sąrašas ir nustatymas)</string>
<string name="settings_theme_background_item_transparent">Skaidrus</string>
<string name="settings_theme_background_item_dim">Dim</string>
<string name="settings_theme_background_item_blur">Blur</string>
<string name="settings_theme_background_item_solid">Solidus</string>
<string name="settings_theme_font">Šriftas</string>
<!-- names for @array/settings_theme_font_values -->
<string name="settings_theme_font_item_system_default">Sistemos numatytasis</string>
<string name="settings_theme_font_item_sans_serif">Be serifo</string>
<string name="settings_theme_font_item_serif">Serifas</string>
<string name="settings_theme_font_item_monospace">Monoerdvė</string>
<string name="settings_theme_font_item_serif_monospace">Serifo monospace</string>
<string name="settings_theme_monochrome_icons">Vienspalvių programų piktogramos</string>
<string name="settings_launcher_section_date_time"><![CDATA[Date & time]]></string>
<string name="settings_clock_color">Spalva</string>
<string name="settings_clock_time_visible">Rodyti laiką</string>
<string name="settings_clock_date_visible">Rodyti datą</string>
<string name="settings_clock_localized">Naudoti lokalizuotą datos formatą</string>
<string name="settings_clock_show_seconds">Rodyti sekundes</string>
<string name="settings_clock_flip_date_time">Apversti datą ir laiką</string>
<string name="settings_theme_wallpaper">Pasirinkite ekrano foną</string>
<string name="settings_launcher_section_display">Ekranas</string>
</resources>

View file

@ -289,9 +289,9 @@
<string name="settings_display_hide_navigation_bar">Navigatiebalk verbergen</string>
<string name="settings_display_rotate_screen">Scherm draaien</string>
<string name="settings_launcher_section_functionality">Functionaliteit</string>
<string name="settings_enabled_gestures_double_swipe">Dubbele veeg acties</string>
<string name="settings_enabled_gestures_double_swipe">Dubbele veeg gebaaren</string>
<string name="settings_enabled_gestures_double_swipe_summary">Met twee vingers vegen</string>
<string name="settings_enabled_gestures_edge_swipe">Hoek-acties</string>
<string name="settings_enabled_gestures_edge_swipe">Hoek-gebaren</string>
<string name="settings_enabled_gestures_edge_swipe_edge_width">Kantbreedte</string>
<string name="settings_functionality_auto_launch">Start zoekresultaten</string>
<string name="settings_functionality_auto_launch_summary">Spatie drukken om deze functie tijdelijk te onderdruken.</string>

View file

@ -285,4 +285,22 @@
<string name="alert_recent_apps_failed">错误:无法展示最近应用屏幕。(如果您刚刚升级了本启动器,请尝试在手机设置中手动禁用再重新启用“无障碍”服务。)</string>
<string name="list_other_launch_other_launcher">启动其他启动器</string>
<string name="settings_functionality_auto_close_keyboard">滚动应用程序列表时自动隐藏键盘</string>
<string name="settings_widgets_widgets">设置小部件</string>
<string name="widget_menu_remove">删除</string>
<string name="widget_menu_enable_interaction">开启交互功能</string>
<string name="widget_menu_disable_interaction">关闭交互功能</string>
<string name="widget_clock_label">时钟</string>
<string name="manage_widget_panels_delete">删除</string>
<string name="manage_widget_panels_rename">重命名</string>
<string name="widget_panel_default_name">小部件面板 #%1$d</string>
<string name="widget_panels_title">小部件面板</string>
<string name="dialog_select_widget_panel_title">选择小部件面板</string>
<string name="list_other_open_widget_panel">打开小部件面板</string>
<string name="dialog_create_widget_panel_title">创建新面板</string>
<string name="dialog_select_widget_panel_info_no_panels"><![CDATA[未发现小部件面板。您可以在“设置 > 启动器 > 设置小部件面板”中进行创建。]]></string>
<string name="settings_launcher_section_widgets">桌面小部件</string>
<string name="alert_widget_panel_not_found">该小部件面板不存在</string>
<string name="widget_clock_description">μLauncher 默认时钟小部件</string>
<string name="select_widget_title">选择小部件</string>
<string name="settings_widgets_custom_panels">设置小部件面板</string>
</resources>

View file

@ -161,7 +161,9 @@
-
-->
<string name="settings_meta_link_github" translatable="false">https://github.com/jrpie/Launcher</string>
<string name="settings_meta_link_docs" translatable="false">https://launcher.jrpie.de/</string>
<string name="settings_meta_report_bug_link" translatable="false">https://github.com/jrpie/Launcher/issues/new?template=bug_report.yaml</string>
<string name="settings_meta_report_bug_mail" translatable="false">android-launcher-crash@jrpie.de</string>
<string name="settings_meta_report_vulnerability_link" translatable="false">https://github.com/jrpie/Launcher/security/policy</string>
<string name="settings_meta_fork_contact_url" translatable="false">https://s.jrpie.de/contact</string>
<string name="settings_meta_privacy_url" translatable="false">https://s.jrpie.de/android-legal</string>

View file

@ -419,5 +419,29 @@
<string name="list_other_open_widget_panel">Open Widget Panel</string>
<string name="alert_widget_panel_not_found">This widget panel no longer exists.</string>
<string name="settings_launcher_section_widgets">Widgets</string>
<string name="notification_crash_title">μLauncher crashed</string>
<string name="notification_crash_explanation">Sorry! Click for more information.</string>
<string name="crash_info"><![CDATA[
Looks like something went wrong, sorry about that!<br><br>
For privacy reasons, crash logs are not collected automatically.<br>
However logs are very useful for debugging, so I would be very grateful if you could send me the attached log by mail
or create a bug report on github.<br><br>
Note that crash logs might contain <strong>sensitive information</strong>, e.g. the name of an app you tried to launch.
Please <strong>redact</strong> such information before sending the report.
<h2>What can I do now?</h2>
If this bug appears again and again, you can try several things:
<ul>
<li>Force stop μLauncher</li>
<li>Clear μLauncher\'s storage (<strong>Your settings will be lost!</strong>)</li>
<li>Install an older version (<a href=\"https://github.com/jrpie/Launcher/releases\">GitHub</a>, <a href=\"https://f-droid.org/en/packages/de.jrpie.android.launcher\">F-Droid</a>)</li>
</ul>
]]>
</string>
<string name="report_crash_button_copy">Copy crash report to clipboard</string>
<string name="report_crash_button_mail">Send report by mail</string>
<string name="report_crash_button_report">Create bug report on GitHub</string>
<string name="report_crash_title">μLauncher crashed</string>
<string name="send_email">Send Email</string>
<string name="notification_channel_crash">Crashes and Debug Information</string>
<string name="settings_meta_view_docs">Documentation</string>
</resources>

View file

@ -14,6 +14,7 @@
<item name="android:buttonStyle">@style/Widget.AppCompat.Button.Colored</item>
<item name="colorButtonNormal">?colorAccent</item>
<item name="cardBackgroundColor">@color/cardview_dark_background</item>
<!--<item name="android:popupMenuStyle">@style/PopupMenuCustom</item>-->
@ -32,6 +33,7 @@
<item name="colorPrimaryDark">@color/darkTheme_background_color</item>
<item name="colorAccent">@color/darkTheme_accent_color</item>
<item name="android:colorBackground">@color/darkTheme_background_color</item>
<item name="cardBackgroundColor">@color/cardview_dark_background</item>
<item name="android:textColor">@color/darkTheme_text_color</item>
</style>
@ -40,6 +42,7 @@
<item name="colorPrimaryDark">@color/finnmglasTheme_background_color</item>
<item name="colorAccent">@color/finnmglasTheme_accent_color</item>
<item name="android:colorBackground">@color/finnmglasTheme_background_color</item>
<item name="cardBackgroundColor">@color/cardview_dark_background</item>
<item name="android:textColor">@color/finnmglasTheme_text_color</item>
</style>
@ -48,6 +51,7 @@
<item name="colorPrimaryDark">@color/lightTheme_background_color</item>
<item name="colorAccent">@color/lightTheme_accent_color</item>
<item name="android:colorBackground">@color/lightTheme_background_color</item>
<item name="cardBackgroundColor">@color/cardview_light_background</item>
<item name="android:textColor">@color/lightTheme_text_color</item>
</style>
@ -57,6 +61,7 @@
<item name="colorPrimaryDark">@color/material_dynamic_primary50</item>
<item name="colorAccent">@color/material_dynamic_tertiary50</item>
<item name="android:colorBackground">@color/material_dynamic_neutral10</item>
<item name="cardBackgroundColor">@color/cardview_dark_background</item>
<item name="android:textColor">@color/material_dynamic_neutral_variant90</item>
</style>