Join video sessions with a short link
When a Contact Center agent uses the Zoom client to invite a user to an unscheduled video session, Contact Center
sends an invitation email containing a shortened link like https://shortlink.zoom.com/cc/bMefyOFPf to invite
the user to join the video session. When the user opens or pastes that shortened link into your Contact Center
app, have your app route the URL to the handler.
Configure URL handling
Call handleShortLinkVideoURL(url) after your app receives a Contact Center short link from either the clipboard or an app.
For a clipboard flow:
- Read the primary clip only when clipboard checking is enabled.
- Trim the text and reject an empty value or text longer than 512 characters.
- Ask the user to confirm before opening the link.
private static final String TAG = "ShortLinkVideo";
private static final int MAX_CLIPBOARD_LINK_LENGTH = 512;
// Call this method from the positive action of your clipboard confirmation dialog.
private void handleShortLinkVideoURL(String shortLink) {
// Normalize and limit clipboard text before passing it to the SDK.
String normalizedLink = shortLink == null ? "" : shortLink.trim();
if (normalizedLink.isEmpty() || normalizedLink.length() > MAX_CLIPBOARD_LINK_LENGTH) {
return;
}
ZoomCCVideoService service = ZoomCCInterface.INSTANCE.getZoomCCVideoService();
ZoomCCVideoListener shortLinkVideoListener = new ZoomCCVideoListener() {
@Override
public void onError(int error, long detail, @NonNull String description) {
// Called when link handling fails after the request is accepted.
// Show a generic message; keep details in logs only.
Log.e(TAG, "Short-link video error: error=" + error + ", detail=" + detail);
}
@Override
public void onClientEvent(@NonNull ClientEvent event) {
}
@Override
public void unreadMsgCountChanged(int count) {
}
@Override
public void onEngagementEnd(@NonNull String engagementId) {
}
@Override
public void onEngagementStart(@NonNull String engagementId) {
}
@Override
public void onLoginStatus(@Nullable IMStatus status) {
}
};
service.addListener(shortLinkVideoListener);
// true means handling started, not that joining has finished.
boolean accepted = service.handleShortLinkVideoURL(normalizedLink);
if (!accepted) {
service.removeListener(shortLinkVideoListener);
Toast.makeText(this, "Unable to open short link", Toast.LENGTH_SHORT).show();
return;
}
}
private String getClipboardJoinLink() {
if (clipboardManager == null) {
return null;
}
try {
if (!clipboardManager.hasPrimaryClip()) {
return null;
}
ClipData clip = clipboardManager.getPrimaryClip();
if (clip == null || clip.getItemCount() == 0) {
return null;
}
// Use the first clipboard item as plain text.
CharSequence clipText = clip.getItemAt(0).coerceToText(this);
String text = clipText == null ? null : clipText.toString().trim();
if (text == null || text.isEmpty() || text.length() > MAX_CLIPBOARD_LINK_LENGTH) {
return null;
}
return text;
} catch (RuntimeException e) {
Log.w(TAG, "Unable to read clipboard text.", e);
return null;
}
}
@Override
protected void onResume() {
super.onResume();
// Listen only while the Activity is visible.
if (clipboardJoinLinkCheckEnabled && clipboardManager != null && clipboardListener != null) {
clipboardManager.addPrimaryClipChangedListener(clipboardListener);
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus && clipboardJoinLinkCheckEnabled) {
checkClipboardJoinLink();
}
}
@Override
protected void onPause() {
// Stop clipboard callbacks while the Activity is paused.
if (clipboardManager != null && clipboardListener != null) {
clipboardManager.removePrimaryClipChangedListener(clipboardListener);
}
super.onPause();
}
@Override
protected void onDestroy() {
if (clipboardManager != null && clipboardListener != null) {
clipboardManager.removePrimaryClipChangedListener(clipboardListener);
}
// Close the confirmation dialog before releasing the Activity.
if (clipboardDialog != null) {
clipboardDialog.dismiss();
clipboardDialog = null;
}
super.onDestroy();
}
private const val TAG = "ShortLinkVideo"
private const val MAX_CLIPBOARD_LINK_LENGTH = 512
private val clipboardManager by lazy {
getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
}
private var clipboardJoinLinkCheckEnabled = false
private var lastPromptedClipboardLink: String? = null
private var clipboardDialog: AlertDialog? = null
// Re-check the clipboard when its content changes.
private val clipboardListener = ClipboardManager.OnPrimaryClipChangedListener {
checkClipboardJoinLink()
}
private fun enableClipboardJoinLinkCheck() {
clipboardJoinLinkCheckEnabled = true
// Remove first so repeated clicks do not register duplicates.
clipboardManager.removePrimaryClipChangedListener(clipboardListener)
clipboardManager.addPrimaryClipChangedListener(clipboardListener)
checkClipboardJoinLink()
}
private fun checkClipboardJoinLink() {
val shortLink = getClipboardJoinLink() ?: return
if (shortLink == lastPromptedClipboardLink || clipboardDialog?.isShowing == true) {
return
}
lastPromptedClipboardLink = shortLink
// Ask before using text copied from another app.
clipboardDialog = AlertDialog.Builder(this)
.setTitle("Open short link?")
.setMessage("A link was found in your clipboard.")
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
handleShortLinkVideoURL(shortLink)
dialog.dismiss()
}
.setOnDismissListener { clipboardDialog = null }
.show()
}
private fun handleShortLinkVideoURL(shortLink: String) {
// Normalize and limit clipboard text before passing it to the SDK.
val normalizedLink = shortLink.trim()
if (normalizedLink.isEmpty() || normalizedLink.length > MAX_CLIPBOARD_LINK_LENGTH) {
return
}
val service = ZoomCCInterface.getZoomCCVideoService()
val shortLinkVideoListener = object : ZoomCCVideoListener {
override fun onError(error: Int, detail: Long, description: String) {
// Called when link handling fails after the request is accepted.
// Show a generic message; keep details in logs only.
Log.e(TAG, "Short-link video error: error=" + error + ", detail=" + detail)
}
}
service.addListener(shortLinkVideoListener)
val accepted = service.handleShortLinkVideoURL(normalizedLink)
if (!accepted) {
service.removeListener(shortLinkVideoListener)
Toast.makeText(this, "Unable to open short link", Toast.LENGTH_SHORT).show()
return
}
}
private fun getClipboardJoinLink(): String? {
val text = try {
if (!clipboardManager.hasPrimaryClip()) return null
val clip = clipboardManager.primaryClip ?: return null
if (clip.itemCount == 0) return null
// Use the first clipboard item as plain text.
clip.getItemAt(0).coerceToText(this)?.toString()?.trim()
} catch (e: RuntimeException) {
Log.w(TAG, "Unable to read clipboard text.", e)
return null
}
return text?.takeIf { it.isNotEmpty() }
?.takeIf { it.length <= MAX_CLIPBOARD_LINK_LENGTH }
}
override fun onResume() {
super.onResume()
// Listen only while the Activity is visible.
if (clipboardJoinLinkCheckEnabled) {
clipboardManager.addPrimaryClipChangedListener(clipboardListener)
}
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus && clipboardJoinLinkCheckEnabled) {
checkClipboardJoinLink()
}
}
override fun onPause() {
// Stop clipboard callbacks while the Activity is paused.
clipboardManager.removePrimaryClipChangedListener(clipboardListener)
super.onPause()
}
override fun onDestroy() {
clipboardManager.removePrimaryClipChangedListener(clipboardListener)
clipboardDialog?.dismiss()
super.onDestroy()
}
Handle invalid or expired links
If handleShortLinkVideoURL(url) returns false, the SDK did not accept the link or could not start the flow. Show a generic invalid-link message and do not open a video session.
A return value of true means that handling started. It does not guarantee that link resolution or joining completed successfully. Later failures are delivered to onError.
If the SDK can't process the link, return false from your URL-handling path and execute this callback method.
@Override
public void onError(int error, long detail, @NonNull String description) {
}
override fun onError(error: Int, detail: Long, description: String) {
}
After this completes, onError returns an error code and relevant error information.