diff --git a/app/build.gradle b/app/build.gradle index 42f0a2b..54e7b2b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { minSdkVersion 21 targetSdkVersion 35 compileSdk 35 - versionCode 46 - versionName "0.2.1" + versionCode 47 + versionName "0.2.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b10784..087ec28 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ tools:ignore="QueryAllPackagesPermission" /> + + @@ -80,6 +82,9 @@ + - \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt index 3c2e3bc..ba47942 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -25,9 +25,10 @@ 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; +const val APP_WIDGET_HOST_ID = 42 class Application : android.app.Application() { @@ -106,6 +107,11 @@ class Application : android.app.Application() { // TODO Error: Invalid resource ID 0x00000000. // DynamicColors.applyToActivitiesIfAvailable(this) + Thread.setDefaultUncaughtExceptionHandler { _, throwable -> + sendCrashNotification(this@Application, throwable) + exitProcess(1) + } + if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { torchManager = TorchManager(this) @@ -114,8 +120,6 @@ class Application : android.app.Application() { appWidgetHost = AppWidgetHost(this.applicationContext, APP_WIDGET_HOST_ID) appWidgetManager = AppWidgetManager.getInstance(this.applicationContext) - appWidgetHost.startListening() - val preferences = PreferenceManager.getDefaultSharedPreferences(this) LauncherPreferences.init(preferences, this.resources) @@ -157,6 +161,8 @@ class Application : android.app.Application() { removeUnusedShortcuts(this) } loadApps() + + createNotificationChannels(this) } fun getCustomAppNames(): HashMap { @@ -170,10 +176,4 @@ class Application : android.app.Application() { apps.postValue(getApps(packageManager, applicationContext)) } } - - override fun onTerminate() { - appWidgetHost.stopListening() - super.onTerminate() - - } } diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt index 9679ae5..b6df30b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -6,9 +6,6 @@ import android.app.role.RoleManager import android.content.ActivityNotFoundException import android.content.ClipData import android.content.ClipboardManager -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider -import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.content.Intent import android.content.pm.LauncherApps @@ -227,3 +224,13 @@ fun copyToClipboard(context: Context, text: String) { val clipData = ClipData.newPlainText("Debug Info", text) clipboardManager.setPrimaryClip(clipData) } + +fun writeEmail(context: Context, to: String, subject: String, text: String) { + val intent = Intent(Intent.ACTION_SENDTO) + intent.setData("mailto:".toUri()) + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(to)) + intent.putExtra(Intent.EXTRA_SUBJECT, subject) + intent.putExtra(Intent.EXTRA_TEXT, text) + context.startActivity(Intent.createChooser(intent, context.getString(R.string.send_email))) +} + diff --git a/app/src/main/java/de/jrpie/android/launcher/Notifications.kt b/app/src/main/java/de/jrpie/android/launcher/Notifications.kt new file mode 100644 index 0000000..0cf0efb --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/Notifications.kt @@ -0,0 +1,87 @@ +package de.jrpie.android.launcher + +import android.app.Activity +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import de.jrpie.android.launcher.ui.EXTRA_CRASH_LOG +import de.jrpie.android.launcher.ui.ReportCrashActivity +import java.io.PrintWriter +import java.io.StringWriter +import kotlin.random.Random + +private val NOTIFICATION_CHANNEL_CRASH = "launcher:crash" + +fun createNotificationChannels(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationChannel = NotificationChannel( + NOTIFICATION_CHANNEL_CRASH, + context.getString(R.string.notification_channel_crash), + NotificationManager.IMPORTANCE_HIGH + ) + + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(notificationChannel) + } +} + +fun requestNotificationPermission(activity: Activity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return + } + + val permission = + (activity.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) + + if (!permission) { + ActivityCompat.requestPermissions( + activity, + arrayOf( android.Manifest.permission.POST_NOTIFICATIONS ), + 1 + ) + } +} + +fun sendCrashNotification(context: Context, throwable: Throwable) { + val stringWriter = StringWriter() + val printWriter = PrintWriter(stringWriter) + throwable.printStackTrace(printWriter) + + val intent = Intent(context, ReportCrashActivity::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + intent.putExtra(EXTRA_CRASH_LOG, stringWriter.toString()) + + val pendingIntent = PendingIntent.getActivity( + context, + Random.nextInt(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_CRASH) + .setSmallIcon(R.drawable.baseline_bug_report_24) + .setContentTitle(context.getString(R.string.notification_crash_title)) + .setContentText(context.getString(R.string.notification_crash_explanation)) + .setContentIntent(pendingIntent) + .setAutoCancel(false) + .setPriority(NotificationCompat.PRIORITY_HIGH) + + val notificationManager = NotificationManagerCompat.from(context) + try { + notificationManager.notify( + 0, + builder.build() + ) + } catch (e: SecurityException) { + Log.e("Crash Notification", "Could not send notification") + } +} diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt b/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt index 6768116..a2ea801 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/TorchManager.kt @@ -1,7 +1,6 @@ package de.jrpie.android.launcher.actions import android.content.Context -import android.hardware.camera2.CameraAccessException import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraManager import android.os.Build diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt index d7829a6..84c4179 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/WidgetPanelAction.kt @@ -1,5 +1,6 @@ package de.jrpie.android.launcher.actions +import android.app.Activity import android.content.Context import android.content.Intent import android.graphics.Rect @@ -25,11 +26,18 @@ class WidgetPanelAction(val widgetPanelId: Int) : Action { override fun invoke(context: Context, rect: Rect?): Boolean { - if (WidgetPanel.byId(widgetPanelId) == null) { + if (context is WidgetPanelActivity) { + if (context.widgetPanelId == widgetPanelId) { + context.finish() + return true + } + } + + if (WidgetPanel.byId(this.widgetPanelId) == null) { Toast.makeText(context, R.string.alert_widget_panel_not_found, Toast.LENGTH_LONG).show() } else { context.startActivity(Intent(context, WidgetPanelActivity::class.java).also { - it.putExtra(EXTRA_PANEL_ID, widgetPanelId) + it.putExtra(EXTRA_PANEL_ID, this.widgetPanelId) }) } return true diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt index 93b4cbf..541510a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/lock/LockMethod.kt @@ -6,7 +6,6 @@ import android.widget.Button import androidx.appcompat.app.AlertDialog import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.R -import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService import de.jrpie.android.launcher.preferences.LauncherPreferences diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt index fd86d79..a4b1f43 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt @@ -2,7 +2,6 @@ package de.jrpie.android.launcher.preferences import android.content.Context import android.util.Log -import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.BuildConfig import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.apps.AbstractAppInfo @@ -10,22 +9,25 @@ import de.jrpie.android.launcher.apps.AbstractAppInfo.Companion.INVALID_USER import de.jrpie.android.launcher.apps.AppInfo import de.jrpie.android.launcher.apps.DetailedAppInfo import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1 +import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion100 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion2 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion4 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown +import de.jrpie.android.launcher.sendCrashNotification import de.jrpie.android.launcher.ui.HomeActivity import de.jrpie.android.launcher.widgets.ClockWidget import de.jrpie.android.launcher.widgets.DebugInfoWidget import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition -import de.jrpie.android.launcher.widgets.deleteAllWidgets +import de.jrpie.android.launcher.widgets.generateInternalId +import de.jrpie.android.launcher.widgets.getAppWidgetHost /* Current version of the structure of preferences. * Increase when breaking changes are introduced and write an appropriate case in * `migratePreferencesToNewVersion` */ -const val PREFERENCE_VERSION = 100 +const val PREFERENCE_VERSION = 101 const val UNKNOWN_PREFERENCE_VERSION = -1 private const val TAG = "Launcher - Preferences" @@ -64,6 +66,10 @@ fun migratePreferencesToNewVersion(context: Context) { migratePreferencesFromVersion4(context) Log.i(TAG, "migration of preferences complete (4 -> ${PREFERENCE_VERSION}).") } + 100 -> { + migratePreferencesFromVersion100(context) + Log.i(TAG, "migration of preferences complete (100 -> ${PREFERENCE_VERSION}).") + } else -> { Log.w( @@ -76,6 +82,7 @@ fun migratePreferencesToNewVersion(context: Context) { } } catch (e: Exception) { Log.e(TAG, "Unable to restore preferences:\n${e.stackTrace}") + sendCrashNotification(context, e) resetPreferences(context) } } @@ -84,12 +91,12 @@ fun resetPreferences(context: Context) { Log.i(TAG, "Resetting preferences") LauncherPreferences.clear() LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) - deleteAllWidgets(context) + context.getAppWidgetHost().deleteHost() LauncherPreferences.widgets().widgets( setOf( ClockWidget( - (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + generateInternalId(), WidgetPosition(1, 3, 10, 4), WidgetPanel.HOME.id ) @@ -101,7 +108,7 @@ fun resetPreferences(context: Context) { LauncherPreferences.widgets().widgets().also { it.add( DebugInfoWidget( - (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + generateInternalId(), WidgetPosition(1, 1, 10, 4), WidgetPanel.HOME.id ) diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt new file mode 100644 index 0000000..43e4bc7 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version100.kt @@ -0,0 +1,39 @@ +package de.jrpie.android.launcher.preferences.legacy + +import android.content.Context +import de.jrpie.android.launcher.Application +import de.jrpie.android.launcher.preferences.LauncherPreferences +import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION +import de.jrpie.android.launcher.widgets.ClockWidget +import de.jrpie.android.launcher.widgets.DebugInfoWidget +import de.jrpie.android.launcher.widgets.generateInternalId +import de.jrpie.android.launcher.widgets.updateWidget + +fun migratePreferencesFromVersion100(context: Context) { + assert(PREFERENCE_VERSION == 101) + assert(LauncherPreferences.internal().versionCode() == 100) + + val widgets = LauncherPreferences.widgets().widgets() ?: setOf() + widgets.forEach { widget -> + when (widget) { + is ClockWidget -> { + val id = widget.id + val newId = generateInternalId() + (context.applicationContext as Application).appWidgetHost.deleteAppWidgetId(id) + widget.delete(context) + widget.id = newId + updateWidget(widget) + } + is DebugInfoWidget -> { + val id = widget.id + val newId = generateInternalId() + (context.applicationContext as Application).appWidgetHost.deleteAppWidgetId(id) + widget.delete(context) + widget.id = newId + updateWidget(widget) + } + else -> {} + } + } + LauncherPreferences.internal().versionCode(101) +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt index a9ab3a1..fb353d5 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version4.kt @@ -1,25 +1,24 @@ 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.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition +import de.jrpie.android.launcher.widgets.generateInternalId fun migratePreferencesFromVersion4(context: Context) { - assert(PREFERENCE_VERSION == 100) assert(LauncherPreferences.internal().versionCode() < 100) LauncherPreferences.widgets().widgets( setOf( ClockWidget( - (context.applicationContext as Application).appWidgetHost.allocateAppWidgetId(), + generateInternalId(), WidgetPosition(1, 3, 10, 4), WidgetPanel.HOME.id ) ) ) LauncherPreferences.internal().versionCode(100) + migratePreferencesFromVersion100(context) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index f501107..76bf443 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -1,16 +1,9 @@ package de.jrpie.android.launcher.ui -import android.annotation.SuppressLint -import android.app.Activity import android.content.SharedPreferences -import android.content.res.Configuration import android.content.res.Resources -import android.os.Build import android.os.Bundle -import android.view.KeyEvent -import android.view.MotionEvent import android.view.View -import android.window.OnBackInvokedDispatcher import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Gesture @@ -19,6 +12,7 @@ import de.jrpie.android.launcher.databinding.ActivityHomeBinding import de.jrpie.android.launcher.openTutorial import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.tutorial.TutorialActivity +import de.jrpie.android.launcher.ui.util.LauncherGestureActivity /** * [HomeActivity] is the actual application Launcher, @@ -32,10 +26,9 @@ import de.jrpie.android.launcher.ui.tutorial.TutorialActivity * - Setting global variables (preferences etc.) * - Opening the [TutorialActivity] on new installations */ -class HomeActivity : UIObject, Activity() { +class HomeActivity : UIObject, LauncherGestureActivity() { private lateinit var binding: ActivityHomeBinding - private var touchGestureDetector: TouchGestureDetector? = null private var sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> @@ -54,35 +47,21 @@ class HomeActivity : UIObject, Activity() { } override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + super.onCreate(savedInstanceState) super.onCreate() - // Initialise layout binding = ActivityHomeBinding.inflate(layoutInflater) setContentView(binding.root) - // Handle back key / gesture on Android 13+, cf. onKeyDown() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - onBackInvokedDispatcher.registerOnBackInvokedCallback( - OnBackInvokedDispatcher.PRIORITY_OVERLAY - ) { - handleBack() - } - } binding.buttonFallbackSettings.setOnClickListener { LauncherAction.SETTINGS.invoke(this) } } - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - touchGestureDetector?.updateScreenSize(windowManager) - } - override fun onStart() { - super.onStart() + super.onStart() super.onStart() // If the tutorial was not finished, start it @@ -93,15 +72,6 @@ class HomeActivity : UIObject, Activity() { LauncherPreferences.getSharedPreferences() .registerOnSharedPreferenceChangeListener(sharedPreferencesListener) - (application as Application).appWidgetHost.startListening() - - } - - - - override fun onStop() { - (application as Application).appWidgetHost.stopListening() - super.onStop() } override fun onWindowFocusChanged(hasFocus: Boolean) { @@ -112,7 +82,6 @@ class HomeActivity : UIObject, Activity() { } } - private fun updateSettingsFallbackButtonVisibility() { // If µLauncher settings can not be reached from any action bound to an enabled gesture, // show the fallback button. @@ -131,81 +100,42 @@ class HomeActivity : UIObject, Activity() { return modifyTheme(super.getTheme()) } + override fun onPause() { + try { + (application as Application).appWidgetHost.stopListening() + } catch (e: Exception) { + // Throws a NullPointerException on Android 12 an earlier, see #172 + e.printStackTrace() + } + super.onPause() + } + override fun onResume() { super.onResume() - - /* This should be initialized in onCreate() - However on some devices there seems to be a bug where the touchGestureDetector - is not working properly after resuming the app. - Reinitializing the touchGestureDetector every time the app is resumed might help to fix that. - (see issue #138) - */ - touchGestureDetector = TouchGestureDetector( - this, 0, 0, - LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f - ).also { - it.updateScreenSize(windowManager) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - binding.root.setOnApplyWindowInsetsListener { _, windowInsets -> - @Suppress("deprecation") // required to support API 29 - val insets = windowInsets.systemGestureInsets - touchGestureDetector?.setSystemGestureInsets(insets) - - windowInsets - } - } updateSettingsFallbackButtonVisibility() binding.homeWidgetContainer.updateWidgets(this@HomeActivity, LauncherPreferences.widgets().widgets() ) + + (application as Application).appWidgetHost.startListening() } + override fun onDestroy() { LauncherPreferences.getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener) super.onDestroy() } - @SuppressLint("GestureBackNavigation") - override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - when (keyCode) { - KeyEvent.KEYCODE_BACK -> { - // Only used pre Android 13, cf. onBackInvokedDispatcher - handleBack() - } - - KeyEvent.KEYCODE_VOLUME_UP -> { - if (Action.forGesture(Gesture.VOLUME_UP) == LauncherAction.VOLUME_UP) { - // Let the OS handle the key event. This works better with some custom ROMs - // and apps like Samsung Sound Assistant. - return false - } - Gesture.VOLUME_UP(this) - } - - KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (Action.forGesture(Gesture.VOLUME_DOWN) == LauncherAction.VOLUME_DOWN) { - // see above - return false - } - Gesture.VOLUME_DOWN(this) - } - } - return true - } - - override fun onTouchEvent(event: MotionEvent): Boolean { - touchGestureDetector?.onTouchEvent(event) - return true - } - - private fun handleBack() { + override fun handleBack() { Gesture.BACK(this) } + override fun getRootView(): View { + return binding.root + } + override fun isHomeScreen(): Boolean { return true } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt index 3dbdda8..70f737f 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/PinShortcutActivity.kt @@ -1,7 +1,6 @@ package de.jrpie.android.launcher.ui import android.app.AlertDialog -import android.app.Service import android.content.Context import android.content.pm.LauncherApps import android.content.pm.LauncherApps.PinItemRequest @@ -45,7 +44,7 @@ class PinShortcutActivity : AppCompatActivity(), UIObject { binding = ActivityPinShortcutBinding.inflate(layoutInflater) setContentView(binding.root) - val launcherApps = getSystemService(Service.LAUNCHER_APPS_SERVICE) as LauncherApps + val launcherApps = getSystemService(LAUNCHER_APPS_SERVICE) as LauncherApps val request = launcherApps.getPinItemRequest(intent) this.request = request @@ -56,7 +55,7 @@ class PinShortcutActivity : AppCompatActivity(), UIObject { if (request.requestType == PinItemRequest.REQUEST_TYPE_APPWIDGET) { - // TODO + // TODO handle app widgets request.getAppWidgetProviderInfo(this) // startActivity() finish() diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt new file mode 100644 index 0000000..5d95769 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt @@ -0,0 +1,57 @@ +package de.jrpie.android.launcher.ui + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import de.jrpie.android.launcher.R +import de.jrpie.android.launcher.copyToClipboard +import de.jrpie.android.launcher.databinding.ActivityReportCrashBinding +import de.jrpie.android.launcher.getDeviceInfo +import de.jrpie.android.launcher.openInBrowser +import de.jrpie.android.launcher.writeEmail + +const val EXTRA_CRASH_LOG = "crashLog" + +class ReportCrashActivity : AppCompatActivity() { + // We don't know what caused the crash, so this Activity should use as little functionality as possible. + // In particular it is not a UIObject (and hence looks quite ugly) + private lateinit var binding: ActivityReportCrashBinding + private var report: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Initialise layout + binding = ActivityReportCrashBinding.inflate(layoutInflater) + setContentView(binding.root) + + setTitle(R.string.report_crash_title) + setSupportActionBar(binding.reportCrashAppbar) + supportActionBar?.setDisplayHomeAsUpEnabled(false) + + report = intent.getStringExtra(EXTRA_CRASH_LOG) + + binding.reportCrashButtonCopy.setOnClickListener { + copyToClipboard(this, + "Device Info:\n${getDeviceInfo()}\n\nCrash Log:\n${report}") + } + + binding.reportCrashButtonMail.setOnClickListener { + writeEmail( + this, + getString(R.string.settings_meta_report_bug_mail), + "Crash in μLauncher", + "Hi!\nUnfortunately, μLauncher crashed:\n" + + "\nDevice Info\n\n${getDeviceInfo()}\n\n" + + "\nCrash Log\n\n${report}\n" + + "\nAdditional Information\n\n" + + "[Please add additional information: What did you do when the crash happened? Do you know how to trigger it? ... ]" + ) + } + binding.reportCrashButtonReport.setOnClickListener { + openInBrowser( + getString(R.string.settings_meta_report_bug_link), + this + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt index 8e8ed4e..8264752 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt @@ -17,6 +17,7 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.tan +@Suppress("PrivatePropertyName") class TouchGestureDetector( private val context: Context, var width: Int, @@ -34,13 +35,13 @@ class TouchGestureDetector( private val MIN_TRIANGLE_HEIGHT = 250 - private val longPressHandler = Handler(Looper.getMainLooper()) - private var systemGestureInsetTop = 100 private var systemGestureInsetBottom = 0 private var systemGestureInsetLeft = 0 private var systemGestureInsetRight = 0 + private val longPressHandler = Handler(Looper.getMainLooper()) + data class Vector(val x: Float, val y: Float) { fun absSquared(): Float { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt index 65278ce..784d6fa 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/AppsRecyclerAdapter.kt @@ -237,9 +237,4 @@ class AppsRecyclerAdapter( appFilter.favoritesVisibility = v updateAppsList() } - - fun setHiddenAppsVisibility(v: AppFilter.Companion.AppSetVisibility) { - appFilter.hiddenVisibility = v - updateAppsList() - } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt index a8e59ba..ed26729 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/list/apps/ListFragmentApps.kt @@ -96,7 +96,6 @@ class ListFragmentApps : Fragment(), UIObject { if (LauncherPreferences.functionality().searchAutoCloseKeyboard()) { addOnScrollListener(object : RecyclerView.OnScrollListener() { var totalDy: Int = 0 - var threshold = (resources.displayMetrics.density * 100).toInt() override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { totalDy += dy diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt b/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt index 759d8cd..1a0e802 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/settings/meta/SettingsFragmentMeta.kt @@ -79,6 +79,9 @@ class SettingsFragmentMeta : Fragment(), UIObject { // 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() diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt b/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt index 8feaa07..548c30b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt @@ -5,9 +5,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup 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 +31,10 @@ class TutorialFragment5Finish : Fragment(), UIObject { override fun onStart() { super.onStart() super.onStart() + requestNotificationPermission(requireActivity()) } + override fun setOnClicks() { super.setOnClicks() binding.tutorialFinishButtonStart.setOnClickListener { finishTutorial() } @@ -44,6 +46,7 @@ class TutorialFragment5Finish : Fragment(), UIObject { LauncherPreferences.internal().startedTime(System.currentTimeMillis() / 1000L) } context?.let { setDefaultHomeScreen(it, checkDefault = true) } + activity?.finish() } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/util/LauncherGestureActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/util/LauncherGestureActivity.kt new file mode 100644 index 0000000..0be09f9 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/util/LauncherGestureActivity.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt index 54d3869..cbe5395 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt @@ -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: } } } - - - } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt index 5f8adf1..5eab32f 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -61,7 +61,7 @@ open class WidgetContainerView( it.value.y + it.value.height ).contains(position) == true }.any { - Widget.byId(context, it.key)?.allowInteraction == false + Widget.byId(it.key)?.allowInteraction == false } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt index 4392451..7d51b08 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetPanelActivity.kt @@ -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.onCreate(savedInstanceState) + super.onCreate(savedInstanceState) super.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.onStart() + super.onStart() super.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 } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt index 4b5c0c2..531cdc1 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt @@ -5,7 +5,6 @@ import android.appwidget.AppWidgetManager import android.content.Intent import android.content.SharedPreferences import android.content.res.Resources -import android.graphics.Rect import android.os.Bundle import android.util.Log import android.view.ViewGroup @@ -21,7 +20,6 @@ import de.jrpie.android.launcher.widgets.GRID_SIZE import de.jrpie.android.launcher.widgets.WidgetPanel import de.jrpie.android.launcher.widgets.WidgetPosition import kotlin.math.max -import kotlin.math.min import kotlin.math.roundToInt @@ -92,13 +90,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) { @@ -121,13 +130,8 @@ class ManageWidgetsActivity : UIObject, Activity() { private fun selectWidget() { - 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 +144,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 +162,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) diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt index 0efdb43..eeb98df 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt @@ -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() diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt index 76a2572..f49f7da 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -31,19 +31,19 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe WidgetContainerView(widgetPanelId, context, attrs) { constructor(context: Context, attrs: AttributeSet?) : this(WidgetPanel.HOME.id, context, attrs) - val TOUCH_SLOP: Int - val TOUCH_SLOP_SQUARE: Int - val LONG_PRESS_TIMEOUT: Long + val touchSlop: Int + val touchSlopSquare: Int + val longPressTimeout: Long private var overlayViewById = HashMap() init { val configuration = ViewConfiguration.get(context) - TOUCH_SLOP = configuration.scaledTouchSlop - TOUCH_SLOP_SQUARE = TOUCH_SLOP * TOUCH_SLOP + touchSlop = configuration.scaledTouchSlop + touchSlopSquare = touchSlop * touchSlop - LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong() + longPressTimeout = ViewConfiguration.getLongPressTimeout().toLong() } @@ -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)) @@ -127,14 +127,14 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) endInteraction() } - }, LONG_PRESS_TIMEOUT) + }, longPressTimeout) } if (event.actionMasked == MotionEvent.ACTION_MOVE || event.actionMasked == MotionEvent.ACTION_UP ) { val distanceX = event.x - (currentGestureStart?.x ?: return true) val distanceY = event.y - (currentGestureStart?.y ?: return true) - if (distanceX * distanceX + distanceY * distanceY > TOUCH_SLOP_SQUARE) { + if (distanceX * distanceX + distanceY * distanceY > touchSlopSquare) { longPressHandler.removeCallbacksAndMessages(null) } val view = selectedWidgetOverlayView ?: return true @@ -160,9 +160,8 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe } if (event.actionMasked == MotionEvent.ACTION_UP) { - longPressHandler.removeCallbacksAndMessages(null) val id = selectedWidgetOverlayView?.widgetId ?: return true - val widget = Widget.byId(context, id) ?: return true + val widget = Widget.byId(id) ?: return true widget.position = newPosition endInteraction() updateWidget(widget) @@ -176,8 +175,16 @@ class WidgetManagerView(widgetPanelId: Int, context: Context, attrs: AttributeSe } private fun endInteraction() { - startWidgetPosition = null - selectedWidgetOverlayView?.mode = null + synchronized(this) { + longPressHandler.removeCallbacksAndMessages(null) + startWidgetPosition = null + selectedWidgetOverlayView?.mode = null + } + } + + override fun onDetachedFromWindow() { + endInteraction() + super.onDetachedFromWindow() } override fun updateWidgets(activity: Activity, widgets: Collection?) { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt index 61006b8..0363069 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt @@ -54,26 +54,18 @@ class WidgetOverlayView : ViewGroup { var widgetId: Int = -1 set(newId) { field = newId - preview = Widget.byId(context, widgetId)?.getPreview(context) + preview = Widget.byId(widgetId)?.getPreview(context) } - constructor(context: Context) : super(context) { - init(null, 0) - } + constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - init(attrs, 0) - } + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( context, attrs, defStyle - ) { - init(attrs, defStyle) - } - - private fun init(attrs: AttributeSet?, defStyle: Int) { } + ) override fun onDraw(canvas: Canvas) { super.onDraw(canvas) @@ -101,13 +93,13 @@ class WidgetOverlayView : ViewGroup { } fun showPopupMenu() { - val widget = Widget.byId(context, widgetId)?: return + val widget = Widget.byId(widgetId)?: return val menu = PopupMenu(context, popupAnchor) menu.menu.let { it.add( context.getString(R.string.widget_menu_remove) ).setOnMenuItemClickListener { _ -> - Widget.byId(context, widgetId)?.delete(context) + Widget.byId(widgetId)?.delete(context) return@setOnMenuItemClickListener true } it.add( @@ -126,7 +118,7 @@ class WidgetOverlayView : ViewGroup { } fun getHandles(): List { - return listOf( + return listOf( Handle(WidgetManagerView.EditMode.TOP, Rect(HANDLE_EDGE_SIZE, 0, width - HANDLE_EDGE_SIZE, HANDLE_SIZE)), Handle(WidgetManagerView.EditMode.BOTTOM, diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt index 22a63eb..a968962 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -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) diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt index d0d1c0e..29d9308 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -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): ClockView? { diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt index 01ecddc..75ae6d0 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/DebugInfoWidget.kt @@ -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 diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt index 28539a2..73f5d81 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -27,7 +27,9 @@ sealed class Widget { abstract fun configure(activity: Activity, requestCode: Int) fun delete(context: Context) { - context.getAppWidgetHost().deleteAppWidgetId(id) + if (id >= 0) { + context.getAppWidgetHost().deleteAppWidgetId(id) + } LauncherPreferences.widgets().widgets( LauncherPreferences.widgets().widgets()?.also { @@ -36,10 +38,6 @@ sealed class Widget { ) } - fun getPanel(): WidgetPanel? { - return WidgetPanel.byId(panelId) - } - override fun hashCode(): Int { return id } @@ -55,9 +53,9 @@ sealed class Widget { fun deserialize(serialized: String): Widget { return Json.decodeFromString(serialized) } - fun byId(context: Context, id: Int): Widget? { + fun byId(id: Int): Widget? { // TODO: do some caching - return LauncherPreferences.widgets().widgets().firstOrNull() { + return LauncherPreferences.widgets().widgets().firstOrNull { it.id == id } } diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt index cded50c..864e5d3 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -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 @@ -62,8 +55,8 @@ fun getAppWidgetProviders( context: Context ): List { (context.getSystemService(Service.USER_SERVICE) as UserManager).userProfiles } list.addAll( - profiles.map { - appWidgetManager.getInstalledProvidersForProfile(it) + profiles.map { profile -> + appWidgetManager.getInstalledProvidersForProfile(profile) .map { LauncherAppWidgetProvider(it) } }.flatten() ) @@ -79,6 +72,13 @@ fun updateWidget(widget: Widget) { ) } + +// TODO: this needs to be improved +fun generateInternalId(): Int { + val minId = min(-5,(LauncherPreferences.widgets().widgets() ?: setOf()).minOfOrNull { it.id } ?: 0) + return minId -1 +} + fun updateWidgetPanel(widgetPanel: WidgetPanel) { LauncherPreferences.widgets().customPanels( (LauncherPreferences.widgets().customPanels() ?: setOf()) @@ -92,4 +92,4 @@ fun Context.getAppWidgetHost(): AppWidgetHost { } fun Context.getAppWidgetManager(): AppWidgetManager { return (this.applicationContext as Application).appWidgetManager -} +} \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_bug_report_24.xml b/app/src/main/res/drawable/baseline_bug_report_24.xml new file mode 100644 index 0000000..ef399ba --- /dev/null +++ b/app/src/main/res/drawable/baseline_bug_report_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/activity_manage_widget_panels.xml b/app/src/main/res/layout/activity_manage_widget_panels.xml index 60413a3..f84f42f 100644 --- a/app/src/main/res/layout/activity_manage_widget_panels.xml +++ b/app/src/main/res/layout/activity_manage_widget_panels.xml @@ -40,6 +40,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" + android:contentDescription="@string/content_description_close" android:gravity="center" android:includeFontPadding="true" android:paddingLeft="16sp" @@ -64,6 +65,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" + android:contentDescription="@string/content_description_add_widget_panel" android:src="@drawable/baseline_add_24" app:layout_anchor="@+id/manage_widget_panels_recycler" app:layout_anchorGravity="end|bottom" diff --git a/app/src/main/res/layout/activity_manage_widgets.xml b/app/src/main/res/layout/activity_manage_widgets.xml index 66404ee..4e63ec9 100644 --- a/app/src/main/res/layout/activity_manage_widgets.xml +++ b/app/src/main/res/layout/activity_manage_widgets.xml @@ -20,6 +20,7 @@ android:layout_height="wrap_content" android:layout_margin="16dp" android:clickable="true" + android:contentDescription="@string/content_description_add_widget" android:focusable="true" android:src="@drawable/baseline_add_24" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/activity_pin_shortcut.xml b/app/src/main/res/layout/activity_pin_shortcut.xml index 2519374..da724e7 100644 --- a/app/src/main/res/layout/activity_pin_shortcut.xml +++ b/app/src/main/res/layout/activity_pin_shortcut.xml @@ -44,6 +44,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" + android:contentDescription="@string/content_description_close" android:gravity="center" android:includeFontPadding="true" android:paddingLeft="16sp" diff --git a/app/src/main/res/layout/activity_report_crash.xml b/app/src/main/res/layout/activity_report_crash.xml new file mode 100644 index 0000000..c7f2110 --- /dev/null +++ b/app/src/main/res/layout/activity_report_crash.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + +