Call Convenience methods for Stitch and JavaScript

In this getting started guide we'll cover adding call methods to the Conversation we created in the simple conversation with audio getting started guide. We'll deal with member call events that trigger on the application and call state events that trigger on the Call object.

Concepts

This guide will introduce you to the following concepts.

  • Calls - calling an User in your application without creating a Conversation first
  • Call Events - member:call event that fires on an Application
  • Call Status Events - call:status:changed event that fires on an Application when a status in the Call changes

Before you begin

1 - Update the JavaScript App

We will use the application we already created for the first audio getting started guide. All the basic setup has been done in the previous guides and should be in place. We can now focus on updating the client-side application.

1.1 - Add call control UI

First, we'll add the UI for a user to call another user, and then see when it's being called. We'll also add UI in order to hang up the call. We'll hide the call-incoming and call-members using CSS until the user is interacting with a call. Let's add the UI at the top of the conversations area.

  <style>
    #call-incoming, #call-members {
      display: none
    }
  </style>
  <section id="conversations">
    <form id="call-form">
      <h1>Call User</h1>
      <input type="text" name="username" value="">
      <input type="submit" value="Call" />
    </form>
    <div>
      <div id="call-incoming">
        <p></p><button id="yes">Yes</button><button id="no">No</button>
      </div>
      <div id="call-members">
        <p></p><button id="hang-up">Hang Up</button>
      </div>
    </div>
    ...
  </section>

And add the new UI in the class constructor

constructor() {
  ...
  this.callForm = document.getElementById('call-form')
  this.callIncoming = document.getElementById('call-incoming')
  this.callMembers = document.getElementById('call-members')
  this.callYes = document.getElementById('yes')
  this.callNo = document.getElementById('no')
  this.hangUpButton = document.getElementById('hang-up')
}

1.2 - Add helper methods

We hid the call-incoming and call-members elements via CSS, so let's add showCallIncoming(member) and showCallMembers(member) as a helper methods to display the incoming call notification and add information about the members in a call:

showCallIncoming(member) {
  var memberName
  if (member == "unknown") {
    memberName = "a phone"
  } else {
    memberName = member.user.name
  }
  this.callIncoming.style.display = "block"
  this.callIncoming.children[0].textContent = "Incoming call from " + memberName + ". Do you want to answer?"
}

showCallMembers(member) {
  var memberName
  if (member == "unknown") {
    memberName = "a phone"
  } else {
    memberName = member.user.name
  }
  this.callMembers.style.display = "block"
  this.callIncoming.style.display = "none"
  this.callMembers.children[0].textContent = "You are in a call with " + memberName
}

We've added some logic in the methods in order to identify the caller as a User in the application or a person calling from a phone. When we receive a phone call into our application, the member is listed as unknown.

1.3 - Add member:call listener

Next, we'll add a listener for member:call events on the app, so that we can let the user know when someone calls. We'll call the showCallIncoming method we just created, and that sets up the UI that allows a user to either answer() the incoming call or hangsUp(). If the user answers the call, the SDK automatically creates an <audio> element and passes the stream into it. So we'll have to call showCallMembers in order to display information about the call and a way to hang up the call. Let's update the app promise after login(userToken) with the code:

...
.login(userToken)
.then(app => {
  ...
  this.app = app

  app.on("member:call", (member, call) => {
    this.call = call
    console.log("member:call - ", call);
    if ((this.call.from != "unknown") && (this.app.me.name != this.call.from.user.name)) {
      this.showCallIncoming(call.from)
    } else {
      this.showCallMembers("unknown")
    }
  })
})

1.4 - Add Call functionality

With these first parts we're listening member:call events on the application. Now let's see how to trigger those type of events, by making a call. Let's add an event listener for callForm inside the setupUserEvents() method. We'll take a list of user names from the input and pass those to the call() method on the Application object.

setupUserEvents() {
  ...

  this.callForm.addEventListener('submit', (event) => {
    event.preventDefault()
    var usernames = this.callForm.children.username.value.split(",").map(username => username.trim())

    this.app.call(usernames)
  })
}

