Skip to content

Instantly share code, notes, and snippets.

@mohsenk
Created December 21, 2019 09:42
Show Gist options
  • Save mohsenk/ced7528bc5b0ec81d17269deb8364a2b to your computer and use it in GitHub Desktop.
Save mohsenk/ced7528bc5b0ec81d17269deb8364a2b to your computer and use it in GitHub Desktop.
Homa App Call Activity
package mobi.homa.app.activities;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.gson.JsonParser;
import com.squareup.picasso.Picasso;
import org.apache.commons.lang3.StringUtils;
import org.threeten.bp.LocalTime;
import org.threeten.bp.format.DateTimeFormatter;
import org.webrtc.PeerConnection;
import java.text.SimpleDateFormat;
import java.util.Timer;
import java.util.TimerTask;
import io.kavenegar.sdk.call.enums.Environment;
import io.kavenegar.sdk.call.log.DefaultLogger;
import io.kavenegar.sdk.call.log.RemoteLogger;
import mobi.homa.app.MyApplication;
import mobi.homa.app.R;
import mobi.homa.app.audio.HomaAudioManager;
import mobi.homa.app.utils.CircleTransform;
import mobi.homa.app.utils.TimeUtils;
import io.kavenegar.sdk.call.Call;
import io.kavenegar.sdk.call.CallEventListener;
import io.kavenegar.sdk.call.KavenegarCall;
import io.kavenegar.sdk.call.core.JoinCallback;
import io.kavenegar.sdk.call.core.KavenegarException;
import io.kavenegar.sdk.call.enums.CallDirection;
import io.kavenegar.sdk.call.enums.CallFinishedReason;
import io.kavenegar.sdk.call.enums.CallStatus;
import io.kavenegar.sdk.call.enums.JoinStatus;
import io.kavenegar.sdk.call.enums.MediaState;
import io.kavenegar.sdk.call.enums.MessagingState;
import io.kavenegar.sdk.call.log.Logger;
import io.kavenegar.sdk.call.messaging.MediaStateChangedEvent;
import io.kavenegar.sdk.call.messaging.MessagingStateChangedEvent;
import io.kavenegar.sdk.call.webrtc.models.LocalMediaStateChangedEvent;
/**
* Created by mohsen on 2/27/2018 AD.
*/
public class CallActivity extends Activity implements CallEventListener, JoinCallback {
static final String TAG = "CallActivity";
TextView statusText;
TextView usernameText;
ImageButton acceptButton;
ImageButton rejectButton;
ImageButton hangupButton;
ImageButton speakerButton;
ImageButton muteButton;
ImageView avatarImage;
HomaAudioManager audioManager;
Call nativeCall;
mobi.homa.app.models.Call call;
Logger logger;
Timer timer = new Timer();
CallFinishedReason endType;
TextView mediaStatusText;
TextView remoteMediaStatusText;
TextView messagingStatusText;
@Override
protected void onCreate(Bundle savedInstanceState) {
try {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
setContentView(R.layout.activity_call);
hangupButton = findViewById(R.id.hangup_button);
statusText = findViewById(R.id.status_text);
usernameText = findViewById(R.id.username_text);
acceptButton = findViewById(R.id.accept_button);
rejectButton = findViewById(R.id.reject_button);
speakerButton = findViewById(R.id.speaker_button);
muteButton = findViewById(R.id.mute_button);
avatarImage = findViewById(R.id.avatar_image);
mediaStatusText = findViewById(R.id.media_status_text);
messagingStatusText = findViewById(R.id.messaging_status_text);
remoteMediaStatusText = findViewById(R.id.remote_media_status_text);
acceptButton.setOnClickListener(view -> acceptClick());
rejectButton.setOnClickListener(view -> rejectClick());
hangupButton.setOnClickListener(view -> hangupClick());
speakerButton.setEnabled(false);
muteButton.setEnabled(false);
speakerButton.setOnClickListener(view -> speakerClick());
muteButton.setOnClickListener(view -> muteClick());
audioManager = new HomaAudioManager(this, R.raw.connected, R.raw.disconnected);
this.setInitUI();
if (!hasAudioPermission()) {
requestAudioPermission();
} else {
this.start();
}
if (!hasReadPhoneStatePermission()) {
requestReadPhoneStatePermission();
}
if (KavenegarCall.getInstance().getEnvironment() == Environment.PRODUCTION) {
// mediaStatusText.setVisibility(View.GONE);
// messagingStatusText.setVisibility(View.GONE);
// remoteMediaStatusText.setVisibility(View.GONE);
}
timer.schedule(new TimerTask() {
@Override
public void run() {
if (nativeCall != null && nativeCall.getStatus() == CallStatus.CONVERSATION) {
CallActivity.this.updateDuration();
}
}
}, 0, 1000);
} catch (Exception ex) {
ex.printStackTrace();
}
}
void updateDuration() {
runOnUiThread(() -> {
if (nativeCall.getStatus() == CallStatus.CONVERSATION && nativeCall.getCallerMediaState() == MediaState.CONNECTED && nativeCall.getReceptorMediaState() == MediaState.CONNECTED) {
LocalTime t = LocalTime.MIDNIGHT.plusSeconds(this.nativeCall.getDuration() / 1000);
String formatted = DateTimeFormatter.ofPattern("m:ss").format(t);
statusText.setText(formatted);
}
});
}
public void start() {
try {
String accessToken = getIntent().getStringExtra("access_token");
String callId = getIntent().getStringExtra("call_id");
String callPayload = getIntent().getStringExtra("call");
MyApplication.setCurrentCallId(callId);
Logger logger;
if (KavenegarCall.getInstance().getEnvironment() == Environment.PRODUCTION) {
logger = new DefaultLogger();
} else {
//logger = new RemoteLogger(Log.INFO, callId, accessToken, Environment.DEVELOPMENT);
logger = new DefaultLogger();
}
this.logger = logger;
KavenegarCall.getInstance().initCall(callId, accessToken, logger, this, this);
this.call = new mobi.homa.app.models.Call(new JsonParser().parse(callPayload).getAsJsonObject());
} catch (Exception ex) {
logger.error(TAG, "Messaging start has exception ", ex);
}
}
@Override
public void onResult(JoinStatus status, Call call) {
runOnUiThread(() -> {
try {
if (status == JoinStatus.SUCCESS) {
try {
this.nativeCall = call;
call.setMessagingStateChangedHandler(this::messagingSateChanged);
call.setMediaStateChangedEventHandler(this::peerConnectionStateChanged);
audioManager.initializeAudioForCall();
if (nativeCall.getDirection() == CallDirection.OUTBOUND) {
logger.info(TAG, "Call to " + nativeCall.getReceptor() + ", id : " + nativeCall.getId() + "===============================================================");
initOutgoingCall();
} else {
logger.info(TAG, "Receive call from : " + nativeCall.getCaller() + " , id : " + nativeCall.getId() + " ===============================================================");
initIncomingCall();
}
} catch (Exception ex) {
logger.error(TAG, "Messaging start callback", ex);
Toast.makeText(CallActivity.this, "Messaging start has error : " + ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
this.endType = CallFinishedReason.UNKNOWN;
this.finish();
}
} else {
Toast.makeText(CallActivity.this, "Messaging start result : " + status, Toast.LENGTH_LONG).show();
logger.warn(TAG, "Messaging start result is not success : " + status.toString());
this.endType = CallFinishedReason.UNKNOWN;
this.finish();
}
} catch (Exception ex) {
logger.error(TAG, "Messaging start has exception ", ex);
}
});
}
private void initIncomingCall() throws KavenegarException {
try {
statusText.setText("Incoming");
this.nativeCall.ringing();
usernameText.setText(call.getCaller().getFullName());
loadAvatar(call.getCaller().getAvatarURL());
Uri ringtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
audioManager.startIncomingRinger(ringtone, false);
} catch (Exception ex) {
logger.error(TAG, "", ex);
}
setAnswerUI();
}
private void initOutgoingCall() {
try {
statusText.setText("Calling");
usernameText.setText(call.getReceptor().getFullName());
loadAvatar(call.getReceptor().getAvatarURL());
setCallUI();
Uri dataUri = Uri.parse("android.resource://" + this.getPackageName() + "/" + R.raw.outring);
audioManager.startOutgoingRinger(dataUri, false);
} catch (Exception ex) {
logger.error(TAG, "", ex);
}
}
void loadAvatar(String avatarURL) {
Picasso.get().load(avatarURL)
.placeholder(R.drawable.ic_user)
.error(R.drawable.ic_user)
.transform(new CircleTransform()).into(avatarImage);
}
// Kavenegar Call State Change Events ====================================================================
private void peerConnectionStateChanged(LocalMediaStateChangedEvent event) {
this.runOnUiThread(() -> {
PeerConnection.IceConnectionState old = event.getOldState();
PeerConnection.IceConnectionState current = event.getNewState();
if ((old == PeerConnection.IceConnectionState.COMPLETED || old == PeerConnection.IceConnectionState.CONNECTED) && current == PeerConnection.IceConnectionState.DISCONNECTED) {
nativeCall.setStatus(CallStatus.PAUSED, true);
}
mediaStatusText.setText("Media: " + old.name().toLowerCase() + " => " + current.name().toLowerCase());
});
}
private void messagingSateChanged(MessagingStateChangedEvent event) {
this.runOnUiThread(() -> {
MessagingState old = event.getOldState();
MessagingState current = event.getNewState();
messagingStatusText.setText("Messaging: " + old.name().toLowerCase() + " => " + current.name().toLowerCase());
});
}
// UI Methods ============================================================================================
public void setInitUI() {
acceptButton.setVisibility(View.GONE);
rejectButton.setVisibility(View.GONE);
hangupButton.setVisibility(View.GONE);
speakerButton.setVisibility(View.GONE);
muteButton.setVisibility(View.GONE);
}
public void setCallUI() {
speakerButton.setEnabled(true);
muteButton.setEnabled(true);
acceptButton.setVisibility(View.GONE);
rejectButton.setVisibility(View.GONE);
hangupButton.setVisibility(View.VISIBLE);
speakerButton.setVisibility(View.VISIBLE);
muteButton.setVisibility(View.VISIBLE);
}
public void setAnswerUI() {
speakerButton.setEnabled(true);
muteButton.setEnabled(true);
hangupButton.setVisibility(View.GONE);
acceptButton.setVisibility(View.VISIBLE);
rejectButton.setVisibility(View.VISIBLE);
speakerButton.setVisibility(View.VISIBLE);
muteButton.setVisibility(View.VISIBLE);
}
private void setCommunicationUI() {
speakerButton.setEnabled(true);
muteButton.setEnabled(true);
acceptButton.setVisibility(View.GONE);
rejectButton.setVisibility(View.GONE);
hangupButton.setVisibility(View.VISIBLE);
}
// UI Events =================================================================================================
public void acceptClick() {
try {
this.nativeCall.accept();
setCommunicationUI();
} catch (Exception ex) {
logger.error(TAG, "", ex);
}
}
public void rejectClick() {
try {
this.nativeCall.reject();
} catch (Exception ex) {
logger.error(TAG, "", ex);
}
}
public void hangupClick() {
try {
this.nativeCall.hangup();
} catch (Exception ex) {
logger.error(TAG, "", ex);
}
}
public void muteClick() {
Uri dataUri = Uri.parse("android.resource://" + this.getPackageName() + "/" + R.raw.reconnecting);
if (audioManager.isMicrophoneMute()) {
audioManager.setMicrophoneMute(false);
muteButton.setBackground(ContextCompat.getDrawable(this, R.drawable.circle_button_with_border));
} else {
audioManager.setMicrophoneMute(true);
muteButton.setBackground(ContextCompat.getDrawable(this, R.drawable.gray_button_background));
}
}
public void speakerClick() {
if (audioManager.isSpeakerOn()) {
audioManager.setSpeakerOn(false);
speakerButton.setBackground(ContextCompat.getDrawable(this, R.drawable.circle_button_with_border));
} else {
audioManager.setSpeakerOn(true);
speakerButton.setBackground(ContextCompat.getDrawable(this, R.drawable.gray_button_background));
}
}
// ============================================================================================================== //
@Override
public void finish() {
if (this.nativeCall != null) {
try {
this.nativeCall.dispose();
audioManager.stop(false);
} catch (Exception ex) {
logger.error(KavenegarCall.TAG, "Exception in dispose call", ex);
}
logger.info(TAG, "Finish call with id : " + nativeCall.getId());
Intent returnIntent = new Intent();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
returnIntent.putExtra("callerId", call.getCallerId());
returnIntent.putExtra("receptorId", call.getReceptorId());
returnIntent.putExtra("createdAt", TimeUtils.toISOFormat(call.getCreatedAt()));
returnIntent.putExtra("startedAt", nativeCall.getStartedAt() != null ? dateFormat.format(nativeCall.getStartedAt()) : "");
returnIntent.putExtra("finishedAt", nativeCall.getFinishedAt() != null ? dateFormat.format(nativeCall.getFinishedAt()) : "");
returnIntent.putExtra("endType", endType);
returnIntent.putExtra("remoteId", nativeCall.getId());
returnIntent.putExtra("callId", call.getId());
returnIntent.putExtra("duration", nativeCall.getDuration());
setResult(200, returnIntent);
MyApplication.setCurrentCallId(null);
super.finish();
} else {
super.finish();
}
}
// Call events =============================================================================================== //
@Override
public void onMediaStateChanged(MediaStateChangedEvent event) {
runOnUiThread(() -> {
remoteMediaStatusText.setText("Caller Media =" + nativeCall.getCallerMediaState().toString() + " , Receptor Media =" + nativeCall.getReceptorMediaState().toString());
// //Toast.makeText(this, event.toString(), Toast.LENGTH_LONG).show();
// if (nativeCall.getStatus() == CallStatus.CONVERSATION && nativeCall.getCallerMediaState() == MediaState.CONNECTED && nativeCall.getReceptorMediaState() == MediaState.CONNECTED) {
// }
});
}
@Override
public void onCallFinished(CallFinishedReason reason) {
logger.info(TAG, "Call finished with reason :" + reason.name());
this.endType = reason;
this.finish();
}
@Override
public void onCallStateChanged(CallStatus state, boolean isLocalChange) {
runOnUiThread(() -> {
setCallStatus(state);
switch (state) {
case NEW: {
break;
}
case TRYING: {
break;
}
case RINGING: {
break;
}
case ACCEPTED: {
audioManager.stop(false);
break;
}
case CONVERSATION: {
setCommunicationUI();
audioManager.startCommunication(false);
break;
}
case PAUSED: {
Uri dataUri = Uri.parse("android.resource://" + this.getPackageName() + "/" + R.raw.reconnecting);
audioManager.startBeepRinger(dataUri, false);
}
case FINISHED: {
break;
}
}
});
}
void setCallStatus(CallStatus status) {
String text = "";
switch (status) {
case TRYING: {
break;
}
case RINGING: {
text = "Ringing";
break;
}
case ACCEPTED: {
text = "Connecting";
break;
}
case CONVERSATION: {
text = "Connecting";
break;
}
case PAUSED: {
text = "Reconnecting";
break;
}
case FINISHED: {
text = "Finished";
break;
}
}
statusText.setText(StringUtils.capitalize(text));
}
// Permissions =============================================================================================== //
boolean hasAudioPermission() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
}
boolean hasReadPhoneStatePermission() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED;
}
void requestReadPhoneStatePermission() {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_PHONE_STATE}, 0);
}
void requestAudioPermission() {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, 0);
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case 0: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
this.start();
} else {
Toast.makeText(this, "Failed to access the microphone.\nclick allow when asked for permission.", Toast.LENGTH_SHORT).show();
}
break;
}
}
}
}
package mobi.homa.app.audio;
import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
public class HomaAudioManager implements AudioManager.OnAudioFocusChangeListener {
private static final String TAG = "AudioManager";
private final Context context;
private final IncomingRinger incomingRinger;
private final OutgoingRinger outgoingRinger;
private final OutgoingRinger beepRinger;
private final SoundPool soundPool;
private final int connectedSoundId;
private final int disconnectedSoundId;
public HomaAudioManager(Context context, Integer connectedSound, Integer disconnectedSound) {
this.context = context.getApplicationContext();
this.incomingRinger = new IncomingRinger(context);
this.outgoingRinger = new OutgoingRinger(context);
this.beepRinger = new OutgoingRinger(context);
this.soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
this.connectedSoundId = this.soundPool.load(context, connectedSound, 1);
this.disconnectedSoundId = this.soundPool.load(context, disconnectedSound, 1);
}
public static AudioManager getAudioManager(Context context) {
return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
public void initializeAudioForCall() {
AudioManager audioManager = getAudioManager(context);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE);
} else {
audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN);
}
}
public void startIncomingRinger(Uri ringtoneUri, boolean vibrate) {
AudioManager audioManager = getAudioManager(context);
boolean speaker = !audioManager.isWiredHeadsetOn() && !audioManager.isBluetoothScoOn();
audioManager.setMode(AudioManager.MODE_RINGTONE);
audioManager.setMicrophoneMute(false);
audioManager.setSpeakerphoneOn(speaker);
incomingRinger.start(ringtoneUri, vibrate);
}
public void startOutgoingRinger(Uri type, boolean speakerOff) {
AudioManager audioManager = getAudioManager(context);
audioManager.setMicrophoneMute(false);
if (speakerOff) {
audioManager.setSpeakerphoneOn(false);
}
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
outgoingRinger.start(type);
}
public void startBeepRinger(Uri type, boolean speakerOff) {
outgoingRinger.stop();
incomingRinger.stop();
AudioManager audioManager = getAudioManager(context);
audioManager.setMicrophoneMute(false);
if (speakerOff) {
audioManager.setSpeakerphoneOn(false);
}
audioManager.setMode(AudioManager.MODE_RINGTONE);
beepRinger.start(type);
}
public void silenceIncomingRinger() {
incomingRinger.stop();
}
public void startCommunication(boolean preserveSpeakerphone) {
AudioManager audioManager = getAudioManager(context);
incomingRinger.stop();
outgoingRinger.stop();
beepRinger.stop();
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
if (!preserveSpeakerphone) {
audioManager.setSpeakerphoneOn(false);
}
soundPool.play(connectedSoundId, 1.0f, 1.0f, 0, 0, 1.0f);
}
public void stop(boolean playDisconnected) {
AudioManager audioManager = getAudioManager(context);
incomingRinger.stop();
outgoingRinger.stop();
beepRinger.stop();
if (playDisconnected) {
soundPool.play(disconnectedSoundId, 1.0f, 1.0f, 0, 0, 1.0f);
}
if (audioManager.isBluetoothScoOn()) {
audioManager.setBluetoothScoOn(false);
audioManager.stopBluetoothSco();
}
audioManager.setSpeakerphoneOn(false);
audioManager.setMicrophoneMute(false);
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.abandonAudioFocus(null);
}
@Override
public void onAudioFocusChange(int focusChange) {
Log.i(TAG, "Audio Focus Changed , " + focusChange);
}
public boolean isSpeakerOn() {
AudioManager audioManager = getAudioManager(context);
return audioManager.isSpeakerphoneOn();
}
public void setSpeakerOn(boolean status) {
AudioManager audioManager = getAudioManager(context);
audioManager.setSpeakerphoneOn(status);
if (status && audioManager.isBluetoothScoOn()) {
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
}
}
public void setMicrophoneMute(boolean on) {
AudioManager audioManager = getAudioManager(context);
audioManager.setMicrophoneMute(on);
}
public boolean isMicrophoneMute() {
AudioManager audioManager = getAudioManager(context);
return audioManager.isMicrophoneMute();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment