On the train-in a quiet library-in a conference room- When I launched the app, I heard a loud sound. Do you have such an experience? Fortunately, I haven't experienced it yet, but I have a habit of checking the volume many times.
I made an app to solve such a problem.
It is open source (MIT license). Repository
This app sets the speaker (STREAM_MUSIC) volume to zero when the earphones are not connected. Set the volume to zero at the following timing.
This app resides in the notification area (foreground service) and always tries to reduce the speaker volume to zero. However, there are times when you want to use a speaker, so we have implemented a function to temporarily cancel the mute process.
When you tap Do Not Speak! In the notification, a dialog for canceling will be displayed.
If only the above is done, it will be a promotional article, so I will write the implementation content.
This is the core of this app, which makes the speaker volume zero. It can be set in the AudioManager class.
AudioManager audioManager = this.getSystemService(this, AudioManager.class);
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0);
Just get the ʻAudioManager and call the
setStreamVolumemethod. On Android, the volume settings are separated for each stream, and each volume can be set independently. Specify
STREAM_MUSIC` to zero only the music in this app.
Also, ʻandroid.permission.MODIFY_AUDIO_SETTINGS` permission seems to be needed, but it wasn't.
This app does not set the volume to zero when the earphones are connected, so you need to check the connection status of the earphones. You can check the connection status with ʻAudioManager, but the judgment code changes depending on the Android version. Android prior to Android 6.0 M uses the ʻisWiredHeadsetOn
, ʻisBluetoothScoOn, and ʻisBluetoothA2dpOn
methods, and Android 6.0 M uses the getDevices
method.
private boolean isHeadsetConnected() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return this.audioManager.isWiredHeadsetOn() || this.audioManager.isBluetoothScoOn() || this.audioManager.isBluetoothA2dpOn();
} else {
AudioDeviceInfo[] devices = this.audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
for (int i = 0; i < devices.length; i++) {
AudioDeviceInfo device = devices[i];
int type = device.getType();
if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET
|| type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
|| type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
|| type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
) {
return true;
}
}
return false;
}
}
Bluetooth A2DP seems to be used in common Bluetooth earphones, but Bluetooth SCO may not be needed.
Even if the volume is set to zero, it is meaningless if the user or other apps change the volume. Therefore, we need a function to set the volume to zero when the volume is changed. On Android, Content Provider and ContentObserver You can detect the change in volume by registering.
public final class DNSContentObserver extends ContentObserver {
private Runnable callback;
public DNSContentObserver(Handler handler, Runnable callback) {
super(handler);
this.callback = callback;
}
@Override
public boolean deliverSelfNotifications() {
return false;
}
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
this.callback.run();
}
}
this.getContentResolver().registerContentObserver(
android.provider.Settings.System.getUriFor("volume_music_speaker"),
true,
new DNSContentObserver(new Handler(), new Runnable() {
@Override
public void run() {
//Mute processing
}
}));
Get the ContentResolver
class with getContentResolver
and register the ContentObserver with the registerContentObserver
method. At this time, you can use a string called the content URI to specify which data to monitor. In this app, we want to detect changes in speaker volume, so we specify content: // settings / system / volume_music_speaker
.
In the above volume change detection process, if the volume is changed continuously in a short time, ContentObserver
ʻonChange may not occur (multiple changes can be combined into one ʻonChange
?). To mitigate this effect, mute again 1 second after ʻonChange` occurs. I created a class to do the debounce process for that.
public final class Debouncer {
private static final String TAG = "Debouncer";
private final Handler handler;
private final int dueTime;
private final Runnable callback;
private final Runnable checkRunner = new Runnable() {
@Override
public void run() {
Debouncer.this.check();
}
};
private final AtomicBoolean locked = new AtomicBoolean(false);
private int elapsedTime;
private long startTime;
public Debouncer(Handler handler, int dueTime, Runnable callback) {
this.handler = handler;
this.dueTime = dueTime;
this.callback = callback;
this.elapsedTime = this.dueTime;
}
public void update() {
for (; ; ) {
// lock
if (this.locked.compareAndSet(false, true)) {
int elapsed = this.elapsedTime;
// reset time
Log.d(TAG, "reset");
this.startTime = System.currentTimeMillis();
this.elapsedTime = 0;
// not running?
if (elapsed >= this.dueTime) {
this.start(this.dueTime);
}
// unlock
this.locked.set(false);
break;
}
}
}
private void start(int delayTime) {
Log.d(TAG, "start");
this.handler.postDelayed(this.checkRunner, delayTime);
}
private void check() {
Log.d(TAG, "check");
// lock
if (this.locked.compareAndSet(false, true)) {
long currentTime = System.currentTimeMillis();
this.elapsedTime += currentTime - this.startTime;
boolean over = this.elapsedTime >= this.dueTime;
// retry
if (!over) {
int remainTime = this.dueTime - this.elapsedTime;
this.startTime = currentTime;
this.start(remainTime);
}
// unlock
this.locked.set(false);
// callback
if (over) this.callback.run();
}
}
}
On Android, ʻandroid.media.AUDIO_BECOMING_NOISY` is broadcast the moment the earphones are pulled out and the sound is output from the speaker.
public final class DNSReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action == null) return;
switch (action) {
case AudioManager.ACTION_AUDIO_BECOMING_NOISY: {
//Mute processing
break;
}
}
}
}
I also write an intent filter in the manifest.
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
By the way, before updating the connection status of the earphone inside Android, this broadcast is called, so you should not check if the earphone is connected at this timing. This app will forcibly mute regardless of the earphone connection status.
The service will be terminated when the device is restarted or the application is updated. Tragedy may occur if nothing is done, so it is necessary to start the app automatically. In such a case, receive the ʻandroid.intent.action.BOOT_COMPLETED broadcast and the ʻandroid.intent.action.MY_PACKAGE_REPLACED
broadcast and start the service.
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
You also need permissions.
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
The code is similar to ʻAUDIO_BECOMING_NOISY`, so it is omitted.
On Android, you cannot launch the service directly from the launcher. You can (probably) only launch activities from the launcher. However, this app doesn't need to show activity, so I adjusted it to not show activity.
Change ʻAppTheme` to:
<resources>
<style name="AppTheme" parent="android:Theme.Material">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>
This will be a transparent theme for activities without a title bar. You can then start the service with ʻonCreate in
MainActivityand immediately call
finishAndRemoveTask` to hide the activity from the appearance.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Start service
this.finishAndRemoveTask();
this.overridePendingTransition(0, 0);
}
Also, it seems that the transition animation can be canceled by calling ʻoverridePendingTransition`.
When I created the app in Android Studio, the APK size exceeded 1MB. For resident apps, it's definitely better to use less memory, so we've reduced the app size in various ways.
First, set minifyEnabled
and shirinkResources
to true
in ProGuard. Just doing this made it about 700KB.
release {
minifyEnabled true
shrinkResources true
...
}
Normally, I don't think it's a problem if it's as small as KB, but I wasn't satisfied and decided to make it smaller. In this app, the dependency library specified by dependencies
inflated the APK.
build.gradle
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.0-beta01'
implementation 'androidx.core:core-ktx:1.1.0-alpha03'
implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.0-alpha4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4'
}
This is Android X, a support library for maintaining backward compatibility with Android, and I think it is almost essential for general apps. However, you can make the APK smaller by throwing them away. (Not recommended.)
I was able to create an APK less than 100KB by removing AndroidX and writing backward compatible code myself. The final size is about 26KB. (By the way, I wrote it in Kotlin at first, but I changed it to Java because the generated bytecode was subtle. Companion object ...)
Recommended Posts