AudioPlayer: disable screen and switch to earpiece when holding phone to ear

Daniel Gultsch created

Based of initial worked by @harshitbansal05

Change summary

src/main/java/eu/siacs/conversations/services/MediaPlayer.java      |  16 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java   |   1 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java |   4 
src/main/java/eu/siacs/conversations/ui/service/AudioPlayer.java    | 136 
4 files changed, 149 insertions(+), 8 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/services/MediaPlayer.java 🔗

@@ -0,0 +1,16 @@
+package eu.siacs.conversations.services;
+
+public class MediaPlayer extends android.media.MediaPlayer {
+
+    private int streamType;
+
+    @Override
+    public void setAudioStreamType(int streamType) {
+        this.streamType = streamType;
+        super.setAudioStreamType(streamType);
+    }
+
+    public int getAudioStreamType() {
+        return streamType;
+    }
+}

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -1771,6 +1771,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 	public void onStop() {
 		super.onStop();
 		final Activity activity = getActivity();
+		messageListAdapter.unregisterListenerInAudioPlayer();
 		if (activity == null || !activity.isChangingConfigurations()) {
 			hideSoftKeyboard(activity);
 			messageListAdapter.stopAudioPlayer();

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -877,6 +877,10 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 		audioPlayer.stop();
 	}
 
+	public void unregisterListenerInAudioPlayer() {
+		audioPlayer.unregisterListener();
+	}
+
 	public void startStopPending() {
 		audioPlayer.startStopPending();
 	}

src/main/java/eu/siacs/conversations/ui/service/AudioPlayer.java 🔗

@@ -1,13 +1,20 @@
 package eu.siacs.conversations.ui.service;
 
 import android.Manifest;
+import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
-import android.media.MediaPlayer;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.media.AudioManager;
 import android.os.Build;
 import android.os.Handler;
+import android.os.PowerManager;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.content.ContextCompat;
+import android.util.Log;
 import android.view.View;
 import android.widget.ImageButton;
 import android.widget.RelativeLayout;
@@ -17,14 +24,16 @@ import android.widget.TextView;
 import java.lang.ref.WeakReference;
 import java.util.Locale;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.MediaPlayer;
 import eu.siacs.conversations.ui.ConversationsActivity;
 import eu.siacs.conversations.ui.adapter.MessageAdapter;
 import eu.siacs.conversations.ui.util.PendingItem;
 import eu.siacs.conversations.utils.WeakReferenceSet;
 
-public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable {
+public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable, SensorEventListener {
 
 	private static final int REFRESH_INTERVAL = 250;
 	private static final Object LOCK = new Object();
@@ -32,16 +41,36 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 	private static Message currentlyPlayingMessage = null;
 	private final MessageAdapter messageAdapter;
 	private final WeakReferenceSet<RelativeLayout> audioPlayerLayouts = new WeakReferenceSet<>();
+	private final SensorManager sensorManager;
+	private final Sensor proximitySensor;
+	private static PowerManager.WakeLock wakeLock;
 
 	private final PendingItem<WeakReference<ImageButton>> pendingOnClickView = new PendingItem<>();
 
 	private final Handler handler = new Handler();
 
 	public AudioPlayer(MessageAdapter adapter) {
+		final Context context = adapter.getContext();
 		this.messageAdapter = adapter;
+		this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+		this.proximitySensor = this.sensorManager == null ? null : this.sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+		if (Build.VERSION.SDK_INT >= 21) {
+			synchronized (AudioPlayer.LOCK) {
+				if (AudioPlayer.wakeLock == null) {
+					final PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+					AudioPlayer.wakeLock = powerManager == null ? null : powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, AudioPlayer.class.getSimpleName());
+					AudioPlayer.wakeLock.setReferenceCounted(false);
+				}
+			}
+		} else {
+			AudioPlayer.wakeLock = null;
+		}
 		synchronized (AudioPlayer.LOCK) {
 			if (AudioPlayer.player != null) {
 				AudioPlayer.player.setOnCompletionListener(this);
+				if (AudioPlayer.player.isPlaying() && sensorManager != null) {
+					sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
+				}
 			}
 		}
 	}
@@ -125,32 +154,45 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 			viewHolder.progress.setEnabled(false);
 			player.pause();
 			messageAdapter.flagScreenOff();
+			releaseProximityWakeLock();
 			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
 		} else {
 			viewHolder.progress.setEnabled(true);
 			player.start();
 			messageAdapter.flagScreenOn();
+			acquireProximityWakeLock();
 			this.stopRefresher(true);
 			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
 		}
 		return false;
 	}
 
