Skip to content

Commit 4e42c97

Browse files
Build 1.4.0 : Added SoundMaster!
1 parent 5e356b1 commit 4e42c97

27 files changed

+1303
-30
lines changed

.idea/deploymentTargetDropDown.xml

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/deploymentTargetSelector.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ Contains some easy-to-use tools to go beyond the level of control allowed by And
44
### Tools -
55
- [x] **Debloater** - Uninstall system apps and bloatware, with app information from [UAD](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation).
66
- [x] **ThemePatcher** - Unlocks premium content for free, from the Oppo/Realme/Oneplus theme store.
7-
- [x] **LookBack** - Allows downgrade of apps, without uninstallation.
87
- [x] **MixedAudio** - Allows multiple media apps to play at the same time, or mute audio of specific apps.
8+
- [x] **SoundMaster** - Independent volume control for every app, and more! Requires Android 10 or later.
9+
⚠ SoundMaster may not work on apps with strong copyright protection, like Spotify. In case SoundMaster crashes and some apps lose sound output, use MixedAudio to unmute them.
10+
- [x] **LookBack** - Allows downgrade of apps, without uninstallation.
911
- [x] **ADB Shell** - Manually execute other raw ADB commands.
1012
- [x] **Intent Shell** - Allows other apps (Tasker,MacroDroid,etc) to run ADB commands via intent requests.
1113

