From d43e991ae84196daa29741c8da98ff7eb947543f Mon Sep 17 00:00:00 2001 From: krlvm <51774833+krlvm@users.noreply.github.com> Date: Sat, 27 Jul 2024 18:09:45 +0300 Subject: [PATCH] Add support for Quick Tile --- app/src/main/AndroidManifest.xml | 13 ++ .../byedpi/activities/MainActivity.kt | 45 +----- .../byedpi/services/QuickTileService.kt | 133 ++++++++++++++++++ .../byedpi/services/ServiceManager.kt | 49 +++++++ .../byedpi/utility/NotificationUtils.kt | 2 +- .../res/drawable-hdpi/ic_notification.png | Bin 0 -> 820 bytes .../res/drawable-mdpi/ic_notification.png | Bin 0 -> 486 bytes .../res/drawable-xhdpi/ic_notification.png | Bin 0 -> 1158 bytes .../res/drawable-xxhdpi/ic_notification.png | Bin 0 -> 1908 bytes .../res/drawable-xxxhdpi/ic_notification.png | Bin 0 -> 2773 bytes 10 files changed, 201 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/io/github/dovecoteescapee/byedpi/services/QuickTileService.kt create mode 100644 app/src/main/java/io/github/dovecoteescapee/byedpi/services/ServiceManager.kt create mode 100644 app/src/main/res/drawable-hdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-mdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_notification.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 95bf82d..e593f5c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ android:launchMode="singleInstance"> + @@ -55,6 +56,18 @@ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="proxy" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt index c115b1d..58b1cd1 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt @@ -15,8 +15,6 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope -import io.github.dovecoteescapee.byedpi.data.START_ACTION -import io.github.dovecoteescapee.byedpi.data.STOP_ACTION import io.github.dovecoteescapee.byedpi.R import io.github.dovecoteescapee.byedpi.data.AppStatus import io.github.dovecoteescapee.byedpi.data.FAILED_BROADCAST @@ -27,8 +25,7 @@ import io.github.dovecoteescapee.byedpi.data.STOPPED_BROADCAST import io.github.dovecoteescapee.byedpi.data.Sender import io.github.dovecoteescapee.byedpi.fragments.SettingsFragment import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding -import io.github.dovecoteescapee.byedpi.services.ByeDpiProxyService -import io.github.dovecoteescapee.byedpi.services.ByeDpiVpnService +import io.github.dovecoteescapee.byedpi.services.ServiceManager import io.github.dovecoteescapee.byedpi.services.appStatus import io.github.dovecoteescapee.byedpi.utility.getPreferences import io.github.dovecoteescapee.byedpi.utility.mode @@ -57,7 +54,7 @@ class MainActivity : AppCompatActivity() { private val vpnRegister = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == RESULT_OK) { - startVpn() + ServiceManager.start(this, Mode.VPN) } else { Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_SHORT).show() updateStatus() @@ -212,48 +209,16 @@ class MainActivity : AppCompatActivity() { if (intentPrepare != null) { vpnRegister.launch(intentPrepare) } else { - startVpn() + ServiceManager.start(this, Mode.VPN) } } - Mode.Proxy -> startProxy() + Mode.Proxy -> ServiceManager.start(this, Mode.Proxy) } } - private fun startVpn() { - Log.i(TAG, "Starting VPN") - val intent = Intent(this, ByeDpiVpnService::class.java) - intent.action = START_ACTION - startService(intent) - } - - private fun startProxy() { - Log.i(TAG, "Starting proxy") - val intent = Intent(this, ByeDpiProxyService::class.java) - intent.action = START_ACTION - startService(intent) - } - private fun stop() { - val (_, mode) = appStatus - when (mode) { - Mode.VPN -> stopVpn() - Mode.Proxy -> stopProxy() - } - } - - private fun stopVpn() { - Log.i(TAG, "Stopping VPN") - val intent = Intent(this, ByeDpiVpnService::class.java) - intent.action = STOP_ACTION - startService(intent) - } - - private fun stopProxy() { - Log.i(TAG, "Stopping proxy") - val intent = Intent(this, ByeDpiProxyService::class.java) - intent.action = STOP_ACTION - startService(intent) + ServiceManager.stop(this) } private fun updateStatus() { diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/QuickTileService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/QuickTileService.kt new file mode 100644 index 0000000..3647d6b --- /dev/null +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/QuickTileService.kt @@ -0,0 +1,133 @@ +package io.github.dovecoteescapee.byedpi.services + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.VpnService +import android.os.Build +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import android.util.Log +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import androidx.core.service.quicksettings.PendingIntentActivityWrapper +import androidx.core.service.quicksettings.TileServiceCompat +import io.github.dovecoteescapee.byedpi.R +import io.github.dovecoteescapee.byedpi.activities.MainActivity +import io.github.dovecoteescapee.byedpi.data.AppStatus +import io.github.dovecoteescapee.byedpi.data.FAILED_BROADCAST +import io.github.dovecoteescapee.byedpi.data.Mode +import io.github.dovecoteescapee.byedpi.data.SENDER +import io.github.dovecoteescapee.byedpi.data.STARTED_BROADCAST +import io.github.dovecoteescapee.byedpi.data.STOPPED_BROADCAST +import io.github.dovecoteescapee.byedpi.data.Sender +import io.github.dovecoteescapee.byedpi.utility.getPreferences +import io.github.dovecoteescapee.byedpi.utility.mode + + +@RequiresApi(Build.VERSION_CODES.N) +class QuickTileService : TileService() { + + companion object { + private val TAG: String = QuickTileService::class.java.simpleName + } + + private val receiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val senderOrd = intent.getIntExtra(SENDER, -1) + val sender = Sender.entries.getOrNull(senderOrd) + if (sender == null) { + Log.w(TAG, "Received intent with unknown sender: $senderOrd") + return + } + + when (val action = intent.action) { + STARTED_BROADCAST, + STOPPED_BROADCAST -> updateStatus() + + FAILED_BROADCAST -> { + Toast.makeText( + context, + getString(R.string.failed_to_start, sender.name), + Toast.LENGTH_SHORT, + ).show() + updateStatus() + } + + else -> Log.w(TAG, "Unknown action: $action") + } + } + } + + override fun onStartListening() { + updateStatus() + ContextCompat.registerReceiver( + this, + receiver, + IntentFilter().apply { + addAction(STARTED_BROADCAST) + addAction(STOPPED_BROADCAST) + addAction(FAILED_BROADCAST) + }, + ContextCompat.RECEIVER_EXPORTED, + ) + } + + override fun onStopListening() { + unregisterReceiver(receiver) + } + + private fun launchActivity() { + TileServiceCompat.startActivityAndCollapse( + this, PendingIntentActivityWrapper( + this, 0, Intent(this, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT, false + ) + ) + } + + override fun onClick() { + if (qsTile.state == Tile.STATE_UNAVAILABLE) { + return + } + + unlockAndRun(this::handleClick) + } + + private fun setState(newState: Int) { + qsTile.apply { + state = newState + updateTile() + } + } + + private fun updateStatus() { + val (status) = appStatus + setState(if (status == AppStatus.Halted) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE) + } + + private fun handleClick() { + setState(Tile.STATE_ACTIVE) + setState(Tile.STATE_UNAVAILABLE) + + val (status) = appStatus + when (status) { + AppStatus.Halted -> { + val mode = getPreferences(this).mode() + + if (mode == Mode.VPN && VpnService.prepare(this) != null) { + updateStatus() + launchActivity() + return + } + + ServiceManager.start(this, mode) + } + + AppStatus.Running -> ServiceManager.stop(this) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ServiceManager.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ServiceManager.kt new file mode 100644 index 0000000..0e82742 --- /dev/null +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ServiceManager.kt @@ -0,0 +1,49 @@ +package io.github.dovecoteescapee.byedpi.services + +import android.content.Context +import android.content.Intent +import android.util.Log +import io.github.dovecoteescapee.byedpi.data.Mode +import io.github.dovecoteescapee.byedpi.data.START_ACTION +import io.github.dovecoteescapee.byedpi.data.STOP_ACTION + +object ServiceManager { + private val TAG: String = ServiceManager::class.java.simpleName + + fun start(context: Context, mode: Mode) { + when (mode) { + Mode.VPN -> { + Log.i(TAG, "Starting VPN") + val intent = Intent(context, ByeDpiVpnService::class.java) + intent.action = START_ACTION + context.startService(intent) + } + + Mode.Proxy -> { + Log.i(TAG, "Starting proxy") + val intent = Intent(context, ByeDpiProxyService::class.java) + intent.action = START_ACTION + context.startService(intent) + } + } + } + + fun stop(context: Context) { + val (_, mode) = appStatus + when (mode) { + Mode.VPN -> { + Log.i(TAG, "Stopping VPN") + val intent = Intent(context, ByeDpiVpnService::class.java) + intent.action = STOP_ACTION + context.startService(intent) + } + + Mode.Proxy -> { + Log.i(TAG, "Stopping proxy") + val intent = Intent(context, ByeDpiProxyService::class.java) + intent.action = STOP_ACTION + context.startService(intent) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/NotificationUtils.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/NotificationUtils.kt index e7850a7..4ec01ae 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/NotificationUtils.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/NotificationUtils.kt @@ -38,7 +38,7 @@ fun createConnectionNotification( service: Class<*>, ): Notification = NotificationCompat.Builder(context, channelId) - .setSmallIcon(R.mipmap.ic_launcher) + .setSmallIcon(R.drawable.ic_notification) .setSilent(true) .setContentTitle(context.getString(title)) .setContentText(context.getString(content)) diff --git a/app/src/main/res/drawable-hdpi/ic_notification.png b/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..7ce6f29c22efaf4187c56d52075726c03bbf3b07 GIT binary patch literal 820 zcmV-41Izr0P)HID2WG($mIbE4>EWp z5}_nX7?)gf%XRtItp7OMYM;xT&K#xr>Z^0k{?}Un{`dYbr>Ll#Zc-y6v+*|8`D_0K zI&wvfCV#Eq(1`)~4S!&Rzg}?Y#JVs#XR!{8eVc+o@oIdAUvUP$$2q=T!JzmkJcaAA z%?3)Q`gSRy@kU&O<^Eb{Ax9++7aJvgRCn<*mio3Sq4C3T7aF_>t9+YmAZ{GKzz_Hi zSKuHVjT3$Ql+bt~{_N#^TEGd4sQ+6=Ks>XT?mh&2cGy3`ye{BX%QkoW?4=gnfLitRQd*-WQv`y29#e zn+{zB_xc|L-*GUO`8ruca8EprZ-ck+pxBEZT^bafN%dkAAQdgXMox&mko|Bu9>Q0l zfObM(e&vqX4d_UwVzbYkGm?BEmCv{fC*Yg#o9oUalJblR%=vt+x13hl^_wBnBv1zWGYm|G~h|RbYYkgdoL9y+B zbtRq)l84i0Mkq2_Y$_|diYw98`$pvRHXol?C@>IThGK1!JeufYsYh02K0XPb_0&+~ z^W+VZ?+%4@M8=1LiXJ@@XY!*sOKfe@ZKcBJ$s1zRTnp_JJ7*`*$C&ub{1}UIq~!QV yp;J&~sMvPWAojjo6+77JezqSQeM~q19lrsb%Hx81LX5@$0000@P)NQmMjmczP9N%;p{gc6~&hjs8HNlBC(*iwpv6H4TwkaBPk z3K7M4#>|`9?`~l^$x~0;JMU*cZ_PXJY(m067Nt}ruE_CUgYqfdk>kGt#i@9My;v;o z{}@W^aSg*5l=mYbuz(HdllNjlt`Ix102eV^W+EW4qznhxh2@woa~j5T@E!;83Ue_m za}f|o!XdnPe~jS;&v6;8*B*_s=Tn$8rp!|4r3%`j0rj!lT$<(lPhnCPR$wb$;~kbNvvXV?FjLO| zGE8n)W|!!%b~d8gY~v0&CoU+evV~spHrC2qTqtv>dEbRzBK@!?OqSWWplHUHdwAc$ zGA7FWx1gZKmaW@gy)^p#nYY(|IpO|W7~Gy;s9={pZ*UhvsH_57`K!;D)gC^ cE35ErM)+DK;QmFSNJpZCHkK`9%%2694urxT|#4Y5IyY*e-oKj zB_-vdm0L3_NSlH8hVn>rp;+TT1A`M zG@)VQJ+;M;bAc|SJ!xZmf4VTn0D8;CK0rTr|98?SXbJS;HrlkIV8U*QH!L3+Xyoh2 z8=6Z;(5|$(jgbiGhtxjLiOE!IInD`lb)C-)5`}B8uh<-$e&?Yus z0Iv=A6O-HYANT)%dKUTx#?sw3eOf?2=NRaxmG%$}^{J?)?QA@O=Qo>s1CG-U(6=v2 z_uAQ}PYVW^Ll;B4x3=_I=qF!J@789U7-^V~^kO{srM=N1jaaJJd-Qc$Zqud>gM8;@ zAdT)@Xx%&rtvacY23psH@qb5p$J`SjFQo}R?2hlzp?-PNh5=rqmxCNKq!O-8@OO>9 z=0ant6=TpZk_suzWZIJcUH{S}jE6piG~}OX8(W@? zVX$_zH?%!SPapDpJgUvzRf#pKu28}eMiy0(0*H{zZFPaJ>;!+ zhfWZ~u+`?9b(l$e=zJ*{K?9^coS?sW=VXpA^?IFmE*aZwepv@Pp#AG*Dr185Os_(} zj6aW>37r~T^Ufus!seF~pcAkSk|BMOmePfY&oTvB=eDNrdgqcMFJgerE9WrNE_i$+ zo&MW=ukS5ZgR(EHby~Vfj)?rHxx1CVB01mpi}KcD=r_IANg!Xq1ysn@i z=(2tY*bm*F36KYr1^&n0xwSTK-hl3=1zdqHKV_pUUisFn@K{y%2&`ap_4oJHeE{l#8VY1QX~@| Y0&aZ{zBbD<$^ZZW07*qoM6N<$g3~c1u>b%7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification.png b/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..bbb4a71244070666abaf60e654677b89224aa5cb GIT binary patch literal 1908 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 bnMLORR?`Ff$?Wn}00000NkvXXu0mjf!*qez literal 0 HcmV?d00001