diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6a1d69f9..55d3de36 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + () @@ -31,6 +39,10 @@ class DialpadActivity : SimpleActivity() { private val russianCharsMap = HashMap() private var hasRussianLocale = false private var privateCursor: Cursor? = null + private var toneGeneratorHelper: ToneGeneratorHelper? = null + private val longPressTimeout = ViewConfiguration.getLongPressTimeout().toLong() + private val longPressHandler = Handler(Looper.getMainLooper()) + private val pressedKeys = mutableSetOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -45,6 +57,8 @@ class DialpadActivity : SimpleActivity() { speedDialValues = config.getSpeedDialValues() privateCursor = getMyContactsCursor(false, true) + toneGeneratorHelper = ToneGeneratorHelper(this) + if (hasRussianLocale) { initRussianChars() dialpad_2_letters.append("\nАБВГ") @@ -65,30 +79,19 @@ class DialpadActivity : SimpleActivity() { } } - dialpad_0_holder.setOnClickListener { dialpadPressed('0', it) } - dialpad_1_holder.setOnClickListener { dialpadPressed('1', it) } - dialpad_2_holder.setOnClickListener { dialpadPressed('2', it) } - dialpad_3_holder.setOnClickListener { dialpadPressed('3', it) } - dialpad_4_holder.setOnClickListener { dialpadPressed('4', it) } - dialpad_5_holder.setOnClickListener { dialpadPressed('5', it) } - dialpad_6_holder.setOnClickListener { dialpadPressed('6', it) } - dialpad_7_holder.setOnClickListener { dialpadPressed('7', it) } - dialpad_8_holder.setOnClickListener { dialpadPressed('8', it) } - dialpad_9_holder.setOnClickListener { dialpadPressed('9', it) } + setupCharClick(dialpad_1_holder, '1') + setupCharClick(dialpad_2_holder, '2') + setupCharClick(dialpad_3_holder, '3') + setupCharClick(dialpad_4_holder, '4') + setupCharClick(dialpad_5_holder, '5') + setupCharClick(dialpad_6_holder, '6') + setupCharClick(dialpad_7_holder, '7') + setupCharClick(dialpad_8_holder, '8') + setupCharClick(dialpad_9_holder, '9') + setupCharClick(dialpad_0_holder, '0') + setupCharClick(dialpad_asterisk_holder, '*', longClickable = false) + setupCharClick(dialpad_hashtag_holder, '#', longClickable = false) - dialpad_1_holder.setOnLongClickListener { speedDial(1); true } - dialpad_2_holder.setOnLongClickListener { speedDial(2); true } - dialpad_3_holder.setOnLongClickListener { speedDial(3); true } - dialpad_4_holder.setOnLongClickListener { speedDial(4); true } - dialpad_5_holder.setOnLongClickListener { speedDial(5); true } - dialpad_6_holder.setOnLongClickListener { speedDial(6); true } - dialpad_7_holder.setOnLongClickListener { speedDial(7); true } - dialpad_8_holder.setOnLongClickListener { speedDial(8); true } - dialpad_9_holder.setOnLongClickListener { speedDial(9); true } - - dialpad_0_holder.setOnLongClickListener { dialpadPressed('+', null); true } - dialpad_asterisk_holder.setOnClickListener { dialpadPressed('*', it) } - dialpad_hashtag_holder.setOnClickListener { dialpadPressed('#', it) } dialpad_clear_char.setOnClickListener { clearChar(it) } dialpad_clear_char.setOnLongClickListener { clearInput(); true } dialpad_call_button.setOnClickListener { initCall(dialpad_input.value, 0) } @@ -164,12 +167,12 @@ class DialpadActivity : SimpleActivity() { private fun dialpadPressed(char: Char, view: View?) { dialpad_input.addCharacter(char) - view?.performHapticFeedback() + maybePerformDialpadHapticFeedback(view) } private fun clearChar(view: View) { dialpad_input.dispatchKeyEvent(dialpad_input.getKeyEvent(KeyEvent.KEYCODE_DEL)) - view.performHapticFeedback() + maybePerformDialpadHapticFeedback(view) } private fun clearInput() { @@ -270,13 +273,15 @@ class DialpadActivity : SimpleActivity() { } } - private fun speedDial(id: Int) { - if (dialpad_input.value.isEmpty()) { + private fun speedDial(id: Int): Boolean { + if (dialpad_input.value.length == 1) { val speedDial = speedDialValues.firstOrNull { it.id == id } if (speedDial?.isValid() == true) { initCall(speedDial.number, -1) + return true } } + return false } private fun initRussianChars() { @@ -289,4 +294,77 @@ class DialpadActivity : SimpleActivity() { russianCharsMap['ш'] = 8; russianCharsMap['щ'] = 8; russianCharsMap['ъ'] = 8; russianCharsMap['ы'] = 8 russianCharsMap['ь'] = 9; russianCharsMap['э'] = 9; russianCharsMap['ю'] = 9; russianCharsMap['я'] = 9 } + + private fun startDialpadTone(char: Char) { + if (config.dialpadBeeps) { + pressedKeys.add(char) + toneGeneratorHelper?.startTone(char) + } + } + + private fun stopDialpadTone(char: Char) { + if (config.dialpadBeeps) { + if (!pressedKeys.remove(char)) return + if (pressedKeys.isEmpty()) { + toneGeneratorHelper?.stopTone() + } else { + startDialpadTone(pressedKeys.last()) + } + } + } + + private fun maybePerformDialpadHapticFeedback(view: View?) { + if (config.dialpadVibration) { + view?.performHapticFeedback() + } + } + + private fun performLongClick(view: View, char: Char) { + if (char == '0') { + clearChar(view) + dialpadPressed('+', view) + } else { + val result = speedDial(char.digitToInt()) + if (result) { + stopDialpadTone(char) + clearChar(view) + } + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun setupCharClick(view: View, char: Char, longClickable: Boolean = true) { + view.isClickable = true + view.isLongClickable = true + view.setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + dialpadPressed(char, view) + startDialpadTone(char) + if (longClickable) { + longPressHandler.removeCallbacksAndMessages(null) + longPressHandler.postDelayed({ + performLongClick(view, char) + }, longPressTimeout) + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + stopDialpadTone(char) + if (longClickable) { + longPressHandler.removeCallbacksAndMessages(null) + } + } + MotionEvent.ACTION_MOVE -> { + val viewContainsTouchEvent = view.boundingBox.contains(event.rawX.roundToInt(), event.rawY.roundToInt()) + if (!viewContainsTouchEvent) { + stopDialpadTone(char) + if (longClickable) { + longPressHandler.removeCallbacksAndMessages(null) + } + } + } + } + false + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/dialer/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/dialer/activities/SettingsActivity.kt index 1bc55438..f85653fb 100644 --- a/app/src/main/kotlin/com/simplemobiletools/dialer/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/dialer/activities/SettingsActivity.kt @@ -39,6 +39,8 @@ class SettingsActivity : SimpleActivity() { setupDialPadOpen() setupGroupSubsequentCalls() setupStartNameWithSurname() + setupDialpadVibrations() + setupDialpadBeeps() setupShowCallConfirmation() setupDisableProximitySensor() setupDisableSwipeToAnswer() @@ -206,6 +208,22 @@ class SettingsActivity : SimpleActivity() { } } + private fun setupDialpadVibrations() { + settings_dialpad_vibration.isChecked = config.dialpadVibration + settings_dialpad_vibration_holder.setOnClickListener { + settings_dialpad_vibration.toggle() + config.dialpadVibration = settings_dialpad_vibration.isChecked + } + } + + private fun setupDialpadBeeps() { + settings_dialpad_beeps.isChecked = config.dialpadBeeps + settings_dialpad_beeps_holder.setOnClickListener { + settings_dialpad_beeps.toggle() + config.dialpadBeeps = settings_dialpad_beeps.isChecked + } + } + private fun setupShowCallConfirmation() { settings_show_call_confirmation.isChecked = config.showCallConfirmation settings_show_call_confirmation_holder.setOnClickListener { diff --git a/app/src/main/kotlin/com/simplemobiletools/dialer/extensions/View.kt b/app/src/main/kotlin/com/simplemobiletools/dialer/extensions/View.kt new file mode 100644 index 00000000..60980027 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/dialer/extensions/View.kt @@ -0,0 +1,8 @@ +package com.simplemobiletools.dialer.extensions + +import android.graphics.Rect +import android.view.View + +val View.boundingBox + get() = Rect().also { getGlobalVisibleRect(it) } + diff --git a/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/CallManager.kt b/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/CallManager.kt index 8ff7fa19..f86f09ab 100644 --- a/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/CallManager.kt +++ b/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/CallManager.kt @@ -1,9 +1,12 @@ package com.simplemobiletools.dialer.helpers import android.annotation.SuppressLint +import android.content.Context +import android.os.Handler import android.telecom.Call import android.telecom.InCallService import android.telecom.VideoProfile +import com.simplemobiletools.dialer.extensions.config import com.simplemobiletools.dialer.extensions.getStateCompat import com.simplemobiletools.dialer.extensions.hasCapability import com.simplemobiletools.dialer.extensions.isConference @@ -167,9 +170,15 @@ class CallManager { fun getState() = getPrimaryCall()?.getStateCompat() - fun keypad(c: Char) { - call?.playDtmfTone(c) - call?.stopDtmfTone() + fun keypad(context: Context, char: Char) { + call?.playDtmfTone(char) + if (context.config.dialpadBeeps) { + Handler().postDelayed({ + call?.stopDtmfTone() + }, DIALPAD_TONE_LENGTH_MS) + } else { + call?.stopDtmfTone() + } } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/Config.kt index 94ec51de..070341aa 100644 --- a/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/Config.kt @@ -71,4 +71,12 @@ class Config(context: Context) : BaseConfig(context) { var wasOverlaySnackbarConfirmed: Boolean get() = prefs.getBoolean(WAS_OVERLAY_SNACKBAR_CONFIRMED, false) set(wasOverlaySnackbarConfirmed) = prefs.edit().putBoolean(WAS_OVERLAY_SNACKBAR_CONFIRMED, wasOverlaySnackbarConfirmed).apply() + + var dialpadVibration: Boolean + get() = prefs.getBoolean(DIALPAD_VIBRATION, true) + set(dialpadVibration) = prefs.edit().putBoolean(DIALPAD_VIBRATION, dialpadVibration).apply() + + var dialpadBeeps: Boolean + get() = prefs.getBoolean(DIALPAD_BEEPS, false) + set(dialpadBeeps) = prefs.edit().putBoolean(DIALPAD_BEEPS, dialpadBeeps).apply() } diff --git a/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/Constants.kt index fc44663c..3ed434d7 100644 --- a/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/Constants.kt @@ -1,8 +1,8 @@ package com.simplemobiletools.dialer.helpers +import com.simplemobiletools.commons.helpers.TAB_CALL_HISTORY import com.simplemobiletools.commons.helpers.TAB_CONTACTS import com.simplemobiletools.commons.helpers.TAB_FAVORITES -import com.simplemobiletools.commons.helpers.TAB_CALL_HISTORY // shared prefs const val SPEED_DIAL = "speed_dial" @@ -15,7 +15,8 @@ const val SHOW_TABS = "show_tabs" const val FAVORITES_CONTACTS_ORDER = "favorites_contacts_order" const val FAVORITES_CUSTOM_ORDER_SELECTED = "favorites_custom_order_selected" const val WAS_OVERLAY_SNACKBAR_CONFIRMED = "was_overlay_snackbar_confirmed" - +const val DIALPAD_VIBRATION = "dialpad_vibration" +const val DIALPAD_BEEPS = "dialpad_beeps" const val ALL_TABS_MASK = TAB_CONTACTS or TAB_FAVORITES or TAB_CALL_HISTORY val tabsList = arrayListOf(TAB_CONTACTS, TAB_FAVORITES, TAB_CALL_HISTORY) @@ -23,3 +24,5 @@ val tabsList = arrayListOf(TAB_CONTACTS, TAB_FAVORITES, TAB_CALL_HISTORY) private const val PATH = "com.simplemobiletools.dialer.action." const val ACCEPT_CALL = PATH + "accept_call" const val DECLINE_CALL = PATH + "decline_call" + +const val DIALPAD_TONE_LENGTH_MS = 150L // The length of DTMF tones in milliseconds diff --git a/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/ToneGeneratorHelper.kt b/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/ToneGeneratorHelper.kt new file mode 100644 index 00000000..f2008ecc --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/dialer/helpers/ToneGeneratorHelper.kt @@ -0,0 +1,47 @@ +package com.simplemobiletools.dialer.helpers + +import android.content.Context +import android.media.AudioManager +import android.media.AudioManager.STREAM_DTMF +import android.media.ToneGenerator + +class ToneGeneratorHelper(context: Context) { + private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + private val toneGenerator = ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME) + + private fun isSilent(): Boolean { + return audioManager.ringerMode in arrayOf(AudioManager.RINGER_MODE_SILENT, AudioManager.RINGER_MODE_VIBRATE) + } + + fun startTone(char: Char) { + startTone(charToTone[char] ?: -1) + } + + private fun startTone(tone: Int) { + if (tone != -1 && !isSilent()) { + toneGenerator.startTone(tone) + } + } + + fun stopTone() = toneGenerator.stopTone() + + companion object { + const val TONE_RELATIVE_VOLUME = 80 // The DTMF tone volume relative to other sounds in the stream + const val DIAL_TONE_STREAM_TYPE = STREAM_DTMF + + private val charToTone = HashMap().apply { + put('0', ToneGenerator.TONE_DTMF_0) + put('1', ToneGenerator.TONE_DTMF_1) + put('2', ToneGenerator.TONE_DTMF_2) + put('3', ToneGenerator.TONE_DTMF_3) + put('4', ToneGenerator.TONE_DTMF_4) + put('5', ToneGenerator.TONE_DTMF_5) + put('6', ToneGenerator.TONE_DTMF_6) + put('7', ToneGenerator.TONE_DTMF_7) + put('8', ToneGenerator.TONE_DTMF_8) + put('9', ToneGenerator.TONE_DTMF_9) + put('#', ToneGenerator.TONE_DTMF_P) + put('*', ToneGenerator.TONE_DTMF_S) + } + } +} diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 47480e83..10c7a6d0 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -190,7 +190,7 @@ style="@style/SettingsHolderCheckboxStyle" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/ripple_bottom_corners"> + android:background="@drawable/ripple_background"> + + + + + + + + + + + +