[JAVA] Create an Android app for those who don't want to play music on the speaker

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.

DoNotSpeak: An app that doesn't play music on your speakers

Download from the link below Get it on Google Play

It is open source (MIT license). Repository

What to do

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.

Temporary release function

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.

Technical details

If only the above is done, it will be a promotional article, so I will write the implementation content.

Volume setting

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. SpecifySTREAM_MUSIC` to zero only the music in this app.

Also, ʻandroid.permission.MODIFY_AUDIO_SETTINGS` permission seems to be needed, but it wasn't.

Earphone connection status

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.

Volume change detection

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.

Debounce processing

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();
        }
    }
}

Detects that the earphone has been pulled out

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.

reboot

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.

Launch the service from the launcher

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 callfinishAndRemoveTask` 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`.

Minimize app size

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

Create an Android app for those who don't want to play music on the speaker
I want to create a chat screen for the Swift chat app!
The story of releasing the Android app to the Play Store for the first time.
I want to play a GIF image on the Andorid app (Java, Kotlin)
[For those who create portfolios] How to use font-awesome-rails
I want to simplify the log output on Android
For those who want to use MySQL for the database in the environment construction of Rails6 ~.
VS Code FAQ for those who want to escape Eclipse
[For those who create portfolios] How to use chart kick
[For those who create portfolios] How to omit character strings
[For those who want to be an inexperienced engineer in humanities] Would you like to face the escaped mathematics through Project_Euler?
[For those who create portfolios] Reduce mistakes and make the code easier to read -Rubocop Airbnb-
A must-see for those who don't understand the second Heroku deployment!
[For those who create portfolios] How to use binding.pry with Docker
Creating an app and deploying it for the first time on heroku
I want to make an ios.android app
[Android] I want to create a ViewPager that can be used for tutorials
Reference memo for those who take the Ruby Silver exam (taken on December 28, 2020)
[For those who want to do their best in iOS from now on] Summary of the moment when I was surprised after developing an iOS application for two and a half years
An introduction to Java that conveys the C language to those who have been doing it for a long time
I made an Android app for MiRm service
Ssh login to the app server on heroku
Tips for using the Spotify app on Ubuntu
Create your own Android app for Java learning
Create an app by specifying the Rails version
How to make an crazy Android music player
Rails The concept of view componentization of Rails that I want to convey to those who want to quit
[Android Studio] I want to set restrictions on the values registered in EditText [Java]
Create an app that uses the weather API to determine if you need an umbrella.