From 642146de02a01de998d5b84ce53d5597d4d17b9e Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 4 Feb 2021 08:29:09 +0100 Subject: [PATCH] improve call screens, add outgoing ringtone Signed-off-by: Marcel Hibbe --- README.md | 7 + .../talk/activities/MagicCallActivity.kt | 3 - .../java/com/nextcloud/talk/api/NcApi.java | 4 +- .../talk/controllers/CallController.java | 277 ++++++++++++------ .../CallNotificationController.java | 224 ++++++++------ .../talk/controllers/ChatController.kt | 25 +- .../json/conversations/Conversation.java | 2 + .../com/nextcloud/talk/utils/ApiUtils.java | 4 + .../main/res/drawable/ic_comment_white.xml | 29 ++ app/src/main/res/layout/call_item.xml | 23 +- app/src/main/res/layout/call_states.xml | 13 +- app/src/main/res/layout/controller_call.xml | 261 ++++++++++------- .../layout/controller_call_notification.xml | 22 +- app/src/main/res/layout/controller_chat.xml | 12 +- .../main/res/raw/tr110_1_kap8_3_freiton1.ogg | Bin 0 -> 34121 bytes app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/strings.xml | 5 + 17 files changed, 583 insertions(+), 331 deletions(-) create mode 100644 app/src/main/res/drawable/ic_comment_white.xml create mode 100644 app/src/main/res/raw/tr110_1_kap8_3_freiton1.ogg diff --git a/README.md b/README.md index 1a30b6055..a231fa454 100644 --- a/README.md +++ b/README.md @@ -84,5 +84,12 @@ commit automatically with `git commit -s`. You can also use git [aliases](https: like `git config --global alias.ci 'commit -s'`. Now you can commit with `git ci` and the commit will be signed. +## Credits + +### Ringtones + +- [Ringtones by Librem](https://soundcloud.com/feandesign/sets/librem-5-sounds) +- [Telefon-Freiton in Deutschland nach DTAG 1 TR 110-1, Kap. 8.3](https://commons.wikimedia.org/wiki/File:1TR110-1_Kap8.3_Freiton1.ogg) + [dcofile]: https://github.com/nextcloud/talk-android/blob/master/contribute/developer-certificate-of-origin [applyalicense]: https://github.com/nextcloud/talk-android/blob/master/contribute/HowToApplyALicense.md diff --git a/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt index 5075ebace..5421d24bd 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt @@ -40,9 +40,6 @@ import com.nextcloud.talk.controllers.CallNotificationController import com.nextcloud.talk.controllers.ChatController import com.nextcloud.talk.events.ConfigurationChangeEvent import com.nextcloud.talk.utils.bundle.BundleKeys -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY @AutoInjector(NextcloudTalkApplication::class) class MagicCallActivity : BaseActivity() { diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index fbf23c522..cc839685f 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -177,8 +177,10 @@ public interface NcApi { Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken */ + @FormUrlEncoded @POST - Observable joinCall(@Nullable @Header("Authorization") String authorization, @Url String url); + Observable joinCall(@Nullable @Header("Authorization") String authorization, @Url String url, + @Field("flags") Integer inCall); /* Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken diff --git a/app/src/main/java/com/nextcloud/talk/controllers/CallController.java b/app/src/main/java/com/nextcloud/talk/controllers/CallController.java index 490c95178..116f98073 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/CallController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/CallController.java @@ -46,6 +46,10 @@ import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + import com.bluelinelabs.logansquare.LoganSquare; import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.interfaces.DraweeController; @@ -136,9 +140,6 @@ import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; import autodagger.AutoInjector; import butterknife.BindView; import butterknife.OnClick; @@ -177,24 +178,31 @@ public class CallController extends BaseController { @BindView(R.id.pip_video_view) SurfaceViewRenderer pipVideoView; - @BindView(R.id.relative_layout) - RelativeLayout relativeLayout; + @BindView(R.id.controllerCallLayout) + RelativeLayout controllerCallLayout; @BindView(R.id.remote_renderers_layout) LinearLayout remoteRenderersLayout; - @BindView(R.id.callControlsRelativeLayout) - RelativeLayout callControls; + @BindView(R.id.callControlsLinearLayout) + LinearLayout callControls; @BindView(R.id.call_control_microphone) SimpleDraweeView microphoneControlButton; @BindView(R.id.call_control_camera) SimpleDraweeView cameraControlButton; @BindView(R.id.call_control_switch_camera) SimpleDraweeView cameraSwitchButton; - @BindView(R.id.connectingTextView) - TextView connectingTextView; + @BindView(R.id.callStateTextView) + TextView callStateTextView; - @BindView(R.id.connectingRelativeLayoutView) - RelativeLayout connectingView; + @BindView(R.id.callInfosLinearLayout) + LinearLayout callInfosLinearLayout; + @BindView(R.id.callVoiceOrVideoTextView) + TextView callVoiceOrVideoTextView; + @BindView(R.id.callConversationNameTextView) + TextView callConversationNameTextView; + + @BindView(R.id.callStateRelativeLayoutView) + RelativeLayout callStateView; @BindView(R.id.conversationRelativeLayoutView) RelativeLayout conversationView; @@ -202,7 +210,7 @@ public class CallController extends BaseController { @BindView(R.id.errorImageView) ImageView errorImageView; - @BindView(R.id.progress_bar) + @BindView(R.id.callStateProgressBar) ProgressBar progressBar; @Inject @@ -234,6 +242,7 @@ public class CallController extends BaseController { private CameraEnumerator cameraEnumerator; private String roomToken; private UserEntity conversationUser; + private String conversationName; private String callSession; private MediaStream localMediaStream; private String credentials; @@ -247,9 +256,12 @@ public class CallController extends BaseController { private boolean needsPing = true; private boolean isVoiceOnlyCall; + private boolean isIncomingCallFromNotification; private Handler callControlHandler = new Handler(); + private Handler callInfosHandler = new Handler(); private Handler cameraSwitchHandler = new Handler(); + // push to talk private boolean isPTTActive = false; private PulseAnimation pulseAnimation; private View.OnClickListener videoOnClickListener; @@ -276,7 +288,7 @@ public class CallController extends BaseController { @Parcel public enum CallStatus { - CALLING, CALLING_TIMEOUT, ESTABLISHED, IN_CONVERSATION, RECONNECTING, OFFLINE, LEAVING, PUBLISHER_FAILED + CONNECTING, CALLING_TIMEOUT, JOINED, IN_CONVERSATION, RECONNECTING, OFFLINE, LEAVING, PUBLISHER_FAILED } public CallController(Bundle args) { @@ -287,8 +299,13 @@ public class CallController extends BaseController { roomToken = args.getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), ""); conversationUser = args.getParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY()); conversationPassword = args.getString(BundleKeys.INSTANCE.getKEY_CONVERSATION_PASSWORD(), ""); + conversationName = args.getString(BundleKeys.INSTANCE.getKEY_CONVERSATION_NAME(), ""); isVoiceOnlyCall = args.getBoolean(BundleKeys.INSTANCE.getKEY_CALL_VOICE_ONLY(), false); + if (args.containsKey(BundleKeys.INSTANCE.getKEY_FROM_NOTIFICATION_START_CALL())) { + isIncomingCallFromNotification = args.getBoolean(BundleKeys.INSTANCE.getKEY_FROM_NOTIFICATION_START_CALL()); + } + credentials = ApiUtils.getCredentials(conversationUser.getUsername(), conversationUser.getToken()); baseUrl = args.getString(BundleKeys.INSTANCE.getKEY_MODIFIED_BASE_URL(), ""); @@ -298,11 +315,11 @@ public class CallController extends BaseController { } powerManagerUtils = new PowerManagerUtils(); - + if (args.getString("state", "").equalsIgnoreCase("resume")) { setCallState(CallStatus.IN_CONVERSATION); } else { - setCallState(CallStatus.CALLING); + setCallState(CallStatus.CONNECTING); } } @@ -362,7 +379,7 @@ public class CallController extends BaseController { //Create a new PeerConnectionFactory instance. PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); DefaultVideoEncoderFactory defaultVideoEncoderFactory = new DefaultVideoEncoderFactory( - rootEglBase.getEglBaseContext(),true,true); + rootEglBase.getEglBaseContext(), true, true); DefaultVideoDecoderFactory defaultVideoDecoderFactory = new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext()); peerConnectionFactory = PeerConnectionFactory.builder() @@ -456,7 +473,12 @@ public class CallController extends BaseController { cameraSwitchButton.setVisibility(View.GONE); cameraControlButton.setVisibility(View.GONE); pipVideoView.setVisibility(View.GONE); + + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.addRule(RelativeLayout.BELOW, R.id.callInfosLinearLayout); + remoteRenderersLayout.setLayoutParams(params); } else { + callControlEnableSpeaker.setVisibility(View.GONE); if (cameraEnumerator.getDeviceNames().length < 2) { cameraSwitchButton.setVisibility(View.GONE); } @@ -484,7 +506,7 @@ public class CallController extends BaseController { } private boolean isConnectionEstablished() { - return (currentCallStatus.equals(CallStatus.ESTABLISHED) || currentCallStatus.equals(CallStatus.IN_CONVERSATION)); + return (currentCallStatus.equals(CallStatus.JOINED) || currentCallStatus.equals(CallStatus.IN_CONVERSATION)); } @AfterPermissionGranted(100) @@ -648,6 +670,7 @@ public class CallController extends BaseController { boolean onMicrophoneLongClick() { if (!audioOn) { callControlHandler.removeCallbacksAndMessages(null); + callInfosHandler.removeCallbacksAndMessages(null); cameraSwitchHandler.removeCallbacksAndMessages(null); isPTTActive = true; callControls.setVisibility(View.VISIBLE); @@ -735,7 +758,7 @@ public class CallController extends BaseController { } } } - + @OnClick(R.id.callControlToggleChat) void onToggleChatClick() { ((MagicCallActivity) getActivity()).showChat(); @@ -868,6 +891,7 @@ public class CallController extends BaseController { if (show) { callControlHandler.removeCallbacksAndMessages(null); + callInfosHandler.removeCallbacksAndMessages(null); cameraSwitchHandler.removeCallbacksAndMessages(null); alpha = 1.0f; duration = 1000; @@ -875,8 +899,13 @@ public class CallController extends BaseController { callControls.setAlpha(0.0f); callControls.setVisibility(View.VISIBLE); + callInfosLinearLayout.setAlpha(0.0f); + callInfosLinearLayout.setVisibility(View.VISIBLE); + cameraSwitchButton.setAlpha(0.0f); - cameraSwitchButton.setVisibility(View.VISIBLE); + if (videoOn) { + cameraSwitchButton.setVisibility(View.VISIBLE); + } } else { callControlHandler.postDelayed(() -> animateCallControls(false, 0), 5000); return; @@ -920,6 +949,37 @@ public class CallController extends BaseController { }); } + if (callInfosLinearLayout != null) { + callInfosLinearLayout.setEnabled(false); + callInfosLinearLayout.animate() + .translationY(0) + .alpha(alpha) + .setDuration(duration) + .setStartDelay(startDelay) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (callInfosLinearLayout != null) { + if (!show) { + callInfosLinearLayout.setVisibility(View.GONE); + } else { + callInfosHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!isPTTActive) { + animateCallControls(false, 0); + } + } + }, 7500); + } + + callInfosLinearLayout.setEnabled(true); + } + } + }); + } + if (cameraSwitchButton != null) { cameraSwitchButton.setEnabled(false); cameraSwitchButton.animate() @@ -1139,8 +1199,15 @@ public class CallController extends BaseController { } private void performCall() { + Integer inCallFlag; + if (isVoiceOnlyCall) { + inCallFlag = (int) Participant.ParticipantFlags.IN_CALL_WITH_AUDIO.getValue(); + } else { + inCallFlag = (int) Participant.ParticipantFlags.IN_CALL_WITH_AUDIO_AND_VIDEO.getValue(); + } + ncApi.joinCall(credentials, - ApiUtils.getUrlForCall(baseUrl, roomToken)) + ApiUtils.getUrlForCall(baseUrl, roomToken), inCallFlag) .subscribeOn(Schedulers.io()) .retry(3) .observeOn(AndroidSchedulers.mainThread()) @@ -1153,7 +1220,7 @@ public class CallController extends BaseController { @Override public void onNext(GenericOverall genericOverall) { if (!currentCallStatus.equals(CallStatus.LEAVING)) { - setCallState(CallStatus.ESTABLISHED); + setCallState(CallStatus.JOINED); ApplicationWideCurrentRoomHolder.getInstance().setInCall(true); @@ -1339,7 +1406,7 @@ public class CallController extends BaseController { private void receivedSignalingMessage(Signaling signaling) throws IOException { String messageType = signaling.getType(); - if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CALLING)) { + if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CONNECTING)) { return; } @@ -1464,38 +1531,38 @@ public class CallController extends BaseController { } private void hangupNetworkCalls(boolean shutDownView) { - ncApi.leaveCall(credentials, ApiUtils.getUrlForCall(baseUrl, roomToken)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { + ncApi.leaveCall(credentials, ApiUtils.getUrlForCall(baseUrl, roomToken)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { - } + } - @Override - public void onNext(GenericOverall genericOverall) { - if (isMultiSession) { - if (shutDownView && getActivity() != null) { - getActivity().finish(); - } else if (!shutDownView && (currentCallStatus.equals(CallStatus.RECONNECTING) || currentCallStatus.equals(CallStatus.PUBLISHER_FAILED))) { - initiateCall(); - } - } else { - leaveRoom(shutDownView); + @Override + public void onNext(GenericOverall genericOverall) { + if (isMultiSession) { + if (shutDownView && getActivity() != null) { + getActivity().finish(); + } else if (!shutDownView && (currentCallStatus.equals(CallStatus.RECONNECTING) || currentCallStatus.equals(CallStatus.PUBLISHER_FAILED))) { + initiateCall(); } + } else { + leaveRoom(shutDownView); } + } - @Override - public void onError(Throwable e) { + @Override + public void onError(Throwable e) { - } + } - @Override - public void onComplete() { + @Override + public void onComplete() { - } - }); + } + }); } private void leaveRoom(boolean shutDownView) { @@ -1567,7 +1634,7 @@ public class CallController extends BaseController { // Calculate sessions that join the call newSessions.removeAll(oldSesssions); - if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CALLING)) { + if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CONNECTING)) { return; } @@ -1759,7 +1826,7 @@ public class CallController extends BaseController { boolean enableVideo = peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent .PeerConnectionEventType.SENSOR_FAR) && videoOn; if (getActivity() != null && EffortlessPermissions.hasPermissions(getActivity(), PERMISSIONS_CAMERA) && - (currentCallStatus.equals(CallStatus.CALLING) || isConnectionEstablished()) && videoOn + (currentCallStatus.equals(CallStatus.CONNECTING) || isConnectionEstablished()) && videoOn && enableVideo != localVideoTrack.enabled()) { toggleMedia(enableVideo, true); } @@ -2066,7 +2133,7 @@ public class CallController extends BaseController { private void gotNick(String sessionId, String nick, String type) { String remoteRendererTag = sessionId + "+" + type; - if (relativeLayout != null) { + if (controllerCallLayout != null) { RelativeLayout relativeLayout = remoteRenderersLayout.findViewWithTag(remoteRendererTag); TextView textView = relativeLayout.findViewById(R.id.peer_nick_text_view); if (!textView.getText().equals(nick)) { @@ -2079,7 +2146,7 @@ public class CallController extends BaseController { } } - @OnClick(R.id.connectingRelativeLayoutView) + @OnClick(R.id.callStateRelativeLayoutView) public void onConnectingViewClick() { if (currentCallStatus.equals(CallStatus.CALLING_TIMEOUT)) { setCallState(CallStatus.RECONNECTING); @@ -2097,16 +2164,24 @@ public class CallController extends BaseController { } switch (callState) { - case CALLING: + case CONNECTING: handler.post(() -> { playCallingSound(); - connectingTextView.setText(R.string.nc_connecting_call); - if (connectingView.getVisibility() != View.VISIBLE) { - connectingView.setVisibility(View.VISIBLE); + if (isIncomingCallFromNotification) { + callStateTextView.setText(R.string.nc_call_incoming); + } else { + callStateTextView.setText(R.string.nc_call_ringing); + } + callConversationNameTextView.setText(conversationName); + + callVoiceOrVideoTextView.setText(isVoiceOnlyCall ? R.string.nc_voice_call : R.string.nc_video_call); + + if (callStateView.getVisibility() != View.VISIBLE) { + callStateView.setVisibility(View.VISIBLE); } - if (conversationView.getVisibility() != View.INVISIBLE) { - conversationView.setVisibility(View.INVISIBLE); + if (remoteRenderersLayout.getVisibility() != View.INVISIBLE) { + remoteRenderersLayout.setVisibility(View.INVISIBLE); } if (progressBar.getVisibility() != View.VISIBLE) { @@ -2121,17 +2196,18 @@ public class CallController extends BaseController { case CALLING_TIMEOUT: handler.post(() -> { hangup(false); - connectingTextView.setText(R.string.nc_call_timeout); - if (connectingView.getVisibility() != View.VISIBLE) { - connectingView.setVisibility(View.VISIBLE); + callStateTextView.setText(R.string.nc_call_timeout); + callVoiceOrVideoTextView.setText(isVoiceOnlyCall ? R.string.nc_voice_call : R.string.nc_video_call); + if (callStateView.getVisibility() != View.VISIBLE) { + callStateView.setVisibility(View.VISIBLE); } if (progressBar.getVisibility() != View.GONE) { progressBar.setVisibility(View.GONE); } - if (conversationView.getVisibility() != View.INVISIBLE) { - conversationView.setVisibility(View.INVISIBLE); + if (remoteRenderersLayout.getVisibility() != View.INVISIBLE) { + remoteRenderersLayout.setVisibility(View.INVISIBLE); } errorImageView.setImageResource(R.drawable.ic_av_timer_timer_24dp); @@ -2144,12 +2220,13 @@ public class CallController extends BaseController { case RECONNECTING: handler.post(() -> { playCallingSound(); - connectingTextView.setText(R.string.nc_call_reconnecting); - if (connectingView.getVisibility() != View.VISIBLE) { - connectingView.setVisibility(View.VISIBLE); + callStateTextView.setText(R.string.nc_call_reconnecting); + callVoiceOrVideoTextView.setText(isVoiceOnlyCall ? R.string.nc_voice_call : R.string.nc_video_call); + if (callStateView.getVisibility() != View.VISIBLE) { + callStateView.setVisibility(View.VISIBLE); } - if (conversationView.getVisibility() != View.INVISIBLE) { - conversationView.setVisibility(View.INVISIBLE); + if (remoteRenderersLayout.getVisibility() != View.INVISIBLE) { + remoteRenderersLayout.setVisibility(View.INVISIBLE); } if (progressBar.getVisibility() != View.VISIBLE) { progressBar.setVisibility(View.VISIBLE); @@ -2160,13 +2237,18 @@ public class CallController extends BaseController { } }); break; - case ESTABLISHED: + case JOINED: handler.postDelayed(() -> setCallState(CallStatus.CALLING_TIMEOUT), 45000); handler.post(() -> { - if (connectingView != null) { - connectingTextView.setText(R.string.nc_calling); - if (connectingTextView.getVisibility() != View.VISIBLE) { - connectingView.setVisibility(View.VISIBLE); + callVoiceOrVideoTextView.setText(isVoiceOnlyCall ? R.string.nc_voice_call : R.string.nc_video_call); + if (callStateView != null) { + if (isIncomingCallFromNotification) { + callStateTextView.setText(R.string.nc_call_incoming); + } else { + callStateTextView.setText(R.string.nc_call_ringing); + } + if (callStateView.getVisibility() != View.VISIBLE) { + callStateView.setVisibility(View.VISIBLE); } } @@ -2176,9 +2258,9 @@ public class CallController extends BaseController { } } - if (conversationView != null) { - if (conversationView.getVisibility() != View.INVISIBLE) { - conversationView.setVisibility(View.INVISIBLE); + if (remoteRenderersLayout != null) { + if (remoteRenderersLayout.getVisibility() != View.INVISIBLE) { + remoteRenderersLayout.setVisibility(View.INVISIBLE); } } @@ -2192,14 +2274,19 @@ public class CallController extends BaseController { case IN_CONVERSATION: handler.post(() -> { stopCallingSound(); + callVoiceOrVideoTextView.setText(isVoiceOnlyCall ? R.string.nc_voice_call : R.string.nc_video_call); + + if (!isVoiceOnlyCall) { + callInfosLinearLayout.setVisibility(View.GONE); + } if (!isPTTActive) { animateCallControls(false, 5000); } - if (connectingView != null) { - if (connectingView.getVisibility() != View.INVISIBLE) { - connectingView.setVisibility(View.INVISIBLE); + if (callStateView != null) { + if (callStateView.getVisibility() != View.INVISIBLE) { + callStateView.setVisibility(View.INVISIBLE); } } @@ -2209,9 +2296,9 @@ public class CallController extends BaseController { } } - if (conversationView != null) { - if (conversationView.getVisibility() != View.VISIBLE) { - conversationView.setVisibility(View.VISIBLE); + if (remoteRenderersLayout != null) { + if (remoteRenderersLayout.getVisibility() != View.VISIBLE) { + remoteRenderersLayout.setVisibility(View.VISIBLE); } } @@ -2226,18 +2313,18 @@ public class CallController extends BaseController { handler.post(() -> { stopCallingSound(); - if (connectingTextView != null) { - connectingTextView.setText(R.string.nc_offline); + if (callStateTextView != null) { + callStateTextView.setText(R.string.nc_offline); - if (connectingView.getVisibility() != View.VISIBLE) { - connectingView.setVisibility(View.VISIBLE); + if (callStateView.getVisibility() != View.VISIBLE) { + callStateView.setVisibility(View.VISIBLE); } } - if (conversationView != null) { - if (conversationView.getVisibility() != View.INVISIBLE) { - conversationView.setVisibility(View.INVISIBLE); + if (remoteRenderersLayout != null) { + if (remoteRenderersLayout.getVisibility() != View.INVISIBLE) { + remoteRenderersLayout.setVisibility(View.INVISIBLE); } } @@ -2259,9 +2346,10 @@ public class CallController extends BaseController { handler.post(() -> { if (!isDestroyed() && !isBeingDestroyed()) { stopCallingSound(); - connectingTextView.setText(R.string.nc_leaving_call); - connectingView.setVisibility(View.VISIBLE); - conversationView.setVisibility(View.INVISIBLE); + callVoiceOrVideoTextView.setText(isVoiceOnlyCall ? R.string.nc_voice_call : R.string.nc_video_call); + callStateTextView.setText(R.string.nc_leaving_call); + callStateView.setVisibility(View.VISIBLE); + remoteRenderersLayout.setVisibility(View.INVISIBLE); progressBar.setVisibility(View.VISIBLE); errorImageView.setVisibility(View.GONE); } @@ -2274,7 +2362,14 @@ public class CallController extends BaseController { private void playCallingSound() { stopCallingSound(); - Uri ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() + "/raw/librem_by_feandesign_call"); + Uri ringtoneUri; + if (isIncomingCallFromNotification) { + ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() + + "/raw/librem_by_feandesign_call"); + } else { + ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() + "/raw" + + "/tr110_1_kap8_3_freiton1"); + } if (getActivity() != null) { mediaPlayer = new MediaPlayer(); try { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java b/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java index 51c7339da..879c0ce51 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java @@ -70,6 +70,7 @@ import com.nextcloud.talk.events.ConfigurationChangeEvent; import com.nextcloud.talk.models.RingtoneSettings; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.models.json.conversations.Conversation; +import com.nextcloud.talk.models.json.conversations.RoomOverall; import com.nextcloud.talk.models.json.conversations.RoomsOverall; import com.nextcloud.talk.models.json.participants.Participant; import com.nextcloud.talk.models.json.participants.ParticipantsOverall; @@ -83,6 +84,7 @@ import com.nextcloud.talk.utils.singletons.AvatarStatusCodeHolder; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import org.jetbrains.annotations.NotNull; import org.michaelevans.colorart.library.ColorArt; import org.parceler.Parcels; @@ -121,6 +123,9 @@ public class CallNotificationController extends BaseController { @Inject Context context; + @BindView(R.id.incomingCallVoiceOrVideoTextView) + TextView incomingCallVoiceOrVideoTextView; + @BindView(R.id.conversationNameTextView) TextView conversationNameTextView; @@ -197,6 +202,8 @@ public class CallNotificationController extends BaseController { private void proceedToCall() { originalBundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), currentConversation.getToken()); + originalBundle.putString(BundleKeys.INSTANCE.getKEY_CONVERSATION_NAME(), currentConversation.getDisplayName()); + originalBundle.putString(BundleKeys.INSTANCE.getKEY_CONVERSATION_NAME(), currentConversation.getDisplayName()); getRouter().replaceTopController(RouterTransaction.with(new CallController(originalBundle)) .popChangeHandler(new HorizontalChangeHandler()) @@ -253,38 +260,77 @@ public class CallNotificationController extends BaseController { } private void handleFromNotification() { - ncApi.getRooms(credentials, ApiUtils.getUrlForGetRooms(userBeingCalled.getBaseUrl())) - .subscribeOn(Schedulers.io()) - .retry(3) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - disposablesList.add(d); - } + boolean isConversationApiV3 = userBeingCalled.hasSpreedFeatureCapability("conversation-v3"); + if(isConversationApiV3) { + ncApi.getRoom(credentials, ApiUtils.getRoomV3(userBeingCalled.getBaseUrl(), roomId)) + .subscribeOn(Schedulers.io()) + .retry(3) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + disposablesList.add(d); + } - @Override - public void onNext(RoomsOverall roomsOverall) { - for (Conversation conversation : roomsOverall.getOcs().getData()) { - if (roomId.equals(conversation.getRoomId()) || roomId.equals(conversation.token)) { - currentConversation = conversation; - runAllThings(); - break; + @Override + public void onNext(@NotNull RoomOverall roomOverall) { + currentConversation = roomOverall.getOcs().data; + runAllThings(); + + boolean hasCallFlags = userBeingCalled.hasSpreedFeatureCapability("conversation-call-flags"); + if (hasCallFlags) { + if (isInCallWithVideo(currentConversation.callFlag)){ + incomingCallVoiceOrVideoTextView.setText(R.string.nc_video_call); + } else { + incomingCallVoiceOrVideoTextView.setText(R.string.nc_voice_call); + } } } - } + @Override + public void onError(Throwable e) { - @Override - public void onError(Throwable e) { + } - } + @Override + public void onComplete() { - @Override - public void onComplete() { + } + }); + } else { + ncApi.getRoom(credentials, ApiUtils.getRoom(userBeingCalled.getBaseUrl(), roomId)) + .subscribeOn(Schedulers.io()) + .retry(3) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + disposablesList.add(d); + } - } - }); + @SuppressLint("LongLogTag") + @Override + public void onNext(@NotNull RoomOverall roomOverall) { + currentConversation = roomOverall.getOcs().data; + runAllThings(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + } + } + + private boolean isInCallWithVideo(int callFlag) { + return (Participant.ParticipantFlags.IN_CALL_WITH_VIDEO.getValue() == callFlag + || Participant.ParticipantFlags.IN_CALL_WITH_AUDIO_AND_VIDEO.getValue() == callFlag); } private void runAllThings() { @@ -321,73 +367,14 @@ public class CallNotificationController extends BaseController { } if (DoNotDisturbUtils.INSTANCE.shouldPlaySound()) { - String callRingtonePreferenceString = appPreferences.getCallRingtoneUri(); - Uri ringtoneUri; - - if (TextUtils.isEmpty(callRingtonePreferenceString)) { - // play default sound - ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() + - "/raw/librem_by_feandesign_call"); - } else { - try { - RingtoneSettings ringtoneSettings = LoganSquare.parse(callRingtonePreferenceString, RingtoneSettings.class); - ringtoneUri = ringtoneSettings.getRingtoneUri(); - } catch (IOException e) { - Log.e(TAG, "Failed to parse ringtone settings"); - ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() + - "/raw/librem_by_feandesign_call"); - } - } - - if (ringtoneUri != null && getActivity() != null) { - mediaPlayer = new MediaPlayer(); - try { - mediaPlayer.setDataSource(getActivity(), ringtoneUri); - - mediaPlayer.setLooping(true); - AudioAttributes audioAttributes = new AudioAttributes.Builder().setContentType(AudioAttributes - .CONTENT_TYPE_SONIFICATION).setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build(); - mediaPlayer.setAudioAttributes(audioAttributes); - - mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start()); - - mediaPlayer.prepareAsync(); - } catch (IOException e) { - Log.e(TAG, "Failed to set data source"); - } - } + playRingtoneSound(); } if (DoNotDisturbUtils.INSTANCE.shouldVibrate(appPreferences.getShouldVibrateSetting())) { - vibrator = (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE); - - if (vibrator != null) { - long[] vibratePattern = new long[]{0, 400, 800, 600, 800, 800, 800, 1000}; - int[] amplitudes = new int[]{0, 255, 0, 255, 0, 255, 0, 255}; - - VibrationEffect vibrationEffect; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (vibrator.hasAmplitudeControl()) { - vibrationEffect = VibrationEffect.createWaveform(vibratePattern, amplitudes, -1); - //vibrator.vibrate(vibrationEffect); - } else { - vibrationEffect = VibrationEffect.createWaveform(vibratePattern, -1); - //vibrator.vibrate(vibrationEffect); - } - } else { - //vibrator.vibrate(vibratePattern, -1); - } - } - - handler.postDelayed(() -> { - if (vibrator != null) { - vibrator.cancel(); - } - }, 10000); + vibrate(); } } - @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent(ConfigurationChangeEvent configurationChangeEvent) { ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) avatarImageView.getLayoutParams(); @@ -509,4 +496,71 @@ public class CallNotificationController extends BaseController { } } } + + @SuppressLint("LongLogTag") + private void playRingtoneSound() { + String callRingtonePreferenceString = appPreferences.getCallRingtoneUri(); + Uri ringtoneUri; + + if (TextUtils.isEmpty(callRingtonePreferenceString)) { + // play default sound + ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() + + "/raw/librem_by_feandesign_call"); + } else { + try { + RingtoneSettings ringtoneSettings = LoganSquare.parse(callRingtonePreferenceString, RingtoneSettings.class); + ringtoneUri = ringtoneSettings.getRingtoneUri(); + } catch (IOException e) { + Log.e(TAG, "Failed to parse ringtone settings"); + ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() + + "/raw/librem_by_feandesign_call"); + } + } + + if (ringtoneUri != null && getActivity() != null) { + mediaPlayer = new MediaPlayer(); + try { + mediaPlayer.setDataSource(getActivity(), ringtoneUri); + + mediaPlayer.setLooping(true); + AudioAttributes audioAttributes = new AudioAttributes.Builder().setContentType(AudioAttributes + .CONTENT_TYPE_SONIFICATION).setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build(); + mediaPlayer.setAudioAttributes(audioAttributes); + + mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start()); + + mediaPlayer.prepareAsync(); + } catch (IOException e) { + Log.e(TAG, "Failed to set data source"); + } + } + } + + private void vibrate() { + vibrator = (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE); + + if (vibrator != null) { + long[] vibratePattern = new long[]{0, 400, 800, 600, 800, 800, 800, 1000}; + int[] amplitudes = new int[]{0, 255, 0, 255, 0, 255, 0, 255}; + + VibrationEffect vibrationEffect; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (vibrator.hasAmplitudeControl()) { + vibrationEffect = VibrationEffect.createWaveform(vibratePattern, amplitudes, -1); + //vibrator.vibrate(vibrationEffect); + } else { + vibrationEffect = VibrationEffect.createWaveform(vibratePattern, -1); + //vibrator.vibrate(vibrationEffect); + } + } else { + //vibrator.vibrate(vibratePattern, -1); + } + } + + handler.postDelayed(() -> { + if (vibrator != null) { + vibrator.cancel(); + } + }, 10000); + } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index 46b5aa4da..f960d8688 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -1395,41 +1395,34 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter private fun startACall(isVoiceOnlyCall: Boolean) { isLeavingForConversation = true - if (!isVoiceOnlyCall) { - val videoCallIntent = getIntentForCall(false) - if (videoCallIntent != null) { - startActivity(videoCallIntent) - } - } else { - val voiceCallIntent = getIntentForCall(true) - if (voiceCallIntent != null) { - startActivity(voiceCallIntent) - } + val callIntent = getIntentForCall(isVoiceOnlyCall) + if (callIntent != null) { + startActivity(callIntent) } } private fun getIntentForCall(isVoiceOnlyCall: Boolean): Intent? { - if (currentConversation != null) { + currentConversation?.let { val bundle = Bundle() bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) bundle.putString(BundleKeys.KEY_ROOM_ID, roomId) bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword) bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl) + bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, it.displayName) if (isVoiceOnlyCall) { bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true) } - if (activity != null) { + return if (activity != null) { val callIntent = Intent(activity, MagicCallActivity::class.java) callIntent.putExtras(bundle) - - return callIntent + callIntent } else { - return null + null } - } else { + } ?:run { return null } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java index ed990f941..0f1ebc214 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java @@ -94,6 +94,8 @@ public class Conversation { public Long lobbyTimer; @JsonField(name = "lastReadMessage") public int lastReadMessage; + @JsonField(name = "callFlag") + public int callFlag; public boolean isPublic() { return (ConversationType.ROOM_PUBLIC_CALL.equals(type)); diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 76159e641..82b80f0c3 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -127,6 +127,10 @@ public class ApiUtils { return baseUrl + ocsApiVersion + spreedApiVersion + "/room/" + token; } + public static String getRoomV3(String baseUrl, String token) { + return baseUrl + ocsApiVersion + "/apps/spreed/api/v3" + "/room/" + token; + } + public static RetrofitBucket getRetrofitBucketForCreateRoom(String baseUrl, String roomType, @Nullable String invite, @Nullable String conversationName) { diff --git a/app/src/main/res/drawable/ic_comment_white.xml b/app/src/main/res/drawable/ic_comment_white.xml new file mode 100644 index 000000000..74f759c9e --- /dev/null +++ b/app/src/main/res/drawable/ic_comment_white.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/app/src/main/res/layout/call_item.xml b/app/src/main/res/layout/call_item.xml index 2ab2db2d2..5e9e49bab 100644 --- a/app/src/main/res/layout/call_item.xml +++ b/app/src/main/res/layout/call_item.xml @@ -26,6 +26,18 @@ android:layout_weight="1" android:orientation="vertical"> + + - - + android:layout_height="wrap_content"> + ~ @author Marcel Hibbe + ~ Copyright (C) 2021 Marcel Hibbe ~ ~ 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 @@ -19,135 +21,176 @@ --> - - - + android:orientation="vertical" + tools:context=".activities.MagicCallActivity"> + - + android:visibility="visible" + android:layout_weight="1" + tools:visibility="visible"> - - + + - + + + + + + + + + + + + + + + + + + + + + + android:animateLayoutChanges="true" + android:orientation="horizontal" + android:background="@android:color/transparent" + android:gravity="center" + android:layout_alignBottom="@id/linearWrapperLayout" + android:layout_marginBottom="10dp"> - + + - + - + - - - - - - - - + - - - - - - - - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/controller_call_notification.xml b/app/src/main/res/layout/controller_call_notification.xml index ee7c6dc8f..4ed9351c9 100644 --- a/app/src/main/res/layout/controller_call_notification.xml +++ b/app/src/main/res/layout/controller_call_notification.xml @@ -47,7 +47,8 @@ android:visibility="gone" app:backgroundImage="@color/nc_darkGreen" app:placeholderImage="@drawable/ic_call_white_24dp" - app:roundAsCircle="true" /> + app:roundAsCircle="true" + tools:visibility="visible"/> + app:roundAsCircle="true" + tools:visibility="visible"/> + + + android:visibility="gone" + tools:visibility="visible"/> + android:visibility="gone" + tools:visibility="visible"/> r7~5!a%AWb(Kah%k&v_vnUW$YL$%K_&%+6sl5 z+i?hoGF1pgG)P1frS^ZV`+1(-uH${b_xry0{r>NJU1wj(?%DUgr{`X4{qEmd>wd=1 zn>Ug03Hv&r$Gn;P+~y$9jJG^s>8u4oc!@Xc6!S^tcpiOokvAT{DSzQN@snKwlBU~T zruf<=VCv}zA5U2jIRCrxOXpcES}Uab1z_{esvHsZUYQzP%K+?WdQ-lxSIulfV71zGf& zV?c-!kqFfeEG-=JHm%OALvb57W1CETw{DYtWQTPF_dK0e-sz`jL*5Q7F;ml?s!U)a zL~5PZwsRlVX&v`V7#B5bv+!8dtlPqP)1W&&GcDHW^e>J5X~Gb7VeAA~n}pa%ch~N@ z(}LcosXh<-F!-~@TD>7q3HOVR!vgvO7w6E6STj=}Gb{@t!I8V{YWK&lrg!g~-u-Br z-KLvc+iv%Jb${sYFtPVQk4eLGCV6;zczcKV=PdWn+3TM-nZB9qKlOrte#;*^bPFDd z>EgJ%i*R>m;qIb-yOq|iKgB0=a7tpVHy8EW{(98*ZVPtLU9orGrUS?KA1F9k0oTBD7Z3=Le#;aEXR^Vi?TWt9SX`4-rj#bX8*kdL0K+Z2jZA_E7~4ZJ)Q6Xu5~Cm zq;sRgOenYD!2#8TCsW|NQ|}!pKAzeR-ffCJXqB8giK&lP8kChV<51RB$CvPpO-~Q{ zrK)?wyDxJNy}zrD*PvbYmpWZOBDile9~<2`{G&zKc9mJ}Ivwluli9KzIzPqqyeW(| z+qQj#Yr>id?j5Y+mi>RApfy&4F zsHGqkF+XZ3-nh!H*=;)BSNO^PXbOv&S!welpLQtHjI7ZIe+B5UensGzanH$VX&d4S0GIyEY+SPUR*{)+B*xmiqXM9zk z`})ISxlAQW( zsFsqm{Z+s1U0il|ox0nSN>0N5=(2>LTS-pL{LS%Eo8vcbF4?%;(EhI%Rm>O%-UsclY`CG~4Io6iQ)@J;lCEy< z-E-U>a@@OnOd9C%U%hfrv|wWQP))1d|FiL`TT#F7UNwK~5UrKvQ9;{R-rd^y=8|`M zy*IAV+|xyA#kT#!HZFll8&32gJk!%vckYK_L7~($mnM+J+3{5k0j|aXZ~d35yRc)e*)W znw{0@SY*&`$2W)8Ja9=k6!gGRxXT^?T$<%lc4$(Tqww%ruw8sp_-D5kL`8`#;S-(A zk4`QRhq*oI<5oT9e(=Ei%e-A4j+v-G+#_e0o5xtsWdHkYraX@Zov*)BpK0Z?6#lCw0DFdD}+Psyv~p;IW=L z{u6`!z4eFZ8F+Z^9iFqt*K=>M&mQ0BdBg4p56_A8b_@3P+T&fY+;?i-*u0uxkG<>Q z)0Zj1-bRZH`bI-TKJclh?-Rqt&%Y1PuUj{d_UgWe!~IF#4?TjR-TYjv$}`y)t~}Y} zU*PHeq}IW0nfHAU->J(yrcU;KQs+IjHrUN0%xjM?ep=^WV7UHCZrICZ-riXAxXX!gW7Ja?3%P;@8p5+&Uo~0++WmNSJ_norx*n<3H z<@J}!E~WP=Wvv#zLpc+umqm;FY_8=CnztROUeW@vQYg>5fu z!)`AsA7Q@w$frA(wpBFTSlZCPBxHM5rItD@C|n!%N444+pY2RG>JzcvQk|a4o!qrR zAH?YlacwhOuUARzoI5V+ak1@PUAoWG8d4k;dB;(BG;;3{VfS@yr)kFh-rjY=>Ey|N z3%lp)x%wTAG#V=Gek^z4nA17NJykjdjdLwN1lkQg$n8~fe3_2>TMIDf3xADmH~Y<@ zNUi=h9oI&>+VtELIZG?y_sQ;?lVc~%_WSis``KAXBioO*`4v9!@A<}P_8muT{1DT4 zF7#zGEKsP*(K-RD5(=;1add&I`j=Y6H^y|o-rjZ7X{bt=a2=}ZetpVNm+pHbXLT1| ze?03>>h--|sz;M|x^9K;dTn(~$z7-&_xlv4@vX_VM%cK~Xd4IkhQCAY(^;n+llS^9 z{I$6K?Cj(>xqew`@wp4f{`%(WtUCjXjpp1RSE4AVp13 z0kKu5V!PJ*y3tydPB#wybkE}Vs33Qn8;92HbiE!Gr0073(3%OZuKVGgi!gGxtLy1? z?yjy0$so2GD7K64MNTn|L8T+IA(E?0?Ea`4=H}r0s)0J2N(zN(}n%`+Kma`Jz7ra~VO8@+Ozo=FHZ~rnNrJgTI zp-g=3t(w*F**~mXe@cn`2FQChtkM2uU1J~FzcE_t?(Pz|XGG5ui^$#XuBSb+Tt>yk z(X&>7n0k1ZH2`+ZDIEtMzi0+DxK!n6W&?J#Tm^$)iZb6P?v;cquE;>@}{{% z5g1Ks-EQ|SJ#S3+d!M>5ch;%oVn8PYliSZe?NIV8s1gfjB?J1NaQgZbzeGpXm#cW> znYIqmQ~y3IY4@_p{fwv6Y3fQOqK!Ro9179_0UqYs6RM$jc$AR6wst3b?P#QXFXmde z$O#~#SPrha-eSIslh$z=drcUJ_2H+&H~`(&3|fhVPuX{E^IYeC! z@z>F{SPQD;=IQY3kZ-meWHCUHAZ%EwZL95{|%$UVqc#cZ>BJ z!s~~UwS;R-kBt$ojog#fbN$Aj+Iwjl2msCMA#~#Zz_+&F$s?+2>h44|^M|8D*Q)9L zB9&gecpi2;L*}d>HOx+BlX{NGt!GDK_nrM~foT1W5l^oxFY8eF<9F}->kZNSQSbLx zL}Qfns(N3ikL#_T6)GM6*2k{JyW+S=IJ z3We4}q0mZbZG-wzjtLv8~V!J|1k`!K8z+iOF}iy#KtHjg_O7osA6) zNN8h=BccPd5jvgA_!7FKWc*L%HM8~U>;!&yrk$wUwA+Jv8{Vqg=pcSE#G*SOFjyQ! zI%IXzC8giK2E16^xH7`WbN;od`F9RDl;3r3Dsm1lcR5YSFGNL^=%?RKw=ookZPF>; zwA*4VQ6-&4Sa;&Nmqe+<}m53(^AfzJ{{DcLys)p@SeV`ObEckfeVq_^Q#D157 zZ*ulBWouF?AojfI+v(PZE@7M0s)ludI{8G!2ug-pB@>bLx+|&i{DDMSYELv{1YAjI zy_e_!`JSy*9N`}Ztv1w3+vLR&BZ3{kp3)AmJ=G+`BskEjFcorD>EZaWXR1&zf=E>F zVFxO)0~(;Lp|4=iEuaIWAz~ps-*jhPB2hoKdfA38?WKB>tAa|xRS46`ba5kAD3YZfAd*d|=Ep1_txL4YAc6(0 zJzc^K+q|>ZexPJQ0*M9`Tpc})SVRsP6n$cZuEd;(y8Wq(Nw5&$C2d-zP4pHO)IRsw zr*@A#!XDBa4s(M&RE8Ul(-;(@MSfI2_Vw%RC6XoNGa+Vw>LDHShrE@JPbWSp_`rYW^7VSwQS?DC}*4k}x@Ku!k4d zA0y;O-=izP_BE8K6Cc7%qEKO}i6x)}BuyJMV5O<+Z_zCiY~>$UEu{CEott`CK7^^T za@$)088-c9O2IofL#Z|S%1k2LGOkDyVowL%6kkImAKG20`R&CdqAXG-n%wNj7nv4z z(W&ksPh@{J@Oqb?625ps>kcLnkrGbMyNn#ZL_B=5!;_lbM1>Tpl2CeE=o0J4EithK zs|+on)AK5IW$-1Q3enPOp77>XD~5viBF^fN$LSS4y64~MB2u-tChets=q8l~v*Jw; z6s0z=4T%yDnO~iteUAstGc&{voRtJxr|c*d+Aq;rOF&$-bR?=taC4?$$&1iWl?8k~ zqQ^tc81l(A#pCqGWiONGkfEpv>?F#xqs{W5 zOjM9ykybST*Q4L7M0b#oKcB4UeWRlr377y$GUEJXN@aAwH5yAH*T?!u`o}(tY zsbhD}IU+6dQ~a*-1|I-S@L6G2208Uq$@E;tiijO0U3ZP9d;^Kmco%YRURP03zMviR zod-8%4bBdP&g|zPGQn9_3qBYqe$r{eP29&@xrWIdPan-N%{QZ{Tr@2nb%C?2hY32KyE=xB? zY>3Hd94xzgT=v5425HZdQZ&m#PvgfcYV@BGPlO+uYOr6e#e0UL%(O8=w5_0-G zBIX;ZaLc>dI^s7gpLG;S#6JpUpC_1#`O?1Mvffl%-CapU({=@~zIOW&;Hyeql0^D* zq6PW_3@+G50l)_m6Ojg%zMcE@+67i=PP>P~$;OKYvX_-+!Mt{4jjEWpj4g_GoI@m3uLPXwK-A&`3ep$#GmrxEAY=?QuLv47 zi56;>(D(5;OD`o2vp@-`XtVuP0F8286>*Xp%H$VgT}Vf1p~BcfRkA2T3{@7!C-6w@ zkj*}32|1cz(og|*gpN{O#e&8eV9cQ?sow#E0z;WnHt>2EKv{zYQl(ZM1#~Ngl7%Zf z@+9h(Q6Hnv@xZ3(f)~#n#8Edg-rXEPHlrFx86Gq^?M0Tz7%SacO=e(^C&;0@dt zHQgE8SE>ZyQQ_Xh=s7!yJ*}2K;*+BMbkp*7eZi&$G97JcCh3Aotm;I>KdSH7^;t%? z@{QUl_ETX!L6sue!n;H?Z1S?13-+o?+{kAhVYhr>i2huilJukbV^HgcoP;t|BHXuTi% zOoDyUk2Q8*4-*h%XV5Uvax1-o6*lPCD3b?m9ADJ+-@6!))HONyXT=6_1CVs2QFfvO zxfxYmRDaG%u#9|GA$G9B6?Fy(7}gkgSX-4?MoAZC4}hK4SAo2XFl25ef?bs~6(IIE z@YQsFt6{GsHj=1vb0gnm()tR$5|zmi;cL|m?Q;e8N}w%i7ZC<^eC(z&N?!_Cdjd3^ zl(Cp@F_&L|Xdnb+JBgpv8ry(c!!9}*dN_@IZUD=raL!9sD$trdpK`O7;Y}h zv+{dbs?H~sJc<;LY>jUgyat^M z`&9+#$7)_43w#FwMh1OW7v3gMV6QIpgU zZ!!YlKSoxu>J?Fj&EucZG>iLLOL}ERP9&_TaM;!-)!t6e&zCv9&9NUparFFA;KHF> z6p=1JY?}X&+V_3We5rle`@PTQ)gQ0tjCi#5a{-0|I4?3G624?9kA!|bn-MYHe!=UX zi3lcRLXW|_=XGnF^wwlDUkd@DwA-&}nNjU(T#?1!B@^P55e{B?>FDyB<*OX{cFy@H z^~mu6yK@;4S7riEY&rwZBlwm@t*xr4(GEf9m1|YIZvt0~dz}gVkxhHrswaKC&IhE5 zuycIpyb=gszz^?*_LF!%;W;7boau-?qcByfS6QtQq9!qlCVk?of@j&MeH4h)$VG$Q z6Z0DMzKiQ#VZQgxl=}^4)-&G8bgR?r3`w>6L+$BW?=Ra5){^RwhV-^Ms`?q9MqeGd zWyUY7-WUHCw}0QK=(vZ_CVluB}9kK=<)VICeAR0n&%scqb_s-*UU z%?2xjjt13@dw%&bJ)`!xMb~pBMPo1VmS=Z7Y@tN#UQVxSO!qhv>iCOn(dajS+%I_X z_DWOzjn#dSHuD79%vKz&)M;mXn0P*j`@Nk77mkrT=VgEj@AOCs81Q<%z%z0Bms(k4 zfvmAKF@#-9%pP1(p>f*cFRI`#s^BlG;9o)&aE7wkDZhdP`6Owj7mB;g=|+`_|3~_`(obf3~Jr z?6GX3Ci&4|>hhmVb_4r+9FfL1?0w8J*yv3f)utdW&LoBh*q9rJ+}?M_+U^}m=z*kE z!GiRHfg%Cma_Ag2_HvSs>cYf49uiXbD*J0fZKhZo2mP3B8^cUHd+H-#>V6`(2DT2s zS#qs5n|W-ta>YWPWP-+u1=}y0A!)`_tgi)>WNG9rK0m41^R(nM2Uq}hwGtq#Xmx*W zl>gNmWoEY|%0@2$vs*&Y5-a^}uxzkNhYlUU^<^Xc&)gbWTMk5@p0$lFYhS)!@E05k*L?5`PYA2s8ksP?_V znXpy4h5@DNWlz^kp6dHZ*ZC{J!@vwJ!VwD;pKX};lblIZ5B#{KZj|KsSLJ3ZF%RIL z1@z3A1 zop?N?VGtny`KEc6^PK9&`g&#SK6#y$d(Od^m-)vJOEk7zO&h+o&4AvYjS52YRMdD; zv5xsmuIRTr)>P~}X>3|A^B&W0XKL>Id1dE)mN*K0{f9xsuiJBc?JetSH;uLDk#0eD zaW6N%sxSF8gSnvBF)O*lU1WkyEvx8(AJN2EJX`-(Q1>SL^@0FMJu#NEryaLLOD| zEj(RN5PWAx`DDzT;de_u+*h>*CXG|d3N#+N6+>5r5y=FbNb@@rrx*g~4}J`VrWr?< z9(G*PsGkR0>xqxk)2vty4_a=8UU64k`OKV$6kbDGPZ;qUl|S_mnFXxzal}bwa&_~m zg}R=Wyk$TjO4PZgSpx%pKrs|741&#}d#CHZ^KMl0fe9d_y-2=yXBj*lqJe)7=ayjFko+27isZWo51HIEfCCQJ#9}eBCE7@~ zEF2f~he3?MNCietR~^g8qK6gBeK@W|)Z%tp%~+D7EN85^Ri^;y9!E^qCzbvM(Xme* zm8HmUL-%OAZ1=bwXu&+>s~TU&HxO;$)}dMI1~hnQO`YTtAWndUmL&*KfTw>f@1_dA zTB#~38AYjJ=_(e!zMOuB!q+}>?Iwq>6qeT4t|ZdW7i3LOy1eJz;IVNA&GNuG`ypTg znW`e^U&#&R^Raj6c5aph>|H4fU$x&oe+o4I1YLQntabsGDhA3)OA;vEx8qD+883>_ z1YC`9gBbuT0=b2?Sl~w_;)V-HmQCyoix%`iI;#R%K>&&h$ zsN}I9@-A+~`lL-4&B)H}H!oE!vvv^i0Ix&CbY-zJ0FEA%f%Vw1N-glo6}D8IHhJy!Ih8M1!V&cJLcc{?(mDyq8&}8-SdYN z0=^nmqB#tK%(b%%UOx?qC&U82YZ2?g*UQoAgX9!?QDyb_m6#7MoFVhSjwL{WWn^6I z#(7c2{OPspM|KsR9Ix6{{+)7ARaChr>X5g6&8g>A5n^fBH@X`ui6(~<3UX#OlVCCC zg_9qy;Ymm9zB^l0)K<4h8>pWNnz31NTjzB=TKuA*>!~$=ESNvwgu#yb4?Yz!zmcs! zZmU=y^(^yNPm!9mAS3%i@$#{6M!S_Cc$-&Nx+(nNxOvBUG~hbsrGi5wS>{7EHqv+98FYxGRJG_FI;7cq0mA?M_w zNTmAC+FYHUrD35&*47k+y=%Idy}EMAkuPg^G~8K{?foKXU){4e0|zNU{d}sRN%7hK zWU4y&l$E@MRU4<+f9Sds_&-LxKY8USsSUOD>wVZct^VZop+FXR-~Svy9aoZjb8WH)pd*}8W`Xq$E2)Y z1l5hw_Bdh$b(r=|HHlLdgQ*4#&{8dO3VHi@iVPZx@?U_iddmxCm=S<5P-6=fKmH^H z3thGIU+u_?5lE><#(_7(X2xisXC-#1P56qFKP(0d?!(0QaT=LH1q_Q4abhkJXU6t0 zFDUIGRs&-|?<2I_j0i-qMx?>t^U#ZRH^31c%7{myU`x#nLqtr#Moa#5q)g);nIs=F z+(T>^LM71#RgPm=nRD?@1Hp#JfY+Ow8)%q|l*9@f{K_@S>iiVd7neFIi{z|Pa}NO& zP+-6M2vSmY#Wz99=BLXttCP)>1?aWR*T*?zdloIkBjTm>#X)V_50x%Fmg@KJu0QsUm z-9Mb=g-#^xzlZ5La!bd*j3;hKns_9N8&2~?;vM|#L}N}@uRhdYG3J!RL!ZEh)}Bn4 zKuHMR14{*7ET4w;!_bAM80R2I2%SY(H2ShWv4|4i_8epk+cSvr_}h^FAOsjL8@b7pm{}@V0?g2j0)Di_0G5m zK~QT{ns-U8JYbj`)>-X534*Q0M4kV z7M)!0DD+1m+rQ=knfo>v6STsTO5FFpjX&0ldz46-3LBvL-!C2ln{6UA)N z8qF6%B=XcegdF#Fd#jfB{$88ttEH;Ap zvCK=Ks9gx~y0q9;1+;Y_qd0EKw`ZIZ40i@pI;8mP(ag>eHL-@@?DL14&fQBonZNWW zdZ%tKwC!injlxoJo2q(kD&IKrw=36lDKCaPCPr1DwSJ-gsmrpgD&pJmut6bDJ6~>7+bfM19Dbs~Y{Vq)GW+XY? z#4|$R(-9lod_HcROjeJ0GWMkT?1scrrB0+~%un?yJyume zYX>=Zr+?0vT9ujKM`qkJzJMX+{x18)#;Xi(O9z0U5qChLi7|!$exT6?C{;j&LK;5< zRH`)~K^sUeun~4=z@n9nB_cz(HVCnyiLsH;^nZ+Z|II+73b``(zUq{==_%X03|5nh zU)Q{7_y}B`EcQ^RkVnVeei^aN*kK{Qu^~$5dVFz99P>Q}C{N&hKlZk6_<{vxCrnIC z0^ns*;J@ffTH4h9w_g;eWF-HdDRr_v+KDDxz^xy+-KF4o8}YHU$?2aCo-^;)S$3j) za_x5&jS;ds_=$ntbw5y6oTdT|fI7_tG!>Hgwa%z3CO+Q2c25F?7z1K5OaGHb#idr5 zdPE1J!Pe)z-^7D}dVXt+f!rKIu!aB+)Bq=PSJ|q|cd#wBX=hp=7hJdec)TH)(QpQV zDa6WXGH|5Sc``(a!Pa?n*zS>_eGG_Gs~$qk3f7pouM7C5lzKO5eh<3Cy7h=LsE(qg zpFuSbn_OL8680W3B7?uE8f}iL!}`Q0`eT+u|8&1{K3LCw@(Nk*0YD3wO7b`*PZjC7 z9@ajtJbMDpE+>g9^iVNWraepDg{DKc1OOrZY<1ERhm7#Fm8us*opQwewl+?wlu}n1 zP|Zyvl3c*XSwKnh0(c%8P?ves5R7L!#J?!iiORke;`Z4dyxT;ohXZF3H^xY^AOrX~ zP?`{<*jtG})KN{<3RRK7UhA7Q1SPI=5OAL={jKnw88JrK!fXv#J3mK8!#JsAX5M&W zgnNS)QSXdlV(HByV0X)Q<~HoUabFv#NaGe!?=0%36Dp4$+jMT3U<{JWm`9+9G34Qy zOqx<@?JxSUfC09k=(NJ!5|~YhWSmUn&hu+#dZPVQq21)}CJVh0j7S1f+an!kzIEah zT@i1BfJT}f_jC#30?7Ch#Agt1p8=~lB`n{ksE{^|)V7qNft&B9Jnm@$%~JYMHp3h^ zAXQo^y+aDto60VeqBJ@&XzWr6FgpSoA=L_imN{NPPwl!?1`LdeXZmo28zYVrjJG8 z?Q?Gd+?P(M3!+)EU=b~FzgXXlSW0VF8MdtJgf4IfF(qP)Zze4rsZ_bNug|I?$H7+vPhQ z=${KCwpd-M&l!k=UDs}3>iKeISh@1uB8SirMZ?}#(v)uletGrO_!MuTEVTqla=pun z0y}{*vI)2e3>?sKdXQ8JQ`QtFhFM`${fT5&;KFZXxU}>`bTBG%Q21O)xoKd*HftKn zygh*%M%Pk*Zf@Y_AtlWv-vl{#gt$7b*%>={TlrE|hgcc`_euyLbuF%LuFX}X?d zVAopzg@c1L4)<@G@k>_p3g5Jx5!;M|O{`uw0uvHz1H5tVNs0>6M4R0IHlfYyp zm56GjPWt5FeKCb;SFQ!^mculSq`KMT)g-5~l>2Z2Zn7F!1E|LA>TJn`h_L1eHvmm| zy`ReVfy0_GO;i3C29_&(7QO&UYk zFU9ThD+r?yQ|!1(T{MTMb>z9@#R-NPC$<90v#@VFds}+O*H3>OsTUqOTW07}9hMPs zXvvMTOFHkmW-qlGzDrGyNJEQimU-s|&o0`kU)GTRp@G45e-{G&4WPp!!wSgCh8H43 zjA`zBXJyI%XJlwAbhH#Qp~b)z$}>wbxfH-47iCRRq7%YEW77`*+!%ttF=7Ae%T#d? z@_KoNO4X*^)nY95OXmbXw&VeW_$iINWwp8Uz5O5O*ZK9_} zWAY(Jb}4!qQJaIcL46{YeFmr)yVEM%{HQ8r>TqXH?K`xM>%Vwa^?3KrNsr+i%~(f`Jn za3@?Ea9owlxl?=O@ccS0$msw>+mV|kR;RN8;M3<_!z`7gH(SO*WY!vin1^{ce-GSv zeKz%!r~~|M!Ji;h1$!fg#^urRXADcxT?Xi)*>BIn0f?28KvDAUjF%57%ugQ}Q*xpm zFmm7{YTZ%J_*C`8ydU+n7w;f17qX+ErwWsgrVmSErA{2ZDEoN*H2`S9-w6)^cyDRMVIGqz4!0g!ZzUKOP?R*5EaVf*vp)}S9BW4U1O7$ojCNDOdyCqIg*mHa zu8)*qEen9Ql;3(*IOLd`MLIuco@~qO4?-!zO$vpztb72{LDteKgd&~W&MypAfnvk- zRHV6DiwKmIHq9MKEKc9-KWKrkE1)sJpiq!5dpX*n8R*q#ARch%kj;OLtB5rOSOk&( zT#almCNGS*h^pG^tZ0u*7 zW%e_6Tmk^~NUqwTVO$%3?SucN`(U8|^!NFAEnJs6RQWeZ_mBYOA!JoQ)1-O46S|95 zJ@pLm4tXAImSTIWZKwJISwqB*{TLAkF)a+#v-%W&tA=yq2JXn=ZQN`?fE4@^`g<>V z#)4=Yd06*w8(H*PfUF=*l!csEd?o4@ek~czlb+zUW(D7N3-lY-sT+Bl4-sy1RuH?F zR3g28Ey$9@gqu-TaOR2yn1O+s{y3=$sO_vIo$OODXr=`leE5_Wj4AUZGu|k5Jy_NA z{{9!40}R!8|G^VX{lpMKN&UqA|IbcI`B&pS zte?SpumM=I<<_I(zTNcr_8n%v-j%uE>8E*14Nh#nK8rhZuThB!c7Wx3DI}ALX(*ij z+`z&%oCmP(+`fWoZ)eY5EI&*|T(V-W?7{)P;(#-cU;R;@cB15$BCqTN4kybGTIJkZ z1qp1x?xA^7G!U3>iR<9Q6`F22eR;(0#Z11Vg5p)cVx`VgRSBHavG0*rs1ihA^^G;AX57w2c8s9z5{|LvPH>|C4wJD|7R;3#7w21T@&<+ip z9C-R4)OA&d11RJ+p&56^Krzmjt!sWO57U25U|*{+dkpfl0Q3!J#yJ_|j{+`77pXuL zm5c&oMcc|9#Zq#OV$TaJuJh!Hgx|2NqKI>IiNz}*DaUQFlHkb`ik6kZR1El|F>dZR zZ{504%9~GYcyL4mH-}*180g8w;sxcqzzG>w=Qyt{(Y{CU*aw(IML;LdsAEFh&Yf?% zuEd_kBE{@*?5BcxMRh3UoY|=f;LHY`s6x3VdJ95#|SHdbBC zlk!lz%Fq8*2!RiZ1~9T*`S#l4le#P3$VQrnSDs4P=V{(%-|H7W4yFzx;6|BG+Be5I z{dF?RKM|X-;e?Yr;sT)jAG(wGKL7ChKOX?v+KB+y3E@Np@ZG{cYbzmU$bdH&J-V1R z16Ls`5Iz|M$uf{GV=T1!AA5HHw_p4xPBMe_Vi!JfXn(8v^5rL8t}2l`4y#LKvhQAe zP}a1pQ=U<&d~Vb7OgJ~Ev7zpalG3?~@gt8d@&o6p)ZSw9{>XXx7aCvnD0y?mG|vtk zyt1!H0?O{r3J(vTp*jtNXxo;w#k>!C8WFNf60^)*OWeCi&!@GmSlyVJ?o_?r&&RP+ zy2m!68eO~rDgC)9T29ED(GzMHxr1`iM>}OHkqqWlvbG=01Qb2HrVr1KH9eK9eTZJK ztJR~&>;7X7N}b1h+Cq4_DeLz;j?pOcBR}j#&^&oIhN|DFTxr^3U z4OV7C0)1wDi28>el-2u{0n7;G0HryUy9J9kzi-e=0?|F%5X>gm!CFv1xaI_28PYpI3Eeccs58>L6!CJj6C4KtF=eJ4W{6($7XAu5^3aEwfk>WVgw4i2^azg?VyzW)WpKbZDtHn z45LsK1U<&c7Y=_52~!%55w&yhG&^?p)Cw-4ResD;b+6u4ZDLtsSYC?TiB?X@kuOYn z|AkX>wZHj=v#z2h$je8t|!Q*J8BGKU{NC7VaZjmyc$*p^8e=&kp7=Xcb zP_iXp5-Bm2dKqAlvr3P&NI*;|{&<8EyXmQg3?vbcN5a-pF#{tTQ!KfLo%C64e#UabUBFK*mcF z2FZM)q_BqH;>S-!hj|~ejLs8S2p|9$3qhIj!Y@0NJ_P(E41 z)S|mYnHXANecLnKI&NIAYveg@P{obVy5#LseD`=v{sN(WFE?zcgXq4Urc9Knf|GS3 z1W4zw83Jxb-dy zhWhF2pF#xwx6lsD8tQhU?IUZ5QDJVs?r8JB0FBn&oGopvZ6R&iwi}#{3^X7;P}tTQ zJ-)Vcv!M=2h*0R)c;$!_s{wMe`^{Q3agN>+iM&*CZvVU(qBs?)As#( z{!z-F^SInOC==DgD>EdtnaH2mL(N@pEu^J zr(3M0`HV#aLvC@oz^BdBFX-W^L;VPBv!dqxp8v?Q@IzQ>e%~`;HS6p14>>fPntHD3 z^A6eP9R({P7q}1(c7ym?PT8<}ku`wq0n42SMmWN@Z}6Y_71qtd#4R2Fa`Un`IJekP zIaVcp3l_}HNHS9gS1vsmLU9|3xBw%Q-yOM_PUDuG14C?p!vTxXI+_o*y0{h#zbsP% zBEWLdmz}!z&>_%D8zL5?G(bQy5-SE_K~w3m$`oKJDnph9U_+LAUa@_kEl+ghy2$$J zM3ht-21j#!^p7Y?3t6FL#HWWkx6*~Cu#tt=#Th0f>a2RF>Gt4uwWl3y$%P{NmM0TF zyZi(*Bp^;qqFf1sX@w9(SmTvZG%IC8`j}&QzDA!v#qu>HP-8sqwtIT|PAy23DCC6_ zUG7%1c151$!$vN7L3TBmVA2e~0RplJ)B+oCfOG-ToZ-1Qnhpqgmf#_z44E~PRvfE= z2ULqCSg<-^f`E+bbUkQfyN8{GNOD4{;!KCwAE$;&DRv~h7pulEd59I+; zeVO|@+Z5crZ+UWu9-ia+X0}o8LuR*Ui|Crmmo15h>!o|7ZU>_0W&RWIx-Db zT>d^;$1*dZnN=u3rU;$6RNxm{JWRW&_<8{R@SrTY`s6~vo+Wq^QjKiS{`~x>VmQi1 zqXmb@pl1sL%YZ-($XZC7+BxLK*ZH#tV#LCq&@(O}7T#kn@bWijOY|{h0Aglg7C9~y zj#)=86kpk=t9D{K*tyhHx>5TQ=lWl@o z-0BkX`1ut)_8Ufx4I{zXY#y+O7stWA5!~>Psv!(cfQPB_p`apa4&=5B83r`VJiUBw z7_7($LYCi7pTnGkvO?h;lnvK@xHi^7SuZi_OJU!>U#oJC%!^K~k!J#LtLO}Hn{H5} zSm4(j;n$_G_SP*uvdc`K3tV0^lyS%2k@X<69)l+qQh{GKf8(XpH)7QY{^T5i>A8&b z7ddLTx_*jiG{!j~(t(pzV{&vE7xLL=$8H;OefXtyL=-leFZUrc=@`-)-XYm)A(7Op zzP|8BW7{*M`j9RpMyK+GZLm|h+Q*WnD2G?4LZ97>d*5-UiouT6ChCWNd{V>fnR*a% zfsIJu=ejbhC6i`5U*3Oa^N21#`;@$&ySMW@le>1bL-6bW9`-5V6~(tK8cgX*dxmHb z5C@qVxn25?qCsJ2=Wnurz!BopBf zoPY30nf7l}57@qr-Eep8Xun5u*Bw+wGBBlW2_6E&3KHB3X;#&p)sqhDshFL=@VWkO zM#$r)I}IsM=H{MCX~J`04sO!`T!iNffIj1l4TElI5C395*lGET`2aK13ex<=eEj#A z4-hd9f^et?!4DVL>ffVcoH`K60unpY;kMi zz2g0n0C0Nx5C$Ux$dGg6Y_5bG9|AX5oPBqsAGkaR^OEEkOo1AN3gu^=1bzN|2_Hm3 z_kvWpv8b2zW|qo^!Rg*NVx6zlJiCPMOmKAk8Q@?a|JwhWQv}`8LUjJ$?SB~}(9ct2 z5fFg_iRJ{D=*M5@JyJSVJF(c^?sD<;MwOs<{ep_`?8m2QT=_M4zzlc@2kev}XRVnB zEQd#WiQub}tr_IXZFNN+5U!Fyt!GbqIkvU1!7L1x2P}CA;d$^%%D!%3(N@j^mIs9h zhy<=n-aOM0J+hONfFCY{qaVNxNrFRW?tPx3NF1`K%!uxKlJoUbp^=o#*WGvG=`Z%Y z+Qx*b+YT*(>H8IJPn{ z(dyKo0lnbR`GVnh$^v@dhh!u6^=||Dz5!+^3RJ4}!L0z}NJfI%w)9D9)1N_>Q0RmQ zya2cZzRwEIenHj`l74~gL%L7FMQklHF*NRAWMce}oB;FZhW-t%w3sW3r^eW9^C+4d z=rK-NO%S*uGhKM}@Xw)TC5x?2eGYk@*0=P-%~hM1@5`Fr^;u%sip|{~;W`R6?iX>V z$F?U~^GDycYtztv&zY4!7Zkr|el3H8UVw7(Pf*>e9jnaJJm5^=_R z>NMeuZF~0a4PVvp{{8#JoB`lo0*9Rwb%H2v((v1iL1G?w5&XqZ;ei)u5IFB1Y!Ibz ziy@p#01v#d`R?+>lb#r;gNLbeUXbQ6Db{${2ICP25!nr?1Nqc&(kF9ZkQ8@(3yso} z)+|**gEXzk!9=Vk(q<1#Q`FybdP5%w2B0nCHGM7%=vfTZvoZxO=lAj_km1B1JAo%0 z+^kL?VhEfXo|DDcSQr!TWtm)*VG<}Qf8d4TT&I0}u_-zanN)^bExg}YYj&M7>7+j< zqxR07NT60)Am@W*@1JW@Dib?OHN3GNKVzcgPM4#q+D1Zi*x4`{tyB*}t+;2+al!!fAr85)0x~;> z2qUeQ8v}DFGI)q63_^H4W8@|o$tqMG-fqC*4 zt&)|4=2$`Ii4Gy@6>TVG%stJkSq{+ejSAypdLLr%t3PdprwJf9fC@+`aj2Amx-EkR zA1_UV<1(od!kCHU3Q-{7{1!o#9JO6yIQEz(#A<5Gr~PjQ3ofa>J`6*a|88P4`#^`R z3bntP*h~X9m3=mp4o!pa=oKk+C+C#+Sf-kwjvmz^j zS%T-dv0m_iR7k)(4o@+{$BfV=CnvXuNuW--7yJNt!D(DFW!77Xg%}FL;g-7a9Je+D zyQg}3la3-~!bG$xE)n@zI9z(}ku!;){UFEr8=e&GDCY!sX&kI2R-;NNiR-AG-IhY@ zAmywNJS4oEI&i^-oHpP@1AsP`TYU^3A!if&>TJTaH2!=%D-ePK^C58kRdUbqRjQ{5 z$y@@ci{^R^G*YVfbr}3xeTXuVyxWrJT~%={Kl(zkQ{U;ApKg3$OSx z^6}9q>B%YJpaqyW{xv*#vzM!j0nt^F) zo%iA1YP`(@J}fx6)pGlwuO9c`-u5$V^ZeYSs#kZO-|;g8-9%si&d9I@0Am06fBwud AkN^Mx literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ac56c22e8..ea4d45a7e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -64,4 +64,7 @@ #800082C9 #46ffffff + + #BF999999 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 912c87291..23eb125a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -184,6 +184,11 @@ Open settings + Nextcloud Talk voice call + Nextcloud Talk video call + Nextcloud Talk call + INCOMING + RINGING Connecting… Calling… Incoming call from