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
fun bindURL(view: View, urlRes: Int) {
view.setOnClickListener {
openInBrowser(
getString(urlRes),
requireContext()
)
}
}
}
@Preview
@Composable
fun SettingsMetaScreenPreview() {
SettingsMetaScreen(
context = LocalContext.current,
onResetConfirmed = {}
)
}
binding.settingsMetaButtonViewTutorial.setOnClickListener {
openTutorial(requireContext())
}
// 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))
// 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()
}
.setNegativeButton(android.R.string.cancel, null)
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
)
actions.forEachIndexed { index, action ->
SettingsButton(
text = stringResource(action.textResId),
onClick = { action.onClick(context) }
)
if (index == 1 || index == 3 || index == 6) {
SettingsButtonSpacer()
}
}
// 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 {
// 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()
copyToClipboard(context, deviceInfo)
}
)
Spacer(modifier = Modifier.height(48.dp))
}
}
@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)
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()
)
}
}
@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))
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)
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(android.R.string.cancel))
info.setOnClickListener {
copyToClipboard(requireContext(), deviceInfo)
}
},
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
buttonSecurity.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_report_vulnerability_link),
requireContext()
)
)
}
}
}
}
@Preview
@Composable
fun AlertDialogResetSettingsPreview() {
AlertDialogResetSettings(
onDismissRequest = {},
onConfirmation = {},
dialogTitle = "Reset settings",
dialogText = "Are you sure you want to reset all settings?",
icon = Icons.Default.Warning
)
// 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)
}
}
}

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) {
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())

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">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?cardBackgroundColor"
app:cardElevation="8dp"
app:cardCornerRadius="12dp"
app:cardUseCompatPadding="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
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"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
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_width="0dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginStart="10sp"
android:layout_marginEnd="10sp"
android:gravity="start"
android:text=""
android:textSize="20sp"
tools:text="some widget"
style="@style/TextAppearance.Material3.HeadlineSmall"
tools:text="Some Widget"
app:layout_constraintStart_toEndOf="@id/list_widgets_row_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
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:layout_marginEnd="10sp"
android:gravity="start"
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" />
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="0dp"
android:maxHeight="100dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_width="match_parent"
android:layout_height="100dp"
app:layout_constraintStart_toStartOf="parent"
android:maxHeight="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/list_widgets_row_description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/list_widgets_row_icon"
tools:src="@mipmap/ic_launcher_round"
tools:ignore="ContentDescription" />
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.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="0dp"
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_constraintStart_toStartOf="parent"
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="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="16dp"
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" />
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>

0
docs/_index.md Normal file
View file

View file

@ -1,4 +1,8 @@
# Gestures and Actions
+++
weight = 10
+++
# Actions and Gestures
µLauncher's central mechanism for accessing important functionality quickly
is to bind actions (e.g. launching an app) to gestures (e.g. swiping up).

452
docs/alternatives.md Normal file
View file

