From 663463cd38b027e41b89b033796b80469b120ea0 Mon Sep 17 00:00:00 2001 From: partisan Date: Sun, 11 May 2025 17:21:25 +0200 Subject: [PATCH] New menu, organizing settings to its separate categories --- .../weforge/xyz/pulse/AboutActivity.kt | 12 ++ .../weforge/xyz/pulse/MainActivity.kt | 191 ++++-------------- .../weforge/xyz/pulse/MainFragment.kt | 29 +++ .../xyz/pulse/PopupSettingsFragment.kt | 118 +++++++++++ .../partisan/weforge/xyz/pulse/Preferences.kt | 7 +- .../weforge/xyz/pulse/SettingsActivity.kt | 12 ++ app/src/main/res/drawable/tooltip_24px.xml | 10 + app/src/main/res/layout/activity_main.xml | 127 ++++-------- app/src/main/res/layout/fragment_main.xml | 12 ++ .../res/layout/fragment_popup_settings.xml | 107 ++++++++++ app/src/main/res/menu/main_menu.xml | 14 ++ app/src/main/res/values/strings.xml | 5 + 12 files changed, 398 insertions(+), 246 deletions(-) create mode 100644 app/src/main/java/partisan/weforge/xyz/pulse/AboutActivity.kt create mode 100644 app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt create mode 100644 app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt create mode 100644 app/src/main/java/partisan/weforge/xyz/pulse/SettingsActivity.kt create mode 100644 app/src/main/res/drawable/tooltip_24px.xml create mode 100644 app/src/main/res/layout/fragment_main.xml create mode 100644 app/src/main/res/layout/fragment_popup_settings.xml create mode 100644 app/src/main/res/menu/main_menu.xml diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/AboutActivity.kt b/app/src/main/java/partisan/weforge/xyz/pulse/AboutActivity.kt new file mode 100644 index 0000000..7317054 --- /dev/null +++ b/app/src/main/java/partisan/weforge/xyz/pulse/AboutActivity.kt @@ -0,0 +1,12 @@ +package partisan.weforge.xyz.pulse + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class AboutActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // placeholder until real UI is added + setContentView(androidx.appcompat.R.layout.abc_action_bar_title_item) + } +} diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt b/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt index 93a7774..0f9de9a 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/MainActivity.kt @@ -1,183 +1,68 @@ package partisan.weforge.xyz.pulse -import android.Manifest -import android.app.role.RoleManager import android.content.Intent -import android.content.pm.PackageManager import android.os.Bundle -import android.provider.Settings -import androidx.activity.result.contract.ActivityResultContracts +import android.view.Menu +import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity -import androidx.core.widget.doAfterTextChanged -import java.lang.NumberFormatException -import android.text.InputType -import android.widget.CheckBox -import android.widget.EditText -import android.widget.LinearLayout -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.ItemTouchHelper -import partisan.weforge.xyz.pulse.getServicePriority -import partisan.weforge.xyz.pulse.setServicePriority -import partisan.weforge.xyz.pulse.isServiceEnabled -import partisan.weforge.xyz.pulse.setServiceEnabled import partisan.weforge.xyz.pulse.databinding.ActivityMainBinding +import androidx.appcompat.app.ActionBarDrawerToggle import partisan.weforge.xyz.pulse.REQUIRED_PERMISSIONS +import partisan.weforge.xyz.pulse.hasCallRedirectionRole +import partisan.weforge.xyz.pulse.hasDrawOverlays import partisan.weforge.xyz.pulse.hasGeneralPermissions class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var prefs: Preferences - private lateinit var window: PopupWindow - private var roleManager: RoleManager? = null - - private val registerForCallRedirectionRole = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {} - - private val registerForGeneralPermissions = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {} - - private val registerForDrawOverlays = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {} override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - init() - setup() - } - override fun onDestroy() { - super.onDestroy() - window.cancel() - } + setSupportActionBar(binding.topAppBar) - private fun init() { - prefs = Preferences(this) - window = PopupWindow(this, null) - roleManager = getSystemService(RoleManager::class.java) - binding.apply { - redirectionDelay.value = (prefs.redirectionDelay / 1000).toFloat() - popupPosition.editText?.setText(prefs.popupPosition.toString()) - toggle.isChecked = prefs.isEnabled - } - } + val drawerToggle = ActionBarDrawerToggle( + this, + binding.drawerLayout, + binding.topAppBar, + R.string.navigation_drawer_open, + R.string.navigation_drawer_close + ) + binding.drawerLayout.addDrawerListener(drawerToggle) + drawerToggle.syncState() + supportFragmentManager.beginTransaction() + .replace(R.id.fragmentContainer, MainFragment()) + .commit() - private fun setup() { - binding.apply { - redirectionDelay.setLabelFormatter { - String.format("%.1f", it) - } - redirectionDelay.addOnChangeListener { _, value, _ -> - prefs.redirectionDelay = (value * 1000).toLong() - } - popupPosition.setEndIconOnClickListener { - window.preview() - } - popupPosition.editText?.doAfterTextChanged { - try { - prefs.popupPosition = it?.toString()?.toInt() ?: return@doAfterTextChanged - } catch (exc: NumberFormatException) {} - } - toggle.setOnCheckedChangeListener { _, isChecked -> - if (isChecked && !hasPermissions()) { - toggle.isChecked = false - requestPermissions() - return@setOnCheckedChangeListener + binding.navigationView.setNavigationItemSelectedListener { item -> + when (item.itemId) { + R.id.action_popup_settings -> { + supportFragmentManager.beginTransaction() + .replace(R.id.fragmentContainer, PopupSettingsFragment()) + .commit() + true } - prefs.isEnabled = isChecked - } - val services = listOf( - ServiceEntry("vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call", R.string.destination_signal, this@MainActivity.isServiceEnabled("vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call")), - ServiceEntry("vnd.android.cursor.item/vnd.org.telegram.messenger.android.call", R.string.destination_telegram, this@MainActivity.isServiceEnabled("vnd.android.cursor.item/vnd.org.telegram.messenger.android.call")), - ServiceEntry("vnd.android.cursor.item/vnd.ch.threema.app.call", R.string.destination_threema, this@MainActivity.isServiceEnabled("vnd.android.cursor.item/vnd.ch.threema.app.call")), - ServiceEntry("vnd.android.cursor.item/vnd.com.whatsapp.voip.call", R.string.destination_whatsapp, this@MainActivity.isServiceEnabled("vnd.android.cursor.item/vnd.com.whatsapp.voip.call")), - ) - - val adapter = ServiceAdapter( - context = this@MainActivity, - services = services.toMutableList(), - onReordered = { updatedList -> - updatedList.forEachIndexed { index, entry -> - setServicePriority(entry.mimetype, index) - } + R.id.action_settings -> { + startActivity(Intent(this, SettingsActivity::class.java)) + true } - ) - binding.serviceRecycler.adapter = adapter - binding.serviceRecycler.layoutManager = LinearLayoutManager(this@MainActivity) - - val touchHelper = ItemTouchHelper(adapter.dragHelper) - touchHelper.attachToRecyclerView(binding.serviceRecycler) - - adapter.setDragStartListener { viewHolder -> - touchHelper.startDrag(viewHolder) + R.id.action_about -> { + startActivity(Intent(this, AboutActivity::class.java)) + true + } + else -> false + }.also { + binding.drawerLayout.closeDrawers() } - - // binding.serviceConfigList.removeAllViews() - // for ((mimetype, labelRes) in mimetypes) { - // val checkbox = CheckBox(this@MainActivity).apply { - // text = getString(labelRes) - // isChecked = this@MainActivity.isServiceEnabled(mimetype) - // setOnCheckedChangeListener { _, checked -> - // this@MainActivity.setServiceEnabled(mimetype, checked) - // } - // } - - // val priorityInput = EditText(this@MainActivity).apply { - // inputType = InputType.TYPE_CLASS_NUMBER - // setEms(4) - // hint = "Priority" - // setText(this@MainActivity.getServicePriority(mimetype).toString()) - // setOnFocusChangeListener { _, hasFocus -> - // if (!hasFocus) { - // val value = text.toString().toIntOrNull() - // if (value != null) this@MainActivity.setServicePriority(mimetype, value) - // } - // } - // } - - // val row = LinearLayout(this@MainActivity).apply { - // orientation = LinearLayout.HORIZONTAL - // setPadding(0, 16, 0, 16) - // addView(checkbox, LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)) - // addView(priorityInput, LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)) - // } - - // binding.serviceConfigList.addView(row) - // } - } - } - - private fun requestPermissions() { - when { - !hasGeneralPermissions(this) -> registerForGeneralPermissions.launch(REQUIRED_PERMISSIONS) - !hasDrawOverlays() -> requestDrawOverlays() - !hasCallRedirectionRole() -> requestCallRedirectionRole() } } private fun hasPermissions(): Boolean { - return hasGeneralPermissions(this) && - hasDrawOverlays(this) && - hasCallRedirectionRole(this) - } - - private fun requestDrawOverlays() { - registerForDrawOverlays.launch(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)) - } - - private fun hasDrawOverlays(): Boolean { - return Settings.canDrawOverlays(this) - } - - private fun requestCallRedirectionRole() { - registerForCallRedirectionRole - .launch(roleManager?.createRequestRoleIntent(RoleManager.ROLE_CALL_REDIRECTION)) - } - - private fun hasCallRedirectionRole(): Boolean { - return roleManager?.isRoleHeld(RoleManager.ROLE_CALL_REDIRECTION) ?: false + return hasGeneralPermissions(this) && + hasDrawOverlays(this) && + hasCallRedirectionRole(this) } } diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt new file mode 100644 index 0000000..aff9684 --- /dev/null +++ b/app/src/main/java/partisan/weforge/xyz/pulse/MainFragment.kt @@ -0,0 +1,29 @@ +package partisan.weforge.xyz.pulse + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ToggleButton +import androidx.fragment.app.Fragment + +class MainFragment : Fragment() { + + private lateinit var prefs: Preferences + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = inflater.inflate(R.layout.fragment_main, container, false) + prefs = Preferences(requireContext()) + + val toggle = view.findViewById(R.id.toggle) + toggle.isChecked = prefs.isEnabled + toggle.setOnCheckedChangeListener { _, isChecked -> + prefs.isEnabled = isChecked + } + + return view + } +} diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt b/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt new file mode 100644 index 0000000..9c084af --- /dev/null +++ b/app/src/main/java/partisan/weforge/xyz/pulse/PopupSettingsFragment.kt @@ -0,0 +1,118 @@ +package partisan.weforge.xyz.pulse + +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import partisan.weforge.xyz.pulse.databinding.FragmentPopupSettingsBinding + +class PopupSettingsFragment : Fragment() { + + private var _binding: FragmentPopupSettingsBinding? = null + private val binding get() = _binding!! + + private lateinit var prefs: Preferences + private lateinit var window: PopupWindow + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentPopupSettingsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + prefs = Preferences(requireContext()) + window = PopupWindow(requireContext(), null) + + val screenHeight = getScreenHeightPx() + + binding.apply { + redirectionDelay.value = (prefs.redirectionDelay / 1000).toFloat() + redirectionDelay.setLabelFormatter { + String.format("%.1f", it) + } + redirectionDelay.addOnChangeListener { _, value, _ -> + prefs.redirectionDelay = (value * 1000).toLong() + } + + popupEnabledCheckbox.isChecked = prefs.popupEnabled + popupEnabledCheckbox.setOnCheckedChangeListener { _, isChecked -> + prefs.popupEnabled = isChecked + } + + popupPreview.setOnClickListener { + window.preview() + } + + popupHeightSlider.valueFrom = 0f + popupHeightSlider.valueTo = screenHeight.toFloat() + popupHeightSlider.value = prefs.popupPosition.toFloat() + popupHeightSlider.addOnChangeListener { _, value, _ -> + prefs.popupPosition = value.toInt().coerceIn(0, screenHeight) + } + + val services = listOf( + ServiceEntry( + "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call", + R.string.destination_signal, + requireContext().isServiceEnabled("vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call") + ), + ServiceEntry( + "vnd.android.cursor.item/vnd.org.telegram.messenger.android.call", + R.string.destination_telegram, + requireContext().isServiceEnabled("vnd.android.cursor.item/vnd.org.telegram.messenger.android.call") + ), + ServiceEntry( + "vnd.android.cursor.item/vnd.ch.threema.app.call", + R.string.destination_threema, + requireContext().isServiceEnabled("vnd.android.cursor.item/vnd.ch.threema.app.call") + ), + ServiceEntry( + "vnd.android.cursor.item/vnd.com.whatsapp.voip.call", + R.string.destination_whatsapp, + requireContext().isServiceEnabled("vnd.android.cursor.item/vnd.com.whatsapp.voip.call") + ), + ) + + val adapter = ServiceAdapter( + context = requireContext(), + services = services.toMutableList(), + onReordered = { updatedList -> + updatedList.forEachIndexed { index, entry -> + requireContext().setServicePriority(entry.mimetype, index) + } + } + ) + + serviceRecycler.adapter = adapter + serviceRecycler.layoutManager = LinearLayoutManager(requireContext()) + + val touchHelper = ItemTouchHelper(adapter.dragHelper) + touchHelper.attachToRecyclerView(serviceRecycler) + + adapter.setDragStartListener { viewHolder -> + touchHelper.startDrag(viewHolder) + } + } + } + + private fun getScreenHeightPx(): Int { + val metrics = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(metrics) + return metrics.heightPixels + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt b/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt index 5909a36..da09ad8 100644 --- a/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt +++ b/app/src/main/java/partisan/weforge/xyz/pulse/Preferences.kt @@ -13,8 +13,9 @@ class Preferences(ctx: Context) { private const val DEFAULT_REDIRECTION_DELAY = 2000L private const val DEFAULT_POPUP_POSITION = 333 - // migration private const val SERVICE_ENABLED = "service_enabled" + + private const val POPUP_ENABLED = "popup_enabled" } private val prefs = PreferenceManager.getDefaultSharedPreferences(ctx) @@ -23,6 +24,10 @@ class Preferences(ctx: Context) { get() = prefs.getBoolean(ENABLED, prefs.getBoolean(SERVICE_ENABLED, false)) set(value) = prefs.edit { putBoolean(ENABLED, value) } + var popupEnabled: Boolean + get() = prefs.getBoolean(POPUP_ENABLED, true) + set(value) = prefs.edit { putBoolean(POPUP_ENABLED, value) } + var redirectionDelay: Long get() = prefs.getLong(REDIRECTION_DELAY, DEFAULT_REDIRECTION_DELAY) set(value) = prefs.edit { putLong(REDIRECTION_DELAY, value) } diff --git a/app/src/main/java/partisan/weforge/xyz/pulse/SettingsActivity.kt b/app/src/main/java/partisan/weforge/xyz/pulse/SettingsActivity.kt new file mode 100644 index 0000000..ebac437 --- /dev/null +++ b/app/src/main/java/partisan/weforge/xyz/pulse/SettingsActivity.kt @@ -0,0 +1,12 @@ +package partisan.weforge.xyz.pulse + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class SettingsActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // placeholder until real UI is added + setContentView(androidx.appcompat.R.layout.abc_action_bar_title_item) + } +} diff --git a/app/src/main/res/drawable/tooltip_24px.xml b/app/src/main/res/drawable/tooltip_24px.xml new file mode 100644 index 0000000..5770d07 --- /dev/null +++ b/app/src/main/res/drawable/tooltip_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1ef4824..36fed6b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,100 +1,43 @@ - + android:layout_height="match_parent"> - + + - + - + + - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml new file mode 100644 index 0000000..83e7f0d --- /dev/null +++ b/app/src/main/res/layout/fragment_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_popup_settings.xml b/app/src/main/res/layout/fragment_popup_settings.xml new file mode 100644 index 0000000..c9cec7c --- /dev/null +++ b/app/src/main/res/layout/fragment_popup_settings.xml @@ -0,0 +1,107 @@ + + + + + + + +