We'll also have to setup listeners for the other buttons we created in order to manage the call, so let's add them to setupUserEvents().

setupUserEvents() {
  ...

  this.hangUpButton.addEventListener('click', () => {
    this.call.hangUp()
    this.callMembers.style.display = "none"
  })

  this.callYes.addEventListener('click', () => {
    this.call.answer()
    this.showCallMembers(this.call.from)
  })

  this.callNo.addEventListener('click', () => {
    this.call.hangUp()
    this.callIncoming.style.display = "none"
  })
}

If we want to be notified when the call status changes, we need to add a listener for call:status:changed on the Application. Let's update the app promise after login(userToken) with the code::

...
.login(userToken)
.then(app => {
  ...

  app.on("call:status:changed", (call) => {
    console.log("call:status:changed - ", call.status)
  })
})

1.5 - Open the conversation in two browser windows

Now run index.html in two side-by-side browser windows, making sure to login with the user name jamie in one and with alice in the other. Call one from the other, accept the call and start talking. You'll also see events being logged in the browser console.

Thats's it! Your page should now look something like this  .

1.6 - Calling a Stitch user from a phone

After you've set up you're app to handle incoming calls, you can follow the PSTN to IP tutorial  published on our blog to find out how you can connect a phone call to a Stitch user. Now you can make PSTN Phone Calls via the Nexmo Voice API and receive those calls via the Stitch SDK.

Because we've added logic in this quick start guide in order to differentiate between Users and PSTN phone calls, we don't need to change the quick start code, just set up an NCCO, as shown in the PSTN to IP tutorial  .

Where next?

Call Convenience methods for Stitch and Android

In this getting started guide we'll cover adding call methods to the Conversation we created in the simple conversation with audio getting started guide. We'll deal with member call events that trigger on the application and call state events that trigger on the Call object.

The main difference between using these Call convenience methods and enabling and disabling the audio in the previous quickstart is that these methods do a lot of the heavy lifting for you. By calling a user directly, a new conversation is created, and users are automatically invited to the new conversation with audio enabled. This can make it easier to start a separate direct call to a user or start a private group call among users.

Concepts

This guide will introduce you to the following concepts.

  • Calls - calling an User in your application without creating a Conversation first
  • Call Events - CallEvent event that fires on an ConversationClient or Call

Before you begin

1 - Update the Android App

We will use the application we already created for the first audio getting started guide. All the basic setup has been done in the previous guides and should be in place. We can now focus on updating the client-side application.

1.1 - Initiating a call

To initiate call we're going to let the user choose to start a call or enter a conversation after they login.

//LoginActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);
    ...
    chatBtn = findViewById(R.id.chat);
    chatBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            showCallOrConversationDialog();
        }
    });
    ...
}

private void showCallOrConversationDialog() {
    final AlertDialog.Builder dialog = new AlertDialog.Builder(LoginActivity.this)
            .setTitle("Call a user or enter conversation?")
            .setPositiveButton("Enter Conversation", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    retrieveConversations();
                }
            })
            .setNeutralButton("Call User", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    showCallUserDialog();
                }
            });

    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            dialog.show();
        }
    });
}

private void showCallUserDialog() {
    final EditText input = new EditText(LoginActivity.this);
    final AlertDialog.Builder dialog = new AlertDialog.Builder(LoginActivity.this)
            .setTitle("Which user do you want to call?")
            .setPositiveButton("Call", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    callUser(input.getText().toString());
                }
            });

    LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT,
            LinearLayout.LayoutParams.MATCH_PARENT);
    input.setLayoutParams(lp);
    dialog.setView(input);

    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            dialog.show();
        }
    });
}

private void callUser(String username) {
    Intent intent = new Intent(LoginActivity.this, CallActivity.class);
    intent.putExtra("USERNAME", username);
    startActivity(intent);
}

As you can see, the user will still login with the login button. But now the chat button asks them to start a call or enter a conversation. If they start a call, we'll ask what user they want to call and then start a new activity named CallActivity by passing in the user's name in the Intent.

1.2 - Using the CallActivity