@ -0,0 +1,452 @@
+++
weight = 100
+++
# FOSS Launchers
This is a comparison of open-source home screens for Android.
**Inclusion criteria:** Apps in this list must be [open source](https://opensource.org/licenses) and maintained
<!-- New contributers, please reference https://www.markdownguide.org/basic-syntax/ -->
<!-- TEMPLATE
### <launcher name>
**License:** `<license type>`
[Website](<launcher website url>) | [Repository](<github or other repo url>) | [F-Droid](<fdroid url>)
<notes can go here>
**Main mode of interaction:** `<Search | Gestures | App grid>`
#### Features:
:white_check_mark: | :x: | :grey_question: Search: <list what you can search: e.g., apps, contacts, call history, ...>
:white_check_mark: | :x: | :grey_question: Search history
:white_check_mark: | :x: | :grey_question: Customizable gestures: <list usable gestures: e.g., up, down, tap+up, ...>
:white_check_mark: | :x: | :grey_question: Folders
:white_check_mark: | :x: | :grey_question: Tags
:white_check_mark: | :x: | :grey_question: Rename apps
:white_check_mark: | :x: | :grey_question: Widgets
:white_check_mark: | :x: | :grey_question: Private space
:white_check_mark: | :x: | :grey_question: Work profile
:white_check_mark: | :x: | :grey_question: Pinned shortcuts
:white_check_mark: | :x: | :grey_question: Icon packs
:white_check_mark: | :x: | :grey_question: Material You
---
-->
## Grid-Based Launchers
### Discreet Launcher
**License:** `GPL-3.0`
[Website](https://vincent-falzon.com/) | [Repository](https://github.com/falzonv/discreet-launcher) | [F-Droid](https://f-droid.org/en/packages/com.vincent_falzon.discreetlauncher/)
**Main mode of interaction:** `app grid`
#### Features:
:white_check_mark: Search: `apps`
:x: Search history
:x: Customizable gestures
:white_check_mark:Folders
:x: Tags
:white_check_mark: Rename apps
:x: Widgets
:grey_question: Private space
:white_check_mark: Work profile
:white_check_mark: Pinned shortcuts
:white_check_mark: Icon packs
:x: Material You
---
### Fossify
**License:** `GPL-3.0`
[Website](https://www.fossify.org/) | [Repository](https://github.com/FossifyOrg/Launcher) | [F-Droid](https://f-droid.org/en/packages/org.fossify.home/)
**Main mode of interaction:** `app grid`
#### Features:
:white_check_mark: Search: `apps`
:x: Search history
:x: Customizable gestures
:white_check_mark: Folders
:x: Tags
:white_check_mark: Rename apps
:white_check_mark: Widgets
:grey_question: Private space
:x: Work profile
:white_check_mark: Pinned shortcuts
:x: Icon packs
:white_check_mark: Material You
---
### Lawnchair
**License:** `Apache License 2.0`
[Website](https://lawnchair.app/) | [Repository](https://github.com/LawnchairLauncher/lawnchair)
Seems to be a regular (grid of apps) launcher.
**Main mode of interaction:** App grid
#### Features:
:white_check_mark: Search: `Apps & Shortcuts` `Web suggestions` `People` `Files` `Android Settings` `Calculator`
:white_check_mark: Search history
:white_check_mark: Customizable gestures: `double tap` `swipe up` `swipe down` `home button` `back button`
:white_check_mark: Folders
:x: Tags
:white_check_mark: Rename apps
:white_check_mark: Widgets
:grey_question: Private space
:grey_question: Work profile
:x: Pinned shortcuts
:white_check_mark: Icon packs
:white_check_mark: Material You
---
### Rootless Pixel Launcher
> **Abandoned**
**License:** `Apache License 2.0`
[Repository](https://github.com/amirzaidi/Launcher3)
**Main mode of interaction:** `App grid`
#### Features:
:white_check_mark: Search: `Apps`
:x: Search history
:x: Customizable gestures
:x: Folders
:x: Tags
:x: Rename apps
:warning: Widgets `buggy/broken`
:grey_question: Private space
:white_check_mark: Work profile
:white_check_mark: Pinned shortcuts
:white_check_mark: Icon packs
:x: Material You
---
## Search based
### Aster Launcher
> **Abandoned**
**License:** `GPL-3.0`
[Repository](https://github.com/neophtex/AsterLauncher) | [F-Droid](https://f-droid.org/en/packages/com.series.aster.launcher/)
**Main mode of interaction:** `search`
#### Features:
:warning: Search: `apps` (apps list is buggy/broken) `web`
:x: Search history
:x: Customizable gestures
:x: Folders
:x: Tags
:x: Rename apps
:x: Widgets
:grey_question: Private space
:white_check_mark: Work profile
:white_check_mark: Pinned shortcuts
:x: Icon packs
:x: Material You
---
### KISS Launcher
**License:** `GPL-3.0`
[Website](https://kisslauncher.com/) | [Repository](https://github.com/Neamar/KISS) | [F-Droid](https://f-droid.org/packages/fr.neamar.kiss/)
**Main mode of interaction:** `Search` `Some gestures available`
#### Features:
:white_check_mark: Search: `Apps` `Contacts` `Call history`
:white_check_mark: Search history
:white_check_mark: Customizable gestures: `swipe left` `swipe right` `swipe up` `swipe down` `long press` `double tap`
:x: Folders
:white_check_mark: Tags
:white_check_mark: Rename apps
:white_check_mark: Widgets
:grey_question: Private space
:white_check_mark: Work profile
:white_check_mark: Pinned shortcuts
:white_check_mark: Icon packs
:grey_question: Material You
---
### Lunar Launcher
**License:** `GPL-3.0`
[Repository](https://github.com/iamrasel/lunar-launcher) | [F-Droid](https://f-droid.org/en/packages/rasel.lunar.launcher/)
Even natively supports RSS feeds to homescreen?
**Main mode of interaction:** `alphabet scroller`
#### Features:
:white_check_mark: Search: `swipe up` `swipe down` `swipe left` `swipe right` `tap and hold battery indicator/clock` `tap and hold lower part of screen` `double tap` `tap and hold favorite item`
:x: Search history
:x: Customizable gestures
:x: Folders
:x: Tags
:x: Rename apps
:x: Widgets
:grey_question: Private space
:x: Work profile
:white_check_mark: Pinned shortcuts
:x: Icon packs
:x: Material You
---
### OLauncher
**License:** `GPL-3.0`
[Repository](https://github.com/tanujnotes/Olauncher) | [F-Droid](https://f-droid.org/en/packages/app.olauncher/)
Extremely minimal launcher with lots of forks.
**Main mode of interaction:** `Search`
#### Features:
:white_check_mark: Search: `apps`
:x: Search history.
:white_check_mark: Customizable gestures: `swipe left` `swipe right` `double tap`
:x: Folders
:x: Tags
:white_check_mark: Rename apps
:x: Widgets
:grey_question: Private space
:white_check_mark: Work profile
:grey_question: Pinned shortcuts
:x: Icon packs
:grey_question: Material You
#### Forks:
* [Olauncher Clutter Free](https://f-droid.org/en/packages/app.olaunchercf/)
* [mLauncher](https://f-droid.org/en/packages/app.mlauncher/)
* [CLauncher](https://f-droid.org/en/packages/app.clauncher/) (even more minimalistic, search without feedback)
* [CCLauncher](https://f-droid.org/en/packages/app.cclauncher/) (rewrite using compose)
---
### TinyBit Launcher
**License:** `GPL-3.0`
[Repository](https://github.com/TBog/TBLauncher) | [F-Droid](https://f-droid.org/en/packages/rocks.tbog.tblauncher/)
**Main mode of interaction:** `search` `some gestures`
#### Features:
:white_check_mark: Search: `apps` `contacts` `web` `maps` `playstore` `youtube`
:white_check_mark: Search history
:white_check_mark:Customizable gestures: `tap` `double tap` `swipe up` `swipe left` `swipe right` `swipe down on left` `swipe down on right`
:x: Folders
:white_check_mark: Tags
:white_check_mark: Rename apps
:white_check_mark: Widgets
:grey_question: Private space
:white_check_mark: Work profile
:white_check_mark: Pinned shortcuts
:white_check_mark: Icon packs
:x: Material You
---
### YAM Launcher
**License:** `MIT`
[Repository](https://codeberg.org/ottoptj/yamlauncher) | [F-Droid](https://f-droid.org/en/packages/eu.ottop.yamlauncher/)
Similar to OLauncher?
**Main mode of interaction:** `search` `home screen text buttons`
#### Features:
:white_check_mark: Search: `apps` `contacts (optional)`
:x: Search history
:white_check_mark: Customizable gestures: `swipe left` `swipe right`
:x: Folders
:x: Tags
:white_check_mark: Rename apps
:x: Widgets
:grey_question: Private space
:white_check_mark: Work profile
:white_check_mark: Pinned shortcuts
:x: Icon packs
:white_check_mark: Material You
---
## Directory based
### folder launcher
**License:** `MIT`
[Repository](https://github.com/Robby-Blue/SimpleFolderLauncher) | [F-Droid](https://f-droid.org/en/packages/me.robbyblue.mylauncher/)
**Main mode of interaction:** `directory navigation`
#### Features:
:white_check_mark: Search: `apps`
:x: Search history
:x: Customizable gestures
:white_check_mark: Folders
:x: Tags
:white_check_mark: Rename apps
:white_check_mark: Widgets
:grey_question: Private space
:x: Work profile
:white_check_mark: Pinned shortcuts
:x: Icon packs
:x: Material You
---
### Ion Launcher
**License:** `GPL-3.0`
[Repository](https://codeberg.org/zagura/ion-launcher) | [F-Droid](https://f-droid.org/en/packages/one.zagura.IonLauncher/)
**Main mode of interaction:** `App grid` `Search`
#### Features:
:warning: Search: `apps` `contacts: buggy/broken`
:white_check_mark: Search history `shows recently launched apps`
:x: Customizable gestures
:warning: Folders `prebuilt` `not customizable`
:x: Tags
:white_check_mark: Rename apps
:x: Widgets
:grey_question: Private space
:white_check_mark: Work profile
:white_check_mark: Pinned shortcuts
:white_check_mark: Icon packs
:x: Material You
---
## Gesture based
### Pie Launcher
**License:** `MIT`
[Repository](https://github.com/markusfisch/PieLauncher)
**Main mode of interaction:** `Selection wheel`
#### Features:
:white_check_mark: Search: `apps`
:x: Search history
:x: Customizable gestures
:x: Folders
:x: Tags
:x: Rename apps
:x: Widgets
:grey_question: Private space
:white_check_mark: Work profile
:white_check_mark: Pinned shortcuts
:x: Icon packs
:x: Material You
---
### µLauncher
**License:** `MIT`
[Repository](https://github.com/jrpie/launcher) | [F-Droid](https://f-droid.org/en/packages/de.jrpie.android.launcher/)
**Main mode of interaction:** `Gestures` `Search`
#### Features:
:white_check_mark: Search: `apps`
:x: Search history
:white_check_mark: Customizable gestures: `35 avilable` [read the docs](https://github.com/jrpie/launcher/blob/master/docs/actions-and-gestures.md)
:x: Folders
:x: Tags
:white_check_mark: Rename apps
:white_check_mark: Widgets
:white_check_mark: Private space
:white_check_mark: Work profile
:white_check_mark: Pinned shortcuts
:x:Icon packs
:white_check_mark: Material You
---
### Tabular Summary
#### Legend:
:white_check_mark: = Supported
:x: = Unsupported
:warning: = Buggy/Broken; check this launcher's notes above
| Launcher | Search | Search history | Customizable gestures | Folders | Tags | Rename apps | Widgets | Private space | Work profile | Pinned shortcuts | Icon packs | Material You |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| [µLauncher](#µLauncher) | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| [Fossify](#Fossify) | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :grey_question: | :x: | :white_check_mark: | :x: | :white_check_mark: |
| [Lawnchair](#Lawnchair) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :grey_question: | :grey_question: | :x: | :white_check_mark: | :white_check_mark: |
| [Rootless Pixel Launcher](#Rootless-Pixel-Launcher) | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :warning: | :grey_question: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| [KISS Launcher](#KISS-Launcher) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :grey_question: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :grey_question: |
| [Lunar Launcher](#Lunar-Launcher) | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | :grey_question: | :x: | :white_check_mark: | :x: | :x: |
| [OLauncher](#OLauncher) | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :grey_question: | :white_check_mark: | :grey_question: | :x: | :grey_question: |
| [TinyBit Launcher](#TinyBit-Launcher) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :grey_question: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| [YAM Launcher](#YAM-Launcher) | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :grey_question: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| [Ion Launcher](#Ion-Launcher) | :warning: | :white_check_mark: | :x: | :warning: | :x: | :white_check_mark: | :x: | :grey_question: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| [Pie Launcher](#Pie-Launcher) | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | :grey_question: | :white_check_mark: | :white_check_mark: | :x: | :x: |
| [folder launcher](#folder-launcher) | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :grey_question: | :x: | :white_check_mark: | :x: | :x: |
| [Discreet Launcher](#Discreet-Launcher) | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :white_check_mark: | :x: | :grey_question: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
| [Aster Launcher](#Aster-Launcher) | :warning: | :x: | :x: | :x: | :x: | :x: | :x: | :grey_question: | :white_check_mark: | :white_check_mark: | :x: | :x: |
<!-- Automating checkboxes for the table
You can use this python script, copy the contents of the Features section for the launcher, and paste it in the fixit() function. Note, since you're pasting in multiple lines, you will need to start and end your paste with triple-quotes.
import re
def fix(features):
# Use: fixed("""<paste here>""")
fixed = ' | '.join(re.findall('(:[\w\_]*:)', features))
return f"| {fixed} |"
-->
---
## Not Tested
Feel free to test these and add
https://f-droid.org/en/packages/app.easy.launcher/
https://f-droid.org/en/packages/de.nodomain.tobihille.seniorlauncher/
https://f-droid.org/en/packages/com.mrmannwood.hexlauncher/
https://f-droid.org/en/packages/com.simplemobiletools.applauncher/
https://f-droid.org/en/packages/peterfajdiga.fastdraw/
https://f-droid.org/en/packages/de.mm20.launcher2.release/
[Even more launchers](https://docs.arcticons.com/faq/supported-launchers) (most of them don't seem to be FOSS)

54
docs/app-drawer.md Normal file
View file

@ -0,0 +1,54 @@
+++
weight = 10
+++
# App Drawer
Apps that are not needed all the time are shown in the app drawer.
There are several such drawers, but the basic concept is the same.
Besides regular apps, app drawers also show [pinned shortcuts](https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts)[^1].
&mu;Launcher treats apps and shortcuts in the same way.
The idea of the app drawer is to search for apps using the keyboard.
By default[^2] an app is launched automatically once it is the only app matching the query.
This can be prevented by typing space.
Usually only two or three characters are needed which is much faster than scrolling to find an app.
[^1]: A pinned shortcut is created for example when pinning a website to the home screen.
[^2]: There are [several settings](/docs/settings/#functionality) available to modify the behavior.
When long clicking an app, additional options are shown:
* Rename the app
* Add to / remove from Favorites: Adds the app to the [Favorite Apps](#favorite-apps) list or removes it from there.
* Hide / Show: This hides the app from all drawers (except from [Hidden Apps](#hidden-apps)) or makes it visible again if it was hidden.
* App Info: Opens the system settings page for the app.
* Uninstall: Tries to uninstall the app or remove the shortcut.
## All Apps
This lists all apps except hidden apps.
By default it is bound to `Swipe up`.
## Favorite Apps
Only shows favorite apps.
Pressing the star icon on the bottom right of any app drawer toggles whether
only favorite apps should be shown.
Additionally the `Favorite Apps` action can be used to launch this drawer directly.
By default it is bound to `Swipe up (left edge)`.
## Private Space
When [private space](/docs/profiles/#private-space) is available, this drawer
shows only apps from the private space.
It can be opened using the `Private Space` action.
If the private space is locked, instead of showing the list the unlock dialog is shown.
By default, apps from the private space are shown in All Apps as well,
however this is [configurable](/docs/settings/#hide-private-space-from-app-list).
## Hidden Apps
This list shows hidden apps.
It is only accessible through the settings.
The feature is intended to be used only for apps which are not needed at all but [can not be uninstalled](https://en.wikipedia.org/wiki/Software_bloat#Bloatware).

View file

@ -1,3 +1,8 @@
+++
weight = 50
+++
# Building from Source
## Using the command line
@ -37,9 +42,13 @@ for further instructions.
Install [Android Studio](https://developer.android.com/studio), import this project and build it.
See [this guide](https://developer.android.com/studio/run)
for further instructions. How to
for further instructions.
## CI Pipeline
The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds.
> Note: These builds are *not* signed. They are in built in debug mode and only suitable for testing.
{{% hint warning %}}
Note: These builds are not signed.
They are in built in debug mode and only suitable for testing.
{{% /hint %}}

View file

@ -1,3 +1,7 @@
+++
title = 'Differences to the original Launcher'
+++
# Notable changes compared to Finn's Launcher
µLauncher is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher).
@ -51,6 +55,5 @@ The complete list of changes can be viewed [here](https://github.com/jrpie/launc
---
\[original-repo\]: [https://github.com/finnmglas/Launcher](https://github.com/finnmglas/Launcher)
\[hack-font\]: [https://sourcefoundry.org/hack/](https://sourcefoundry.org/hack/)
[original-repo]: https://github.com/finnmglas/Launcher
[hack-font]: https://sourcefoundry.org/hack/

View file

@ -1,3 +1,8 @@
+++
weight = 40
+++
# Contributing
There are several ways to contribute to this app:
@ -13,7 +18,7 @@ There are several ways to contribute to this app:
- Open a new pull request.
See [build.md](build.md) for instructions how to build this project.
See [here](/docs/build) for instructions how to build this project.
The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds.

7
docs/examples/_index.md Normal file
View file

@ -0,0 +1,7 @@
+++
bookCollapseSection = true
weight = 20
+++
# Examples
This section contains some examples how &mu;Launcher can be tweaked.

View file

@ -0,0 +1,18 @@
+++
title = 'Showing Apps on the Home Screen'
+++
# Showing Apps on the Home Screen
Even though this is somewhat contrary to the general idea of &mu;Launcher,
it is possible to show apps on the home screen using widgets.
Users suggested:
* [Launchy](https://launchywidget.com/) (proprietary!)
* KWGT Kustom Widget Maker (proprietary!)
{{% hint danger %}}
Both of these apps are not open source and KWGT even has ads.
Please contact me if you know FOSS alternatives!
{{% /hint %}}

View file

@ -0,0 +1,23 @@
+++
title = 'Integration with Termux'
+++
# Termux
&mu;Launcher has no special support for [Termux](https://termux.dev/).
However it is possible to run Termux commands from &mu;Launcher by using [Termux:Widget](https://wiki.termux.com/wiki/Termux:Widget) to create a pinned shortcut and bind that to a gesture.
* Install Termux:Widget.
* Make sure that &mu;Launcher is set as the default home screen.[^1]
* Put the script you want to run into `~/.shortcuts/`.
* Run `am start com.termux.widget/com.termux.widget.TermuxCreateShortcutActivity`. This will create a pinned shortcut which is treated like an app by &mu;Launcher, i.e. open &mu;Launcher's activity to create a shortcut.
<img src="./screenshot1.png"
alt="screenshot"
width="200" height="400">
<img src="./screenshot2.png"
alt="screenshot"
width="200" height="400">
[^1]: Only the default home screen can access shortcuts.

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -1,43 +0,0 @@
# Welcome to the μLauncher Documentation
## What is μLauncher?
µLauncher is an *minimal* and *distraction-free* Android home screen that lets you launch apps using [swipe gestures and button presses](/actions-and-gestured.md).
This project is a fork of [finnmglas's app Launcher](https://github.com/finnmglas/Launcher). An incomplete list of changes can be found [here](https://github.com/wassupluke/Launcher/blob/master/docs/launcher.md).
## Where can I get μLauncher?
[![Get it on F-Droid](https://fdroid.gitlab.io/artwork/badge/get-it-on.png)](https://f-droid.org/packages/de.jrpie.android.launcher/)
[![Get it on Accrescent](https://accrescent.app/badges/get-it-on.png)](https://accrescent.app/app/de.jrpie.android.launcher.accrescent)
[![Get it on Obtainium](https://raw.githubusercontent.com/ImranR98/Obtainium/b1c8ac6f2ab08497189721a788a5763e28ff64cd/assets/graphics/badge_obtainium.png)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/{%22id%22:%22de.jrpie.android.launcher%22,%22url%22:%22https://github.com/jrpie/Launcher%22,%22author%22:%22jrpie%22,%22name%22:%22%c2%b5Launcher%22,%22additionalSettings%22:%22{\%22apkFilterRegEx\%22:\%22release\%22,\%22invertAPKFilter\%22:false,\%22about\%22:\%22%c2%b5Launcher%20is%20a%20minimal%20home%20screen.\%22}%22})
[![Get it on GitHub](https://raw.githubusercontent.com/NeoApplications/Neo-Backup/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png)](https://github.com/jrpie/launcher/releases/latest)
> You can also [get it on Google Play](https://play.google.com/store/apps/details?id=de.jrpie.android.launcher), but this is not recommend.
## How can I contribute?
See [docs/contribute](/contribute.md)
## Screenshots
![μLauncher Home Screen screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg)
![μLauncher Settings screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg)
![μLauncher All Apps list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg)
![μLauncher Favorite Apps list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg)
![μLauncher Choose App to bind to gesture screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg)
![μLauncher App options card from list view with icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg
)
![μLauncher All Apps list view without icons screenshot](https://github.com/jrpie/launcher/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/8.jpg)
<!-- missing μLauncher grid view screenshot-->

View file

@ -1,3 +1,9 @@
+++
title = 'User Profiles'
weight = 12
+++
# Work Profile
µLauncher is compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used.

View file

@ -1,19 +1,22 @@
# Launcher Settings
+++
weight = 10
+++
Tweaks and customizations can be made from within the Launcher Settings page.
# Settings
These settings let you change wallpapers, change colors and fonts, enable monochrome app icons, change the app drawer layout, and much more.
Tweaks and customizations can be made from within the settings page.
The settings can be opened by binding the Settings action to a gesture (this is especially useful when configuring &mu;Launcher for the first time) or from the settings icon in the app drawer.[^1]
In the following documentation, 'app drawer' will be used to refer to the 'All Apps', 'Favorite Apps' and 'Private Space' views.
[^1]: i.e. the 'All Apps', 'Favorite Apps' and 'Private Space' views.
## Appearance
> ### Choose a wallpaper
### Choose a wallpaper
This triggers Android's mechanism to change the wallpaper using a photos app, file explorer, or native wallpaper setting app.
µLauncher uses the system-wide wallpaper, i.e. this change also affects other launchers.
> ### Font (in-app font)
### Font (in-app font)
Set the font used within the app settings. This setting does not affect the date/time home screen font.
@ -21,17 +24,26 @@ Set the font used within the app settings. This setting does not affect the date
**options:**&nbsp;`Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace`
> ### Text Shadow
### Text Shadow
**type:**&nbsp;`toggle`
> ### Background (app list and setting)
### Background (app list and setting)
Defines which background should be used in app drawers, settings, etc.
to increase legibility.
* `Transparent` does not change the wallpaper.
* `Dim` dims the wallpaper.
* `Blur` tries to blur the wallpaper. This is not possible on all devices. Some older devices don't support the operation. Also blur can be temporarily unavailable when the device is in power saving mode. In these case, `Dim` is used as a fallback.
* `Solid` sets the background to a solid color (depending on the color theme). For the light theme only this option is available.
On the home screen and on widget panels the wallpaper is always shown unmodified.
**type:**&nbsp;`dropdown`
**type:**&nbsp;`Transparent`,`Dim`,`Blur`,`Solid`
> ### Monochrome app icons
### Monochrome app icons
Remove coloring from all app icons. Can help decrease visual stimulus when enabled.
@ -39,49 +51,56 @@ Remove coloring from all app icons. Can help decrease visual stimulus when enabl
## Date & Time
> ### Font (home screen)
These settings effect the clock shown on the home screen (or on widget panels).
If the clock is removed, the settings are not used.
Set the home screen font for date and time. This setting does not affect the in-app font.
### Font (home screen)
Set the home screen font for date and time. This setting does not affect the font of other components.
**type:**&nbsp;`dropdown`
**options:**&nbsp;`Hack`,`System default`,`Sans serif`,`Serif`,`Monospace`,`Serif monospace`
> ### Color [`[bug]`](https://github.com/jrpie/launcher/issues/151)
### Color
Set the color for the home screen date and time.
Accepts a HEX color code (consisting of a '#' followed by three sets of two alphanumeric (letters and numbers) characters. A fourth set of two alphanumeric characters may be added to set the transparency of the color.
Accepts an 6 digit RGB or or 8 digit ARGB color code characters.[^2]
Note that on Android the ARGB color format is used, i.e. the alpha component is specified first.
This differs from the more common RGBA, which is used in web development.
[Color wheel picker](https://rgbacolorpicker.com/color-wheel-picker)
**type:**&nbsp;`HEX`,`RGBA`
[^2]: More precisely, everything that is vaild input for [parseColor](https://developer.android.com/reference/android/graphics/Color#parseColor(java.lang.String)) can be used.
> ### Use localized date format
Adapt the display of dates and times to the specific conventions of a particular locale or region. Different locales use different date orders (e.g., MM/DD/YYYY in the US, DD/MM/YYYY in Europe).
**type:**&nbsp;`ARGB`
### Use localized date format
Adapt the display of dates and times to the specific conventions of a particular locale or region as set by the system. Different locales use different date orders (e.g., MM/DD/YYYY in the US, DD/MM/YYYY in Europe).
**type:**&nbsp;`toggle`
> ### Show time
### Show time
Show the current time on the home screen.
**type:**&nbsp;`toggle`
> ### Show seconds
### Show seconds
Show the current time down to the second on the home screen.
**type:**&nbsp;`toggle`
> ### Show date
### Show date
Show the current date on the home screen.
**type:**&nbsp;`toggle`
> ### Flip date and time
### Flip date and time
Place the current time above the current date on the home screen.
@ -89,7 +108,7 @@ Place the current time above the current date on the home screen.
## Functionality
> ### Launch search results
### Launch search results
Launches any app that matches user keyboard input when no other apps match.
@ -105,37 +124,37 @@ Press space to temporarily disable this feature and allow text entry without pre
**type:**&nbsp;`toggle`
> ### Search the web
### Search the web
Press return/enter while searching the app list to launch a web search.
Press return while searching the app list to launch a web search.
**type:**&nbsp;`toggle`
> ### Start keyboard for search
### Start keyboard for search
Automatically open the keyboard when the app drawer is opened.
**type:**&nbsp;`toggle`
> ### Double swipe gestures
### Double swipe gestures
Enable double swipe (two finger) gestures in launcher settings. Does not erase gesture bindings if accidentally turned off.
**type:**&nbsp;`toggle`
> ### Edge swipe gestures
### Edge swipe gestures
Enable edge swipe (near edges of screen) gestures in launcher settings. Does not erase gesture bindings if accidentally turned off.
**type:**&nbsp;`toggle`
> ### Edge width
### Edge width
Change how large a margin is used for detecting edge gestures. Shows the edge margin preview when using the slider.
**type:**&nbsp;`slider`
> ### Choose method for locking the screen
### Choose method for locking the screen
There are two methods to lock the screen and unfortunately both have downsides.
@ -157,11 +176,11 @@ There are two methods to lock the screen and unfortunately both have downsides.
## Apps
> ### Hidden apps
### Hidden apps
Open an app drawer containing only hidden apps.
> ### Don't show apps that are bound to a gesture in the app list
### Don't show apps that are bound to a gesture in the app list
Remove certain apps from the app drawer if they are already accessible via a gesture.
@ -169,20 +188,21 @@ Reduces redundancy and tidies up app drawer.
**type:**&nbsp;`toggle`
> ### Hide paused apps
### Hide paused apps
Remove paused apps from the app drawer.
For example an app belonging to the work profile is paused when the work profile is inactive.
**type:**&nbsp;`toggle`
> ### Hide private space from app list
### Hide private space from app list
Remove private space from app drawer.
Private space apps can be accessed using a separate app drawer which can be opened with the Private Space action.
**type:**&nbsp;`toggle`
> ### Layout of app list
### Layout of app list
Changes how the apps are displayed when accessing the app drawer.
@ -195,7 +215,7 @@ Changes how the apps are displayed when accessing the app drawer.
**options:**&nbsp;`Default`,`Text`,`Grid`
> ### Reverse the app list
### Reverse the app list
Enable reverse alphabetical sorting of apps in the app drawer.
Useful for keeping apps within easier reach from the keyboard.
@ -204,32 +224,22 @@ Useful for keeping apps within easier reach from the keyboard.
## Display
> ### Rotate screen
### Rotate screen
**type:**&nbsp;`toggle`
> ### Keep screen on
### Keep screen on
**type:**&nbsp;`toggle`
> ### Hide status bar
### Hide status bar
Remove the top status bar from the home screen.
**type:**&nbsp;`toggle`
> ### Hide navigation bar
### Hide navigation bar
Remove the navigation bar from the home screen. Enabling this setting may make it difficult to use the device if gestures are not setup properly.
**type:**&nbsp;`toggle`
## Additional Settings
> ### App Drawer Long Press on App
Access additional per-app details and settings. To use, open the app drawer and long press on any app.
**type:**&nbsp;`dropdown`
**options:**&nbsp;`App Info`,`Add to favorites`,`Hide`,`Rename`,`Uninstall`

28
docs/widgets.md Normal file
View file

@ -0,0 +1,28 @@
+++
title = 'Widgets'
weight = 11
+++
# Widgets
&mu;Launcher allows to add [app widgets](https://developer.android.com/develop/ui/views/appwidgets/overview) to the home screen and to widget panels.
Widgets can be added, moved, removed and configured in `Settings > Manage Widgets`.
It is configurable whether or not interaction with a widget should be enabled.
* If interaction is enabled, touch events are forwarded to the widget as usual.
However, &mu;Launcher [gestures](/docs/actions-and-gestures/) can not be executed in areas where such a widget is present.
* If interaction is disabled, the widget does not respond to any touch events.
This is recommended when using a widget only to display information.
&mu;Launcher's clock behaves similar as an app widget and can be managed in the same way.[^1]
[^1]: However, it is technically not an app widget and cannot be used with other launchers.
# Widget Panels
Widget panels can contain widgets that are not needed on the home screen.
They can be managed in `Settings > Manage Widget Panels`.
Widget panels can be opened by using the [Open Widget Panel](/docs/actions-and-gestures/#available-actions) action.

View file

@ -0,0 +1,10 @@
* Fixed a bug related to widget causing crashes on Android 12 and earlier making the app unusable
* Fixed some additional bugs related to widgets
* Improved Lithuanian translation (thank you, wassupluke!)
* Improved Arabic translation (thank you, anonymous contributor!)
* Improved Chinese translation (thank you, class0068!)
* Improved Dutch translation (thank you, renar!)
* Improved German translation (thank you, renar!)
* Improved Italian translation (thank you, renar!)
* Improved Portuguese translation (thank you, anonymous contributor!)