This commit is contained in:
lucky 2022-02-01 23:38:08 +03:00
commit 0c422c9562
45 changed files with 1724 additions and 0 deletions

View file

@ -0,0 +1,24 @@
package me.lucky.re
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("me.lucky.re", appContext.packageName)
}
}

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.lucky.re">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<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">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".CallRedirectionService"
android:exported="true"
android:permission="android.permission.BIND_CALL_REDIRECTION_SERVICE">
<intent-filter>
<action android:name="android.telecom.CallRedirectionService"/>
</intent-filter>
</service>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,12 @@
package me.lucky.re
import android.app.Application
import com.google.android.material.color.DynamicColors
@Suppress("unused")
class Application : Application() {
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
}
}

View file

@ -0,0 +1,100 @@
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

@ -0,0 +1,67 @@
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

@ -0,0 +1,100 @@
package me.lucky.re
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 androidx.appcompat.app.AppCompatActivity
import me.lucky.re.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
companion object {
private val PERMISSIONS = arrayOf(
Manifest.permission.READ_CONTACTS,
Manifest.permission.CALL_PHONE,
)
}
private lateinit var binding: ActivityMainBinding
private lateinit var prefs: Preferences
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()
}
private fun init() {
prefs = Preferences(this)
roleManager = getSystemService(RoleManager::class.java)
binding.apply {
toggle.isChecked = prefs.isServiceEnabled
}
}
private fun setup() {
binding.apply {
toggle.setOnCheckedChangeListener { _, isChecked ->
if (isChecked && !hasPermissions()) {
toggle.isChecked = false
requestPermissions()
return@setOnCheckedChangeListener
}
prefs.isServiceEnabled = isChecked
}
}
}
private fun requestPermissions() {
when {
!hasGeneralPermissions() -> requestGeneralPermissions()
!hasDrawOverlays() -> requestDrawOverlays()
!hasCallRedirectionRole() -> requestCallRedirectionRole()
}
}
private fun hasPermissions(): Boolean {
return hasGeneralPermissions() && hasDrawOverlays() && hasCallRedirectionRole()
}
private fun requestDrawOverlays() {
registerForDrawOverlays.launch(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION))
}
private fun requestGeneralPermissions() {
registerForGeneralPermissions.launch(PERMISSIONS)
}
private fun hasGeneralPermissions(): Boolean {
return !PERMISSIONS.any { checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED }
}
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
}
}

View file

@ -0,0 +1,17 @@
package me.lucky.re
import android.content.Context
import androidx.core.content.edit
import androidx.preference.PreferenceManager
class Preferences(ctx: Context) {
companion object {
private const val SERVICE_ENABLED = "service_enabled"
}
private val prefs = PreferenceManager.getDefaultSharedPreferences(ctx)
var isServiceEnabled: Boolean
get() = prefs.getBoolean(SERVICE_ENABLED, false)
set(value) = prefs.edit { putBoolean(SERVICE_ENABLED, value) }
}

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.13710937"
android:scaleY="0.13710937"
android:translateX="18.9"
android:translateY="18.9">
<path
android:pathData="M416,196c33.14,0 60,26.86 60,60c0,33.14 -26.86,60 -60,60s-60,-26.86 -60,-60C356,222.86 382.86,196 416,196z"
android:fillColor="#57606F"/>
<path
android:pathData="M256,196c33.14,0 60,26.86 60,60c0,33.14 -26.86,60 -60,60c-33.14,0 -60,-26.86 -60,-60C196,222.86 222.86,196 256,196z"
android:fillColor="#F73E54"/>
<path
android:pathData="M96,196c33.14,0 60,26.86 60,60c0,33.14 -26.86,60 -60,60s-60,-26.86 -60,-60C36,222.86 62.86,196 96,196z"
android:fillColor="#57606F"/>
</group>
</vector>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ToggleButton
android:id="@+id/toggle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingVertical="12dp"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
card_view:cardBackgroundColor="@android:color/transparent"
card_view:cardCornerRadius="32dp"
card_view:cardElevation="0dp"
card_view:contentPadding="0dp">
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/popup"
android:padding="16dp"
android:text="@string/info"
android:textColor="@color/black"
android:textSize="16sp" />
</androidx.cardview.widget.CardView>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,12 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Re" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

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

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="teal_200">#FF03DAC5</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="popup">#E3E3E3E3</color>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View file

@ -0,0 +1,6 @@
<?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>
</resources>

View file

@ -0,0 +1,12 @@
<resources>
<!-- Base application theme. -->
<style name="Theme.Re" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -0,0 +1,17 @@
package me.lucky.re
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}