app/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ android {
1212
minSdk 27
1313
targetSdk 34
1414
versionCode 1
15-
versionName "1.3.0"
15+
versionName "1.4.0"
1616

1717
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1818
}
@@ -40,6 +40,7 @@ dependencies {
4040
implementation 'androidx.appcompat:appcompat:1.6.1'
4141
implementation 'com.google.android.material:material:1.11.0'
4242
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
43+
implementation 'androidx.activity:activity:1.9.0'
4344
testImplementation 'junit:junit:4.13.2'
4445
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
4546
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

app/release/app-release.apk

12.6 KB
Binary file not shown.

app/src/main/AndroidManifest.xml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
<uses-permission android:name="android.permission.INTERNET" />
66
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE" />
77
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
8+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
9+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
10+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
11+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
812
<uses-permission
913
android:name="android.permission.QUERY_ALL_PACKAGES"
1014
tools:ignore="QueryAllPackagesPermission" />
@@ -25,17 +29,29 @@
2529
android:supportsRtl="true"
2630
android:theme="@style/Theme.AdbTools"
2731
tools:targetApi="31">
32+
<service
33+
android:name=".services.SoundMasterService"
34+
android:enabled="true"
35+
android:exported="false"
36+
android:foregroundServiceType="mediaProjection" />
37+
2838
<receiver
2939
android:name=".receivers.IntentReceiver"
30-
android:label="Intent Shell"
3140
android:enabled="true"
32-
android:exported="true">
41+
android:exported="true"
42+
android:label="Intent Shell">
3343
<intent-filter>
3444
<action android:name="com.legendsayantan.adbtools.execute" />
35-
<category android:name="android.intent.category.DEFAULT"/>
45+
46+
<category android:name="android.intent.category.DEFAULT" />
3647
</intent-filter>
3748
</receiver>
3849

50+
<activity
51+
android:name=".SoundMasterActivity"
52+
android:exported="false"
53+
android:label=""
54+
android:theme="@style/DialogActivityTheme" />
3955
<activity
4056
android:name=".MixedAudioActivity"
4157
android:exported="false" />

app/src/main/java/com/legendsayantan/adbtools/MainActivity.kt

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package com.legendsayantan.adbtools
22

3+
import android.annotation.SuppressLint
4+
import android.app.PendingIntent
35
import android.content.ClipData
46
import android.content.ClipboardManager
57
import android.content.Intent
8+
import android.content.pm.PackageManager
69
import android.net.Uri
10+
import android.os.Build
711
import androidx.appcompat.app.AppCompatActivity
812
import android.os.Bundle
913
import android.view.Gravity
@@ -14,10 +18,12 @@ import android.widget.LinearLayout
1418
import android.widget.ScrollView
1519
import android.widget.TextView
1620
import android.widget.Toast
21+
import androidx.core.app.ActivityCompat
22+
import androidx.core.app.NotificationCompat
23+
import androidx.core.app.NotificationManagerCompat
1724
import com.google.android.material.button.MaterialButton
1825
import com.google.android.material.card.MaterialCardView
1926
import com.google.android.material.dialog.MaterialAlertDialogBuilder
20-
import com.google.android.material.materialswitch.MaterialSwitch
2127
import com.google.android.material.switchmaterial.SwitchMaterial
2228
import com.google.android.material.textfield.TextInputEditText
2329
import com.google.android.material.textfield.TextInputLayout
@@ -26,6 +32,7 @@ import com.legendsayantan.adbtools.lib.Utils.Companion.initialiseStatusBar
2632
import java.util.UUID
2733

2834
class MainActivity : AppCompatActivity() {
35+
@SuppressLint("LaunchActivityFromNotification")
2936
override fun onCreate(savedInstanceState: Bundle?) {
3037
super.onCreate(savedInstanceState)
3138
setContentView(R.layout.activity_main)
@@ -43,6 +50,7 @@ class MainActivity : AppCompatActivity() {
4350
val cardThemePatcher = findViewById<MaterialCardView>(R.id.cardThemePatcher)
4451
val cardLookBack = findViewById<MaterialCardView>(R.id.cardLookBack)
4552
val cardMixedAudio = findViewById<MaterialCardView>(R.id.cardMixedAudio)
53+
val cardSoundMaster = findViewById<MaterialCardView>(R.id.cardSoundMaster)
4654
val cardShell = findViewById<MaterialCardView>(R.id.cardShell)
4755
val cardIntentShell = findViewById<MaterialCardView>(R.id.cardIntentShell)
4856
cardDebloat.setOnClickListener {
@@ -77,6 +85,37 @@ class MainActivity : AppCompatActivity() {
7785
)
7886
)
7987
}
88+
cardSoundMaster.setOnClickListener {
89+
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q) return@setOnClickListener
90+
//create notification
91+
val intent = Intent(this, SoundMasterActivity::class.java)
92+
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
93+
val channelId = "notifications"
94+
val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId)
95+
.setSmallIcon(R.drawable.outline_info_24)
96+
.setContentTitle("Tap to configure "+applicationContext.getString(R.string.soundmaster))
97+
.setOngoing(true)
98+
.setContentIntent(
99+
PendingIntent.getActivity(
100+
this,
101+
0,
102+
intent,
103+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
104+
)
105+
)
106+
.setPriority(NotificationCompat.PRIORITY_LOW)
107+
108+
// Show the notification.
109+
with(NotificationManagerCompat.from(applicationContext)) {
110+
if (ActivityCompat.checkSelfPermission(
111+
applicationContext,
112+
android.Manifest.permission.POST_NOTIFICATIONS
113+
) == PackageManager.PERMISSION_GRANTED
114+
) {
115+
notify(3, notificationBuilder.build())
116+
}
117+
}
118+
}
80119
cardShell.setOnClickListener { openShell() }
81120
cardIntentShell.setOnClickListener { intentShell() }
82121
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.legendsayantan.adbtools
2+
3+
import android.annotation.SuppressLint
4+
import android.app.Activity
5+
import android.content.Context
6+
import android.content.Intent
7+
import android.media.projection.MediaProjectionManager
8+
import android.os.Bundle
9+
import android.view.Gravity
10+
import android.view.View
11+
import android.view.WindowManager
12+
import android.widget.ImageView
13+
import android.widget.TextView
14+
import android.widget.Toast
15+
import androidx.appcompat.app.AppCompatActivity
16+
import androidx.constraintlayout.widget.ConstraintLayout
17+
import androidx.recyclerview.widget.RecyclerView
18+
import com.google.android.material.card.MaterialCardView
19+
import com.legendsayantan.adbtools.adapters.VolumeBarAdapter
20+
import com.legendsayantan.adbtools.dialog.NewSliderDialog
21+
import com.legendsayantan.adbtools.lib.ShizukuRunner
22+
import com.legendsayantan.adbtools.lib.Utils.Companion.initialiseStatusBar
23+
import com.legendsayantan.adbtools.services.SoundMasterService
24+
import java.io.File
25+
import java.io.FileNotFoundException
26+
import java.util.Timer
27+
import kotlin.concurrent.timerTask
28+
29+
class SoundMasterActivity : AppCompatActivity() {
30+
private lateinit var mediaProjectionManager: MediaProjectionManager
31+
val preferences by lazy {
32+
applicationContext.getSharedPreferences(
33+
"volumeplus",
34+
Context.MODE_PRIVATE
35+
)
36+
}
37+
var packages: MutableList<String>
38+
get() = try {
39+
File(applicationContext.filesDir, "soundmaster.txt").readText().split("\n")
40+
.toMutableList()
41+
} catch (f: FileNotFoundException) {
42+
mutableListOf()
43+
}
44+
set(value) {
45+
val file = File(applicationContext.filesDir, "soundmaster.txt")
46+
if (!file.exists()) {
47+
file.parentFile?.mkdirs()
48+
file.createNewFile()
49+
}
50+
file.writeText(value.joinToString("\n"))
51+
}
52+
53+
val volumeBarView by lazy { findViewById<RecyclerView>(R.id.volumeBars) }
54+
55+
@SuppressLint("ApplySharedPref")
56+
override fun onCreate(savedInstanceState: Bundle?) {
57+
super.onCreate(savedInstanceState)
58+
setContentView(R.layout.activity_sound_master)
59+
initialiseStatusBar()
60+
//new slider
61+
findViewById<MaterialCardView>(R.id.newSlider).setOnClickListener {
62+
NewSliderDialog(this@SoundMasterActivity) { pkg ->
63+
val newPackages = packages
64+
newPackages.add(pkg)
65+
packages = newPackages
66+
if (SoundMasterService.running) SoundMasterService.onDynamicAttach(pkg)
67+
updateSliders()
68+
}.show()
69+
}
70+
71+
//outside touch
72+
findViewById<ConstraintLayout>(R.id.main).setOnClickListener {
73+
finish()
74+
}
75+
}
76+
77+
override fun onResume() {
78+
updateBtnState()
79+
updateSliders()
80+
super.onResume()
81+
}
82+
83+
fun updateBtnState() {
84+
val btnImage = findViewById<ImageView>(R.id.playPauseButton)
85+
btnImage.setImageResource(if (SoundMasterService.running) R.drawable.baseline_stop_24 else R.drawable.baseline_play_arrow_24)
86+
btnImage.setOnClickListener {
87+
val state = SoundMasterService.running
88+
if (state) {
89+
stopService(Intent(this, SoundMasterService::class.java))
90+
} else if (packages.size > 0) {
91+
if (packages.isEmpty()) {
92+
Toast.makeText(
93+
applicationContext,
94+
"No apps selected to control",
95+
Toast.LENGTH_SHORT
96+
)
97+
.show()
98+
} else {
99+
ShizukuRunner.runAdbCommand("pm grant ${baseContext.packageName} android.permission.RECORD_AUDIO",
100+
object : ShizukuRunner.CommandResultListener {
101+
override fun onCommandResult(output: String, done: Boolean) {
102+
if (done) {
103+
ShizukuRunner.runAdbCommand("appops set ${baseContext.packageName} PROJECT_MEDIA allow",
104+
object : ShizukuRunner.CommandResultListener {
105+
override fun onCommandResult(
106+
output: String,
107+
done: Boolean
108+
) {
109+
if (done) {
110+
mediaProjectionManager =
111+
applicationContext.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
112+
startActivityForResult(
113+
mediaProjectionManager.createScreenCaptureIntent(),
114+
MEDIA_PROJECTION_REQUEST_CODE
115+
)
116+
}
117+
}
118+
})
119+
}
120+
}
121+
})
122+
}
123+
}
124+
var count = 0
125+
Timer().schedule(timerTask {
126+
if (SoundMasterService.running != state) {
127+
updateBtnState()
128+
updateSliders()
129+
cancel()
130+
} else count++
131+
if (count > 50) cancel()
132+
}, 500, 500)
133+
}
134+
}
135+
136+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
137+
super.onActivityResult(requestCode, resultCode, data)
138+
if (requestCode == MEDIA_PROJECTION_REQUEST_CODE) {
139+
if (resultCode == Activity.RESULT_OK) {
140+
Toast.makeText(
141+
applicationContext,
142+
"Controlling audio from selected apps",
143+
Toast.LENGTH_SHORT
144+
).show()
145+
SoundMasterService.projectionData = data
146+
startService(Intent(this, SoundMasterService::class.java).apply {
147+
putExtra("packages", packages.toTypedArray())
148+
})
149+
} else {
150+
Toast.makeText(
151+
this, "Request to obtain MediaProjection denied.",
152+
Toast.LENGTH_SHORT
153+
).show()
154+
}
155+
}
156+
}
157+
158+
private fun updateSliders() {
159+
findViewById<TextView>(R.id.none).visibility =
160+
if (packages.size > 0) View.GONE else View.VISIBLE
161+
Thread {
162+
val sliderMap = HashMap<String, Float>()
163+
for (pkg in packages) {
164+
val volume = SoundMasterService.getVolumeOf(pkg)
165+
sliderMap[pkg] = volume
166+
}
167+
val adapter =
168+
VolumeBarAdapter(this@SoundMasterActivity, sliderMap, { app, vol ->
169+
SoundMasterService.setVolumeOf(app, vol)
170+
}, {
171+
val newPackages = packages
172+
newPackages.remove(it)
173+
packages = newPackages
174+
updateSliders()
175+
SoundMasterService.onDynamicDetach(it)
176+
}, { app, sliderIndex ->
177+
if (sliderIndex == 0) SoundMasterService.getBalanceOf(app)
178+
else SoundMasterService.getBandValueOf(app, sliderIndex - 1)
179+
}, { app, slider, value ->
180+
if (slider == 0) SoundMasterService.setBalanceOf(app, value)
181+
else SoundMasterService.setBandValueOf(app, slider - 1, value)
182+
})
183+
runOnUiThread {
184+
volumeBarView.adapter = adapter
185+
volumeBarView.invalidate()
186+
}
187+
}.start()
188+
}
189+
190+
companion object {
191+
private const val MEDIA_PROJECTION_REQUEST_CODE = 13
192+
}
193+
}

0 commit comments

Comments
 (0)