rename to Red

show redirection destination
fix dialer end call
add priority Signal > Telegram
This commit is contained in:
lucky 2022-02-04 14:22:57 +03:00
parent 9b3c582337
commit 48a9998c26
29 changed files with 291 additions and 219 deletions

View file

@ -7,11 +7,11 @@ android {
compileSdk 32
defaultConfig {
applicationId "me.lucky.re"
applicationId "me.lucky.red"
minSdk 29
targetSdk 32
versionCode 1
versionName "1.0.0"
versionCode 2
versionName "1.0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View file

@ -1,4 +1,4 @@
package me.lucky.re
package me.lucky.red
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("me.lucky.re", appContext.packageName)
assertEquals("me.lucky.red", appContext.packageName)
}
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.lucky.re">
package="me.lucky.red">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />
@ -9,14 +9,12 @@
<uses-feature android:name="android.hardware.telephony" android:required="true" />
<application
android:allowBackup="true"
android:fullBackupContent="true"
android:icon="@mipmap/ic_launcher"
android:name=".Application"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Re">
android:theme="@style/Theme.Red">
<activity
android:name=".MainActivity"

View file

@ -1,100 +0,0 @@
package me.lucky.re
import android.database.Cursor
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.provider.ContactsContract
import android.telecom.CallRedirectionService
import android.telecom.PhoneAccountHandle
class CallRedirectionService : CallRedirectionService() {
companion object {
private const val SIGNAL_MIMETYPE = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call"
private const val TELEGRAM_MIMETYPE = "vnd.android.cursor.item/vnd.org.telegram.messenger.android.call"
}
private lateinit var prefs: Preferences
private lateinit var dialog: DialogWindow
private var connectivityManager: ConnectivityManager? = null
override fun onCreate() {
super.onCreate()
prefs = Preferences(this)
dialog = DialogWindow(this)
connectivityManager = getSystemService(ConnectivityManager::class.java)
}
override fun onPlaceCall(
handle: Uri,
initialPhoneAccount: PhoneAccountHandle,
allowInteractiveResponse: Boolean,
) {
if (!prefs.isServiceEnabled || !hasInternet()) {
placeCallUnmodified()
return
}
val uri = getUriFromPhoneNumber(handle.schemeSpecificPart)
if (uri != null) {
dialog.show(uri)
return
}
placeCallUnmodified()
}
private fun getContactIdByPhoneNumber(phoneNumber: String): String? {
var result: String? = null
val cursor: Cursor?
try {
cursor = contentResolver.query(
Uri.withAppendedPath(
ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
Uri.encode(phoneNumber)
),
arrayOf(ContactsContract.PhoneLookup._ID),
null,
null,
null,
)
} catch (exc: SecurityException) { return null }
cursor?.apply {
if (moveToFirst()) {
result = getString(getColumnIndexOrThrow(ContactsContract.PhoneLookup._ID))
}
close()
}
return result
}
private fun getUriFromPhoneNumber(phoneNumber: String): Uri? {
val contactId = getContactIdByPhoneNumber(phoneNumber) ?: return null
var result: Uri? = null
val cursor: Cursor?
try {
cursor = contentResolver.query(
ContactsContract.Data.CONTENT_URI,
arrayOf(ContactsContract.Data._ID),
"${ContactsContract.Data.CONTACT_ID} = ? AND " +
"${ContactsContract.Data.MIMETYPE} IN (?, ?)",
arrayOf(contactId, SIGNAL_MIMETYPE, TELEGRAM_MIMETYPE),
null,
)
} catch (exc: SecurityException) { return null }
cursor?.apply {
if (moveToFirst()) {
result = Uri.withAppendedPath(
ContactsContract.Data.CONTENT_URI,
Uri.encode(getString(getColumnIndexOrThrow(ContactsContract.Data._ID))),
)
}
close()
}
return result
}
private fun hasInternet(): Boolean {
return connectivityManager
?.getNetworkCapabilities(connectivityManager?.activeNetwork)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false
}
}

View file

@ -1,67 +0,0 @@
package me.lucky.re
import android.content.Intent
import android.graphics.PixelFormat
import android.net.Uri
import android.view.Gravity
import android.view.LayoutInflater
import android.view.WindowManager
import java.util.*
import kotlin.concurrent.timerTask
class DialogWindow(private val service: CallRedirectionService) {
companion object {
private const val CANCEL_DELAY = 3000L
}
private val windowManager = service
.applicationContext
.getSystemService(WindowManager::class.java)
@Suppress("InflateParams")
private val floatView = LayoutInflater
.from(service.applicationContext)
.inflate(R.layout.popup, null)
private val layoutParams: WindowManager.LayoutParams = WindowManager.LayoutParams().apply {
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
gravity = Gravity.BOTTOM
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
y = 384
}
private var data: Uri? = null
private var timer: Timer? = null
init {
floatView.setOnClickListener {
timer?.cancel()
service.placeCallUnmodified()
remove()
}
}
fun show(uri: Uri?) {
remove()
timer?.cancel()
timer = Timer()
timer?.schedule(timerTask {
service.cancelCall()
remove()
Intent(Intent.ACTION_VIEW).also {
it.data = data
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
service.startActivity(it)
}
data = null
}, CANCEL_DELAY)
data = uri
windowManager?.addView(floatView, layoutParams)
}
private fun remove() {
try {
windowManager?.removeView(floatView)
} catch (exc: IllegalArgumentException) {}
}
}

View file

@ -1,4 +1,4 @@
package me.lucky.re
package me.lucky.red
import android.app.Application
import com.google.android.material.color.DynamicColors

View file

@ -0,0 +1,124 @@
package me.lucky.red
import android.Manifest
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.provider.ContactsContract
import android.telecom.CallRedirectionService
import android.telecom.PhoneAccountHandle
import androidx.annotation.RequiresPermission
class CallRedirectionService : CallRedirectionService() {
companion object {
private const val SIGNAL_MIMETYPE =
"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call"
private const val TELEGRAM_MIMETYPE =
"vnd.android.cursor.item/vnd.org.telegram.messenger.android.call"
private val MIMETYPES = mapOf(
SIGNAL_MIMETYPE to 0,
TELEGRAM_MIMETYPE to 1,
)
}
private lateinit var prefs: Preferences
private lateinit var window: PopupWindow
private var connectivityManager: ConnectivityManager? = null
override fun onCreate() {
super.onCreate()
init()
}
private fun init() {
prefs = Preferences(this)
window = PopupWindow(this)
connectivityManager = getSystemService(ConnectivityManager::class.java)
}
override fun onPlaceCall(
handle: Uri,
initialPhoneAccount: PhoneAccountHandle,
allowInteractiveResponse: Boolean,
) {
if (!prefs.isServiceEnabled || !hasInternet() || !allowInteractiveResponse) {
placeCallUnmodified()
return
}
val records: Array<Record>
try {
records = getRecordsFromPhoneNumber(handle.schemeSpecificPart)
} catch (exc: SecurityException) {
placeCallUnmodified()
return
}
val record = records.minByOrNull { MIMETYPES[it.mimetype] ?: 0 }
if (record == null) {
placeCallUnmodified()
return
}
window.show(record.uri, when (record.mimetype) {
SIGNAL_MIMETYPE -> R.string.signal
TELEGRAM_MIMETYPE -> R.string.telegram
else -> return
})
}
@RequiresPermission(Manifest.permission.READ_CONTACTS)
private fun getContactIdByPhoneNumber(phoneNumber: String): String? {
var result: String? = null
val cursor = contentResolver.query(
Uri.withAppendedPath(
ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
Uri.encode(phoneNumber)
),
arrayOf(ContactsContract.PhoneLookup._ID),
null,
null,
null,
)
cursor?.apply {
if (moveToFirst())
result = getString(getColumnIndexOrThrow(ContactsContract.PhoneLookup._ID))
close()
}
return result
}
private data class Record(val uri: Uri, val mimetype: String)
@RequiresPermission(Manifest.permission.READ_CONTACTS)
private fun getRecordsFromPhoneNumber(phoneNumber: String): Array<Record> {
val results = mutableSetOf<Record>()
val contactId = getContactIdByPhoneNumber(phoneNumber) ?: return results.toTypedArray()
val cursor = contentResolver.query(
ContactsContract.Data.CONTENT_URI,
arrayOf(ContactsContract.Data._ID, ContactsContract.Data.MIMETYPE),
"${ContactsContract.Data.CONTACT_ID} = ? AND " +
"${ContactsContract.Data.MIMETYPE} IN " +
"(${MIMETYPES.keys.joinToString(",") { "?" }})",
arrayOf(contactId, *MIMETYPES.keys.toTypedArray()),
null,
)
cursor?.apply {
while (moveToNext())
results.add(Record(
Uri.withAppendedPath(
ContactsContract.Data.CONTENT_URI,
Uri.encode(getString(getColumnIndexOrThrow(ContactsContract.Data._ID))),
),
getString(getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE)),
))
close()
}
return results.toTypedArray()
}
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
private fun hasInternet(): Boolean {
val capabilities = connectivityManager
?.getNetworkCapabilities(connectivityManager?.activeNetwork) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
}

View file

@ -1,4 +1,4 @@
package me.lucky.re
package me.lucky.red
import android.Manifest
import android.app.role.RoleManager
@ -9,7 +9,7 @@ import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import me.lucky.re.databinding.ActivityMainBinding
import me.lucky.red.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
companion object {

View file

@ -0,0 +1,107 @@
package me.lucky.red
import android.Manifest
import android.content.Intent
import android.graphics.PixelFormat
import android.media.AudioManager
import android.net.Uri
import android.view.Gravity
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.TextView
import androidx.annotation.RequiresPermission
import java.util.*
import kotlin.concurrent.timerTask
class PopupWindow(private val service: CallRedirectionService) {
companion object {
private const val CANCEL_DELAY = 2000L
}
private val windowManager = service
.applicationContext
.getSystemService(WindowManager::class.java)
private val audioManager = service
.applicationContext
.getSystemService(AudioManager::class.java)
@Suppress("InflateParams")
private val view = LayoutInflater
.from(service.applicationContext)
.inflate(R.layout.popup, null)
private val layoutParams = WindowManager.LayoutParams().apply {
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
gravity = Gravity.BOTTOM
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
y = 333
}
private var timer: Timer? = null
init {
view.setOnClickListener {
timer?.cancel()
service.placeCallUnmodified()
remove()
}
}
fun show(uri: Uri, destinationId: Int) {
if (!remove()) {
service.placeCallUnmodified()
return
}
timer?.cancel()
timer = Timer()
timer?.schedule(timerTask {
if (!remove()) {
service.placeCallUnmodified()
return@timerTask
}
if (audioManager?.mode != AudioManager.MODE_IN_CALL) {
service.placeCallUnmodified()
return@timerTask
}
try {
call(uri)
} catch (exc: SecurityException) {
service.placeCallUnmodified()
return@timerTask
}
service.cancelCall()
}, CANCEL_DELAY)
view.findViewById<TextView>(R.id.description).text = String.format(
service.getString(R.string.popup),
service.getString(destinationId),
)
if (!add()) {
timer?.cancel()
service.placeCallUnmodified()
}
}
@RequiresPermission(Manifest.permission.CALL_PHONE)
private fun call(data: Uri) {
Intent(Intent.ACTION_VIEW).let {
it.data = data
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
service.startActivity(it)
}
}
private fun add(): Boolean {
try {
windowManager?.addView(view, layoutParams)
} catch (exc: WindowManager.BadTokenException) { return false }
return true
}
private fun remove(): Boolean {
try {
windowManager?.removeView(view)
} catch (exc: IllegalArgumentException) {
} catch (exc: WindowManager.BadTokenException) { return false }
return true
}
}

View file

@ -1,4 +1,4 @@
package me.lucky.re
package me.lucky.red
import android.content.Context
import androidx.core.content.edit

View file

@ -14,7 +14,6 @@
android:layout_height="wrap_content"
android:background="@color/popup"
android:padding="16dp"
android:text="@string/info"
android:textColor="@color/black"
android:textSize="16sp" />

View file

@ -1,6 +1,6 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Re" parent="Theme.Material3.DayNight.NoActionBar">
<style name="Theme.Red" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorOnPrimary">@color/black</item>

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Re</string>
<string name="description">Приложение будет от всей души пытаться перенаправить исходящие вызовы в Сигнал или Телеграм, но иногда у него может не получаться. Для работы ему нужны тонны разрешений. Кликайте на переключатель и выдавайте разрешения пока он не включится.</string>
<string name="info">Перенаправление</string>
<string name="app_name">Red</string>
<string name="description">Приложение будет пытаться перенаправить исходящие вызовы в Signal/Telegram. Для работы ему нужно много разрешений. Кликайте на переключатель и выдавайте разрешения пока он не включится.</string>
<string name="popup">Перенаправление в %1$s</string>
<string name="signal">Signal</string>
<string name="telegram">Telegram</string>
</resources>

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Re</string>
<string name="description">The app will try to redirect outgoing calls to Signal or Telegram if available. For work it will require many permissions. Click on toggle and grant permissions until it turns ON.</string>
<string name="info">Redirecting</string>
<string name="app_name">Red</string>
<string name="description">The app will try to redirect outgoing calls to Signal/Telegram if available. For work it requires many permissions. Click on the toggle and grant permissions until it turns ON.</string>
<string name="popup">Redirecting to %1$s</string>
<string name="signal">Signal</string>
<string name="telegram">Telegram</string>
</resources>

View file

@ -1,6 +1,6 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Re" parent="Theme.Material3.DayNight.NoActionBar">
<style name="Theme.Red" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorOnPrimary">@color/white</item>

View file

@ -1,4 +1,4 @@
package me.lucky.re
package me.lucky.red
import org.junit.Test