This activity will have only two UI pieces, a TextView that tells you who you're in a call with and a "Hang Up" Button. This is what the layout will look like.

<!--activity_call.xml-->
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.nexmo.callingusers.CallActivity">

    <TextView
        android:padding="16dp"
        android:textSize="32sp"
        android:gravity="center"
        android:id="@+id/username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="In Call With Jamie"/>

    <Button
        android:id="@+id/hangup"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:text="Hang Up"
        app:layout_constraintBottom_toBottomOf="parent" />

</android.support.constraint.ConstraintLayout>

Now that we've added the UI we need to implement the calling functionality and handle hanging up on the call.

//CallActivity.java
public class CallActivity extends AppCompatActivity {
    private static final int PERMISSION_REQUEST_AUDIO = 0;

    private ConversationClient conversationClient;
    private Call currentCall;
    private String username;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_call);

        conversationClient = ((ConversationClientApplication) getApplication()).getConversationClient();
        Intent intent = getIntent();
        username = intent.getStringExtra("USERNAME");
        TextView usernameTxt = findViewById(R.id.username);
        usernameTxt.setText("In call with "+ username);

        if (checkAudioPermissions()) {
            callUser(username);
        } else {
            logAndShow("Check audio permissions");
        }

        Button hangUpBtn = findViewById(R.id.hangup);
        hangUpBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                hangup();
            }
        });
    }


    private void callUser(String username) {
        conversationClient.call(Collections.singletonList(username), new RequestHandler<Call>() {
            @Override
            public void onError(NexmoAPIError apiError) {
                logAndShow(apiError.getMessage());
            }

            @Override
            public void onSuccess(Call result) {
                currentCall = result;
                attachCallListeners(currentCall);
            }
        });
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST_AUDIO: {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    callUser(username);
                    break;
                } else {
                    logAndShow("Enable audio permissions to continue");
                    break;
                }
            }
            default: {
                logAndShow("Issue with onRequestPermissionsResult");
                break;
            }
        }
    }

    private void attachCallListeners(Call incomingCall) {
        //Listen for incoming member events in a call
        ResultListener<CallEvent> callEventListener = new ResultListener<CallEvent>() {
            @Override
            public void onSuccess(CallEvent message) {
                Log.d(TAG, "callEvent : state: " + message.getState() + " .content:" + message.toString());
            }
        };
        incomingCall.event().add(callEventListener);
    }

    private boolean checkAudioPermissions() {
        if (ContextCompat.checkSelfPermission(CallActivity.this, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
            return true;
        } else {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, RECORD_AUDIO)) {
                logAndShow("Need permissions granted for Audio to work");
            } else {
                ActivityCompat.requestPermissions(CallActivity.this, new String[]{RECORD_AUDIO}, PERMISSION_REQUEST_AUDIO);
            }
        }
        return false;
    }

    private void hangup() {
        if (currentCall != null) {
            currentCall.hangup(new RequestHandler<Void>() {
                @Override
                public void onError(NexmoAPIError apiError) {
                    logAndShow("Cannot hangup: " + apiError.toString());
                }

                @Override
                public void onSuccess(Void result) {
                    logAndShow("Call completed.");
                    finish();
                }
            });

        }
    }
}

As you can see the activity starts out with automatically calling the user from the previous LoginActivity. We'll also want to handle requesting the correct permissions as we did in the previous quickstart. We'll also want to show any other events in the call. Such as the other user answering, hanging up, or rejecting the call. Our user can also click on the "Hang Up" Button at any time to end the call and finish the activity.

2.1 - Listening for a call

We want to listen for call events, the same way that we listened for conversation invites in the Inviting Members quickstart. First we'll let the user login and select a conversation. If they're called while they're in the ChatActivity, then we'll answer the call in the current activity.

We can do that by adding a ResultListener to conversationClient.callEvent(). When a call comes in, the onSuccess() callback will be fired. You may want to show a UI that allows the user to accept or reject the call, but in this demo we'll just answer the call by calling answer() on the incoming call. Once the call is answered, then we'll attach the call listeners that will listen for incoming member events in a call and show a hang up button that we'll need to implement in the next section.