-	private boolean play(ViewHolder viewHolder, Message message) {
+	private void play(ViewHolder viewHolder, Message message, boolean earpiece, double progress) {
+		if (play(viewHolder, message, earpiece)) {
+			AudioPlayer.player.seekTo((int) (AudioPlayer.player.getDuration() * progress));
+		}
+	}
+
+	private boolean play(ViewHolder viewHolder, Message message, boolean earpiece) {
 		AudioPlayer.player = new MediaPlayer();
 		try {
 			AudioPlayer.currentlyPlayingMessage = message;
+			AudioPlayer.player.setAudioStreamType(earpiece ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC);
 			AudioPlayer.player.setDataSource(messageAdapter.getFileBackend().getFile(message).getAbsolutePath());
 			AudioPlayer.player.setOnCompletionListener(this);
 			AudioPlayer.player.prepare();
 			AudioPlayer.player.start();
 			messageAdapter.flagScreenOn();
+			acquireProximityWakeLock();
 			viewHolder.progress.setEnabled(true);
 			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
+			sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
 			return true;
 		} catch (Exception e) {
 			messageAdapter.flagScreenOff();
+			releaseProximityWakeLock();
 			AudioPlayer.currentlyPlayingMessage = null;
+			sensorManager.unregisterListener(this);
 			return false;
 		}
 	}
@@ -172,7 +214,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 		if (AudioPlayer.player != null) {
 			stopCurrent();
 		}
-		return play(viewHolder, message);
+		return play(viewHolder, message, false);
 	}
 
 	private void stopCurrent() {
@@ -181,6 +223,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 		}
 		AudioPlayer.player.release();
 		messageAdapter.flagScreenOff();
+		releaseProximityWakeLock();
 		AudioPlayer.player = null;
 		resetPlayerUi();
 	}
@@ -206,7 +249,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 	}
 
 	@Override
-	public void onCompletion(MediaPlayer mediaPlayer) {
+	public void onCompletion(android.media.MediaPlayer mediaPlayer) {
 		synchronized (AudioPlayer.LOCK) {
 			this.stopRefresher(false);
 			if (AudioPlayer.player == mediaPlayer) {
@@ -215,7 +258,9 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 			}
 			mediaPlayer.release();
 			messageAdapter.flagScreenOff();
+			releaseProximityWakeLock();
 			resetPlayerUi();
+			sensorManager.unregisterListener(this);
 		}
 	}
 
@@ -250,6 +295,11 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 				stopCurrent();
 			}
 			AudioPlayer.currentlyPlayingMessage = null;
+			sensorManager.unregisterListener(this);
+			if (wakeLock != null && wakeLock.isHeld()) {
+				wakeLock.release();
+			}
+			wakeLock = null;
 		}
 	}
 
@@ -260,6 +310,12 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 		}
 	}
 
+	public void unregisterListener() {
+		if (sensorManager != null) {
+			sensorManager.unregisterListener(this);
+		}
+	}
+
 	@Override
 	public void run() {
 		synchronized (AudioPlayer.LOCK) {
@@ -287,6 +343,70 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 		return true;
 	}
 
+	@Override
+	public void onSensorChanged(SensorEvent event) {
+		if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) {
+			return;
+		}
+		if (AudioPlayer.player == null || !AudioPlayer.player.isPlaying()) {
+			return;
+		}
+		int streamType;
+		if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) {
+			streamType = AudioManager.STREAM_VOICE_CALL;
+		} else {
+			streamType = AudioManager.STREAM_MUSIC;
+		}
+		double position = AudioPlayer.player.getCurrentPosition();
+		double duration = AudioPlayer.player.getDuration();
+		double progress = position / duration;
+		if (AudioPlayer.player.getAudioStreamType() != streamType) {
+			synchronized (AudioPlayer.LOCK) {
+				AudioPlayer.player.stop();
+				AudioPlayer.player.release();
+				AudioPlayer.player = null;
+				try {
+					ViewHolder currentViewHolder = getCurrentViewHolder();
+					if (currentViewHolder != null) {
+						play(currentViewHolder, currentlyPlayingMessage, streamType == AudioManager.STREAM_VOICE_CALL, progress);
+					}
+				} catch (Exception e) {
+					Log.w(Config.LOGTAG, e);
+				}
+			}
+		}
+	}
+
+	@Override
+	public void onAccuracyChanged(Sensor sensor, int i) {
+	}
+
+	private void acquireProximityWakeLock() {
+		synchronized (AudioPlayer.LOCK) {
+			if (wakeLock != null) {
+				wakeLock.acquire();
+			}
+		}
+	}
+
+	private void releaseProximityWakeLock() {
+		synchronized (AudioPlayer.LOCK) {
+			if (wakeLock != null && wakeLock.isHeld()) {
+				wakeLock.release();
+			}
+		}
+	}
+
+	private ViewHolder getCurrentViewHolder() {
+		for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
+			final Message message = (Message) audioPlayer.get().getTag();
+			if (message == currentlyPlayingMessage) {
+				return ViewHolder.get(audioPlayer.get());
+			}
+		}
+		return null;
+	}
+
 	public static class ViewHolder {
 		private TextView runtime;
 		private SeekBar progress;
@@ -297,9 +417,9 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
 			ViewHolder viewHolder = (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER);
 			if (viewHolder == null) {
 				viewHolder = new ViewHolder();
-				viewHolder.runtime = (TextView) audioPlayer.findViewById(R.id.runtime);
-				viewHolder.progress = (SeekBar) audioPlayer.findViewById(R.id.progress);
-				viewHolder.playPause = (ImageButton) audioPlayer.findViewById(R.id.play_pause);
+				viewHolder.runtime = audioPlayer.findViewById(R.id.runtime);
+				viewHolder.progress = audioPlayer.findViewById(R.id.progress);
+				viewHolder.playPause = audioPlayer.findViewById(R.id.play_pause);
 				audioPlayer.setTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER, viewHolder);
 			}
 			return viewHolder;