add silent call feature

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2022-05-24 12:36:12 +02:00 committed by Marcel Hibbe (Rebase PR Action)
parent 6669c9fecc
commit a23d4ef692
7 changed files with 120 additions and 35 deletions

View File

@ -231,6 +231,7 @@ public class CallActivity extends CallBaseActivity {
private boolean microphoneOn = false; private boolean microphoneOn = false;
private boolean isVoiceOnlyCall; private boolean isVoiceOnlyCall;
private boolean isCallWithoutNotification;
private boolean isIncomingCallFromNotification; private boolean isIncomingCallFromNotification;
private Handler callControlHandler = new Handler(); private Handler callControlHandler = new Handler();
private Handler callInfosHandler = new Handler(); private Handler callInfosHandler = new Handler();
@ -287,6 +288,7 @@ public class CallActivity extends CallBaseActivity {
conversationPassword = extras.getString(BundleKeys.INSTANCE.getKEY_CONVERSATION_PASSWORD(), ""); conversationPassword = extras.getString(BundleKeys.INSTANCE.getKEY_CONVERSATION_PASSWORD(), "");
conversationName = extras.getString(BundleKeys.INSTANCE.getKEY_CONVERSATION_NAME(), ""); conversationName = extras.getString(BundleKeys.INSTANCE.getKEY_CONVERSATION_NAME(), "");
isVoiceOnlyCall = extras.getBoolean(BundleKeys.INSTANCE.getKEY_CALL_VOICE_ONLY(), false); isVoiceOnlyCall = extras.getBoolean(BundleKeys.INSTANCE.getKEY_CALL_VOICE_ONLY(), false);
isCallWithoutNotification = extras.getBoolean(BundleKeys.INSTANCE.getKEY_CALL_WITHOUT_NOTIFICATION(), false);
if (extras.containsKey(BundleKeys.INSTANCE.getKEY_FROM_NOTIFICATION_START_CALL())) { if (extras.containsKey(BundleKeys.INSTANCE.getKEY_FROM_NOTIFICATION_START_CALL())) {
isIncomingCallFromNotification = extras.getBoolean(BundleKeys.INSTANCE.getKEY_FROM_NOTIFICATION_START_CALL()); isIncomingCallFromNotification = extras.getBoolean(BundleKeys.INSTANCE.getKEY_FROM_NOTIFICATION_START_CALL());
@ -1356,7 +1358,11 @@ public class CallActivity extends CallBaseActivity {
int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1}); int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
ncApi.joinCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken), inCallFlag) ncApi.joinCall(
credentials,
ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken),
inCallFlag,
isCallWithoutNotification)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.retry(3) .retry(3)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -1825,11 +1831,11 @@ public class CallActivity extends CallBaseActivity {
int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1}); int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
ncApi.getPeersForCall( ncApi.getPeersForCall(
credentials, credentials,
ApiUtils.getUrlForCall( ApiUtils.getUrlForCall(
apiVersion, apiVersion,
baseUrl, baseUrl,
roomToken)) roomToken))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe(new Observer<ParticipantsOverall>() { .subscribe(new Observer<ParticipantsOverall>() {
@Override @Override
@ -2468,7 +2474,7 @@ public class CallActivity extends CallBaseActivity {
mediaPlayer.setDataSource(this, ringtoneUri); mediaPlayer.setDataSource(this, ringtoneUri);
mediaPlayer.setLooping(true); mediaPlayer.setLooping(true);
AudioAttributes audioAttributes = new AudioAttributes.Builder().setContentType( AudioAttributes audioAttributes = new AudioAttributes.Builder().setContentType(
AudioAttributes.CONTENT_TYPE_SONIFICATION) AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build(); .build();
mediaPlayer.setAudioAttributes(audioAttributes); mediaPlayer.setAudioAttributes(audioAttributes);

View File

@ -211,7 +211,8 @@ public interface NcApi {
@FormUrlEncoded @FormUrlEncoded
@POST @POST
Observable<GenericOverall> joinCall(@Nullable @Header("Authorization") String authorization, @Url String url, Observable<GenericOverall> joinCall(@Nullable @Header("Authorization") String authorization, @Url String url,
@Field("flags") Integer inCall); @Field("flags") Integer inCall,
@Field("silent") Boolean callWithoutNotification);
/* /*
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken
@ -239,8 +240,8 @@ public interface NcApi {
*/ */
@GET @GET
Observable<SignalingOverall> pullSignalingMessages(@Nullable @Header("Authorization") String authorization, @Url Observable<SignalingOverall> pullSignalingMessages(@Nullable @Header("Authorization") String authorization, @Url
String String
url); url);
/* /*
QueryMap items are as follows: QueryMap items are as follows:
@ -259,7 +260,7 @@ public interface NcApi {
@FormUrlEncoded @FormUrlEncoded
@PUT @PUT
Observable<GenericOverall> setUserData(@Header("Authorization") String authorization, @Url String url, Observable<GenericOverall> setUserData(@Header("Authorization") String authorization, @Url String url,
@Field("key") String key, @Field("value") String value); @Field("key") String key, @Field("value") String value);
/* /*
@ -281,14 +282,14 @@ public interface NcApi {
@POST @POST
Observable<PushRegistrationOverall> registerDeviceForNotificationsWithNextcloud(@Header("Authorization") Observable<PushRegistrationOverall> registerDeviceForNotificationsWithNextcloud(@Header("Authorization")
String authorization, String authorization,
@Url String url, @Url String url,
@QueryMap Map<String, @QueryMap Map<String,
String> options); String> options);
@DELETE @DELETE
Observable<GenericOverall> unregisterDeviceForNotificationsWithNextcloud(@Header("Authorization") Observable<GenericOverall> unregisterDeviceForNotificationsWithNextcloud(@Header("Authorization")
String authorization, String authorization,
@Url String url); @Url String url);
@FormUrlEncoded @FormUrlEncoded
@ -438,10 +439,10 @@ public interface NcApi {
@FormUrlEncoded @FormUrlEncoded
@POST @POST
Observable<GenericOverall> sendLocation(@Header("Authorization") String authorization, Observable<GenericOverall> sendLocation(@Header("Authorization") String authorization,
@Url String url, @Url String url,
@Field("objectType") String objectType, @Field("objectType") String objectType,
@Field("objectId") String objectId, @Field("objectId") String objectId,
@Field("metaData") String metaData); @Field("metaData") String metaData);
@DELETE @DELETE
Observable<GenericOverall> clearChatHistory(@Header("Authorization") String authorization, @Url String url); Observable<GenericOverall> clearChatHistory(@Header("Authorization") String authorization, @Url String url);
@ -484,23 +485,23 @@ public interface NcApi {
@FormUrlEncoded @FormUrlEncoded
@PUT @PUT
Observable<GenericOverall> setPredefinedStatusMessage(@Header("Authorization") String authorization, Observable<GenericOverall> setPredefinedStatusMessage(@Header("Authorization") String authorization,
@Url String url, @Url String url,
@Field("messageId") String selectedPredefinedMessageId, @Field("messageId") String selectedPredefinedMessageId,
@Field("clearAt") Long clearAt); @Field("clearAt") Long clearAt);
@FormUrlEncoded @FormUrlEncoded
@PUT @PUT
Observable<GenericOverall> setCustomStatusMessage(@Header("Authorization") String authorization, Observable<GenericOverall> setCustomStatusMessage(@Header("Authorization") String authorization,
@Url String url, @Url String url,
@Field("statusIcon") String statusIcon, @Field("statusIcon") String statusIcon,
@Field("message") String message, @Field("message") String message,
@Field("clearAt") Long clearAt); @Field("clearAt") Long clearAt);
@FormUrlEncoded @FormUrlEncoded
@PUT @PUT
Observable<GenericOverall> setStatusType(@Header("Authorization") String authorization, Observable<GenericOverall> setStatusType(@Header("Authorization") String authorization,
@Url String url, @Url String url,
@Field("statusType") String statusType); @Field("statusType") String statusType);
@GET @GET
Observable<StatusesOverall> getUserStatuses(@Header("Authorization") String authorization, @Url String url); Observable<StatusesOverall> getUserStatuses(@Header("Authorization") String authorization, @Url String url);
@ -508,7 +509,7 @@ public interface NcApi {
@POST @POST
Observable<GenericOverall> sendReaction(@Header("Authorization") String authorization, @Url String url, Observable<GenericOverall> sendReaction(@Header("Authorization") String authorization, @Url String url,
@Query("reaction") String reaction); @Query("reaction") String reaction);
@DELETE @DELETE
Observable<GenericOverall> deleteReaction(@Header("Authorization") String authorization, @Url String url, Observable<GenericOverall> deleteReaction(@Header("Authorization") String authorization, @Url String url,

View File

@ -103,7 +103,6 @@ import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.activities.CallActivity import com.nextcloud.talk.activities.CallActivity
import com.nextcloud.talk.activities.MainActivity import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
import com.nextcloud.talk.activities.TakePhotoActivity import com.nextcloud.talk.activities.TakePhotoActivity
import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
@ -143,6 +142,7 @@ import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.mention.Mention import com.nextcloud.talk.models.json.mention.Mention
import com.nextcloud.talk.presenters.MentionAutocompletePresenter import com.nextcloud.talk.presenters.MentionAutocompletePresenter
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
import com.nextcloud.talk.ui.dialog.AttachmentDialog import com.nextcloud.talk.ui.dialog.AttachmentDialog
import com.nextcloud.talk.ui.dialog.MessageActionsDialog import com.nextcloud.talk.ui.dialog.MessageActionsDialog
@ -886,6 +886,35 @@ class ChatController(args: Bundle) :
popupMenu.show() popupMenu.show()
} }
private fun showCallButtonMenu(isVoiceOnlyCall: Boolean) {
val anchor: View? = if (isVoiceOnlyCall) {
activity?.findViewById(R.id.conversation_voice_call)
} else {
activity?.findViewById(R.id.conversation_video_call)
}
if (anchor != null) {
val popupMenu = PopupMenu(
ContextThemeWrapper(view?.context, R.style.CallButtonMenu),
anchor,
Gravity.END
)
popupMenu.inflate(R.menu.chat_call_menu)
popupMenu.setOnMenuItemClickListener { item: MenuItem ->
when (item.itemId) {
R.id.call_without_notification -> startACall(isVoiceOnlyCall, true)
}
true
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
popupMenu.setForceShowIcon(true)
}
popupMenu.show()
}
}
private fun startPlayback(message: ChatMessage) { private fun startPlayback(message: ChatMessage) {
if (!this.isAttached) { if (!this.isAttached) {
@ -1827,7 +1856,7 @@ class ChatController(args: Bundle) :
} }
if (startCallFromNotification != null && startCallFromNotification ?: false) { if (startCallFromNotification != null && startCallFromNotification ?: false) {
startCallFromNotification = false startCallFromNotification = false
startACall(voiceOnly) startACall(voiceOnly, false)
} }
} }
@ -2403,6 +2432,22 @@ class ChatController(args: Bundle) :
if (CapabilitiesUtil.isAbleToCall(conversationUser)) { if (CapabilitiesUtil.isAbleToCall(conversationUser)) {
conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call) conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call) conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
if (CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "silent-call")) {
Handler().post {
activity?.findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {
showCallButtonMenu(true)
true
}
}
Handler().post {
activity?.findViewById<View?>(R.id.conversation_video_call)?.setOnLongClickListener {
showCallButtonMenu(false)
true
}
}
}
} else { } else {
menu.removeItem(R.id.conversation_video_call) menu.removeItem(R.id.conversation_video_call)
menu.removeItem(R.id.conversation_voice_call) menu.removeItem(R.id.conversation_voice_call)
@ -2425,11 +2470,11 @@ class ChatController(args: Bundle) :
return true return true
} }
R.id.conversation_video_call -> { R.id.conversation_video_call -> {
startACall(false) startACall(false, false)
return true return true
} }
R.id.conversation_voice_call -> { R.id.conversation_voice_call -> {
startACall(true) startACall(true, false)
return true return true
} }
R.id.conversation_info -> { R.id.conversation_info -> {
@ -2493,19 +2538,19 @@ class ChatController(args: Bundle) :
currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
} }
private fun startACall(isVoiceOnlyCall: Boolean) { private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
if (currentConversation?.canStartCall == false && currentConversation?.hasCall == false) { if (currentConversation?.canStartCall == false && currentConversation?.hasCall == false) {
Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show()
} else { } else {
ApplicationWideCurrentRoomHolder.getInstance().isDialing = true ApplicationWideCurrentRoomHolder.getInstance().isDialing = true
val callIntent = getIntentForCall(isVoiceOnlyCall) val callIntent = getIntentForCall(isVoiceOnlyCall, callWithoutNotification)
if (callIntent != null) { if (callIntent != null) {
startActivity(callIntent) startActivity(callIntent)
} }
} }
} }
private fun getIntentForCall(isVoiceOnlyCall: Boolean): Intent? { private fun getIntentForCall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean): Intent? {
currentConversation?.let { currentConversation?.let {
val bundle = Bundle() val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, roomToken) bundle.putString(KEY_ROOM_TOKEN, roomToken)
@ -2518,6 +2563,9 @@ class ChatController(args: Bundle) :
if (isVoiceOnlyCall) { if (isVoiceOnlyCall) {
bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true) bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true)
} }
if (callWithoutNotification) {
bundle.putBoolean(BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION, true)
}
return if (activity != null) { return if (activity != null) {
val callIntent = Intent(activity, CallActivity::class.java) val callIntent = Intent(activity, CallActivity::class.java)

View File

@ -55,6 +55,7 @@ object BundleKeys {
val KEY_INVITED_EMAIL = "KEY_INVITED_EMAIL" val KEY_INVITED_EMAIL = "KEY_INVITED_EMAIL"
val KEY_CONVERSATION_NAME = "KEY_CONVERSATION_NAME" val KEY_CONVERSATION_NAME = "KEY_CONVERSATION_NAME"
val KEY_CALL_VOICE_ONLY = "KEY_CALL_VOICE_ONLY" val KEY_CALL_VOICE_ONLY = "KEY_CALL_VOICE_ONLY"
val KEY_CALL_WITHOUT_NOTIFICATION = "KEY_CALL_WITHOUT_NOTIFICATION"
val KEY_ACTIVE_CONVERSATION = "KEY_ACTIVE_CONVERSATION" val KEY_ACTIVE_CONVERSATION = "KEY_ACTIVE_CONVERSATION"
val KEY_SERVER_CAPABILITIES = "KEY_SERVER_CAPABILITIES" val KEY_SERVER_CAPABILITIES = "KEY_SERVER_CAPABILITIES"
val KEY_FROM_NOTIFICATION_START_CALL = "KEY_FROM_NOTIFICATION_START_CALL" val KEY_FROM_NOTIFICATION_START_CALL = "KEY_FROM_NOTIFICATION_START_CALL"

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Marcel Hibbe
~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/call_without_notification"
android:icon="@drawable/ic_baseline_notifications_off_24"
android:title="@string/call_without_notification" />
</menu>

View File

@ -522,5 +522,6 @@
<string name="reactions_tab_all">All</string> <string name="reactions_tab_all">All</string>
<string name="send_without_notification">Send without notification</string> <string name="send_without_notification">Send without notification</string>
<string name="call_without_notification">Call without notification</string>
</resources> </resources>

View File

@ -63,6 +63,8 @@
<item name="iconTint">@color/fontAppbar</item> <item name="iconTint">@color/fontAppbar</item>
</style> </style>
<style name="CallButtonMenu" parent="@style/ChatSendButtonMenu"></style>
<style name="BottomNavigationView" parent="@style/Widget.MaterialComponents.BottomNavigationView"> <style name="BottomNavigationView" parent="@style/Widget.MaterialComponents.BottomNavigationView">
<item name="elevation">1dp</item> <item name="elevation">1dp</item>
</style> </style>