private void attachListeners() {
    //Listen for incoming calls
    conversationClient.callEvent().add(new ResultListener<Call>() {
        @Override
        public void onSuccess(final Call incomingCall) {
            logAndShow("answering Call");
            //Answer an incoming call
            incomingCall.answer(new RequestHandler<Void>() {
                @Override
                public void onError(NexmoAPIError apiError) {
                    logAndShow("Error answer: " + apiError.getMessage());
                }

                @Override
                public void onSuccess(Void result) {
                    //save the call as a member variable so we can reference it outside of this method.
                    currentCall = incomingCall;
                    attachCallListeners(incomingCall);
                    //TODO implement
                    showHangUpButton(true);
                }
            });
        }
    });
}

private void attachCallListeners(Call incomingCall) {
    //Listen for incoming member events in a call
    ResultListener<CallEvent> callEventListener = new ResultListener<CallEvent>() {
        @Override
        public void onSuccess(CallEvent message) {
            Log.d(TAG, "callEvent : state: " + message.getState() + " .content:" + message.toString());
        }
    };
    incomingCall.event().add(callEventListener);
}

2.2 - Let users hang up on a call

Let's add the UI for a user to call another user, and then be able to hang up. We'll hide the Hang Up Button with android:visible="false" until the user is in a call. Let's add the UI in the Options Menu

//app/src/main/res/menu/chat_menu.xml
<item android:id="@+id/hangup"
    android:title="Hang Up Call"
    android:visible="false"
    app:showAsAction="ifRoom"/>

Now we need to handle the user clicking on the "Hang Up Call" button.

//ChatActivity.java
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
    //hold a reference to the optionsMenu so we can change the visibility of `hangup`
    optionsMenu = menu;
    return super.onPrepareOptionsMenu(menu);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.audio:
            requestAudio();
            return true;
        case R.id.hangup:
            //TODO implement
            hangup();
            return true;
        default:
            return super.onOptionsItemSelected(item);
    }
}

private void hangup() {
    if (currentCall != null) {
        currentCall.hangup(new RequestHandler<Void>() {
            @Override
            public void onError(NexmoAPIError apiError) {
                logAndShow("Cannot hangup: " + apiError.toString());
            }

            @Override
            public void onSuccess(Void result) {
                logAndShow("Call completed.");
                //Hide the Hang Up button after the user hangs up
                showHangUpButton(false);
            }
        });

    }
}

private void showHangUpButton(boolean visible) {
    if (optionsMenu != null) {
        optionsMenu.findItem(R.id.hangup).setVisible(visible);
    }
}

2.3 Receive a PSTN Phone Call via Stitch

After you've set up you're app to handle incoming calls, you can follow the phone to app calling guide. Now you can make PSTN Phone Calls via the Nexmo Voice API and receive those calls via the Stitch SDK.

3 - Open the app on two devices

Now run the app on two devices (make sure they have a working mic and speakers!), making sure to login with the user name jamie in one and with alice in the other. Call one from the other, accept the call and start talking. You'll also see events being logged Logcat.

Where next?

Call Convenience methods for Stitch and iOS

In this getting started guide we'll cover adding call methods to the Conversation we created in the simple conversation with audio getting started guide. We'll deal with member call events that trigger on the application and call state events that trigger on the Call object.

The main difference between using these Call convenience methods and enabling and disabling the audio in the previous quickstart is that these methods do a lot of the heavy lifting for you. By calling a user directly, a new conversation is created, and users are automatically invited to the new conversation with audio enabled. This can make it easier to start a separate direct call to a user or start a private group call among users.

Concepts

This guide will introduce you to the following concepts.

  • Calls - calling an User in your application without creating a Conversation first
  • Call Events - CallEvent event that fires on an ConversationClient or Call

Before you begin

1.0 - Updating iOS App

We will use the application we already created for the first audio getting started guide. All the basic setup has been done in the previous guides and should be in place. We can now focus on updating the client-side application.

1.1 Modify the ChatController with .storyboard files

To modify the .storyboard to accommodate a call convenience method, let's perform the following changes:

  • Inside of the scene for ChatViewController.swift add an instance of UIBarButtonItem to the upper right hand corner of the UINavigationController.

  • Control drag from the instance to the ChatViewController.swift to create an action we will program later.

  • Inside of this action simply write the following: call().

1.2 Call Emoji! 📞

With the UIBarButtonItem properly laid out in .storyboard the next step is to configure its UI with a Call Emoji! 📞

  • Under the attributes inspector in the utilities menu change the UIBarButtonItem's System Item to Custom.
  • Inside of the Bar Item's properties, select title.
  • Inside of the Bar Item's title property add a 📞!

1.3 ☎️ call convenience method

How will we configure the UIBarButton's action? We will configure it with our call convenience method, which is built on top of our existing architecture for the client. We access call functionality through class calledmedia. In themediaclass there is a single method for handling calls, calledcall`. There were no puns intended here! ;)

do {
    let users = ["user1", "user2"]

    try client.media.call(users, onSuccess: { result in
        // result contains Call object and any errors from requesting invites for users
    }, onError: { networkError in
        // if you would like to work on the networkError, you can here.
    })
} catch let error {
    // if it is some other error, you can catch it here.
}

The method is really simple. It takes an array of users. It handles connecting with each one of the elements in the user array. Before we program it for real. Let's make sure our UI for chat is setup.

2.0

To ensure the chat is setup, we will configure an instance of UITableView to handle messages in the chat. To implement the UITableView, take the following steps:

  • Add an instance of UITableView to the scene for ChatViewController in .storyboard
  • Control drag to create an reference in ChatViewController
  • Inside of ChatViewController's viewDidLoad(:) configure both the dataSource and delegate properties on our reference to tableView to .self.
  • Last but not least we will add an extension to ensure conformity to the required methods:
extension ChatController : UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return conversation!.events.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cellWithIdentifier", for: indexPath)

        let message = conversation?.events[indexPath.row] as? TextEvent

        cell.textLabel?.text = message?.text

        return cell;
    }

} 

With our chat set up, we are ready to move onto setting up the call method.

2.0 - 📞 + ☎️ equals a call

Earlier we dropped the following in an action for our call emoji 📞: call(). We will program this function with our call convenience method now. Below viewDidLoad() declare a private function called call() like so:

private func call() {
}

Inside of this method copy and paste the code snippet from earlier with our actual call method.

private func call() {

   client.media.call(users, onSuccess: { result in
        // result contains Call object and any errors from requesting invites for users
    }, onError: { networkError in
        // if you would like to work on the networkError, you can here.
    })

}

We no longer need the generic do-try-catch. Inside of the function, however, we will add an instance of UIAlertController. We will loop over each member in the a conversation displayed in the UITableView with the higher order function .forEach so that we add an action for calling each member to the activity sheet:

    // MARK: - Call Convenience Methods
    private func call() {

        let callAlert = UIAlertController(title: "Call", message: "Who would you like to call?", preferredStyle: .sheet)

        conversation?.members.forEach{ member in
            callAlert.addAction(UIAlertAction(title: member.user.username, style: .default, handler: {

                    ConversationClient.instance.media.call(member.user.username, onSuccess: { result in
                        // if you would like to display a UI for calling...
                    }, onError: { networkError in
                        // if you would like to display a log for error...
                    })
            }))
        }

        self.present(callAlert, animated: true, completion: nil)

    }

3.0 Try it out!

There it is. If a member is present in the chat, then he or she may be called. Open the app on two devices. Now run the app on two devices (make sure they have a working mic and speakers!), making sure to login with one user name in one and with another in the other. Call one from the other, accept the call and start talking. You'll also see events being logged. If you would like, you can compare codebases here  .

Calling a Stitch user from a phone

After you've set up you're app to handle incoming calls, you can follow the PSTN to IP tutorial  published on our blog to find out how you can connect a phone call to a Stitch user. Now you can make PSTN Phone Calls via the Nexmo Voice API and receive those calls via the Stitch SDK.

Where next?

Have a look at the Nexmo Conversation iOS SDK API Reference  .