talk-android/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java
Dariusz Olszewski e9ac99c5df
Notification settings UI on Oreo or later
* On devices with Android 8.0 (Oreo) or later, use system UI to change notification settings.
* Refactor sound selection code to cover differences between old (pre Oreo) and new devices.
* Always use the same notification channel for calls (Oreo or later).

Signed-off-by: Dariusz Olszewski <starypatyk@users.noreply.github.com>
2021-12-14 22:05:48 +01:00

440 lines
16 KiB
Java

/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* 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/>.
*/
package com.nextcloud.talk.activities;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.media.AudioAttributes;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import com.facebook.common.executors.UiThreadImmediateExecutorService;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.nextcloud.talk.R;
import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.databinding.CallNotificationActivityBinding;
import com.nextcloud.talk.events.CallNotificationClick;
import com.nextcloud.talk.models.database.CapabilitiesUtil;
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.participants.Participant;
import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.DoNotDisturbUtils;
import com.nextcloud.talk.utils.NotificationUtils;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import org.greenrobot.eventbus.EventBus;
import org.parceler.Parcels;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import autodagger.AutoInjector;
import butterknife.OnClick;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import okhttp3.Cache;
@SuppressLint("LongLogTag")
@AutoInjector(NextcloudTalkApplication.class)
public class CallNotificationActivity extends CallBaseActivity {
public static final String TAG = "CallNotificationActivity";
@Inject
NcApi ncApi;
@Inject
AppPreferences appPreferences;
@Inject
Cache cache;
@Inject
EventBus eventBus;
@Inject
Context context;
private List<Disposable> disposablesList = new ArrayList<>();
private Bundle originalBundle;
private String roomId;
private UserEntity userBeingCalled;
private String credentials;
private Conversation currentConversation;
private MediaPlayer mediaPlayer;
private boolean leavingScreen = false;
private Handler handler;
private CallNotificationActivityBinding binding;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
binding = CallNotificationActivityBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
hideNavigationIfNoPipAvailable();
eventBus.post(new CallNotificationClick());
Bundle extras = getIntent().getExtras();
this.roomId = extras.getString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), "");
this.currentConversation = Parcels.unwrap(extras.getParcelable(BundleKeys.INSTANCE.getKEY_ROOM()));
this.userBeingCalled = extras.getParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY());
this.originalBundle = extras;
credentials = ApiUtils.getCredentials(userBeingCalled.getUsername(), userBeingCalled.getToken());
setCallDescriptionText();
if (currentConversation == null) {
handleFromNotification();
} else {
setUpAfterConversationIsKnown();
}
if (DoNotDisturbUtils.INSTANCE.shouldPlaySound()) {
playRingtoneSound();
}
initClickListeners();
}
@Override
public void onStart() {
super.onStart();
if (handler == null) {
handler = new Handler();
try {
cache.evictAll();
} catch (IOException e) {
Log.e(TAG, "Failed to evict cache");
}
}
}
private void initClickListeners() {
binding.callAnswerVoiceOnlyView.setOnClickListener(l -> {
Log.d(TAG, "accept call (voice only)");
originalBundle.putBoolean(BundleKeys.INSTANCE.getKEY_CALL_VOICE_ONLY(), true);
proceedToCall();
});
binding.callAnswerCameraView.setOnClickListener(l -> {
Log.d(TAG, "accept call (with video)");
originalBundle.putBoolean(BundleKeys.INSTANCE.getKEY_CALL_VOICE_ONLY(), false);
proceedToCall();
});
binding.hangupButton.setOnClickListener(l -> hangup());
}
private void setCallDescriptionText() {
String callDescriptionWithoutTypeInfo =
String.format(
getResources().getString(R.string.nc_call_unknown),
getResources().getString(R.string.nc_app_product_name));
binding.incomingCallVoiceOrVideoTextView.setText(callDescriptionWithoutTypeInfo);
}
private void showAnswerControls() {
binding.callAnswerCameraView.setVisibility(View.VISIBLE);
binding.callAnswerVoiceOnlyView.setVisibility(View.VISIBLE);
}
@OnClick(R.id.hangupButton)
void hangup() {
leavingScreen = true;
finish();
}
private void proceedToCall() {
originalBundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), currentConversation.getToken());
originalBundle.putString(BundleKeys.INSTANCE.getKEY_CONVERSATION_NAME(), currentConversation.getDisplayName());
Intent intent = new Intent(this, CallActivity.class);
intent.putExtras(originalBundle);
startActivity(intent);
}
private void checkIfAnyParticipantsRemainInRoom() {
int apiVersion = ApiUtils.getCallApiVersion(userBeingCalled, new int[]{ApiUtils.APIv4, 1});
ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(apiVersion, userBeingCalled.getBaseUrl(),
currentConversation.getToken()))
.subscribeOn(Schedulers.io())
.repeatWhen(completed -> completed.zipWith(Observable.range(1, 12), (n, i) -> i)
.flatMap(retryCount -> Observable.timer(5, TimeUnit.SECONDS))
.takeWhile(observable -> !leavingScreen))
.subscribe(new Observer<ParticipantsOverall>() {
@Override
public void onSubscribe(Disposable d) {
disposablesList.add(d);
}
@Override
public void onNext(ParticipantsOverall participantsOverall) {
boolean hasParticipantsInCall = false;
boolean inCallOnDifferentDevice = false;
List<Participant> participantList = participantsOverall.getOcs().getData();
hasParticipantsInCall = participantList.size() > 0;
if (hasParticipantsInCall) {
for (Participant participant : participantList) {
if (participant.getActorType() == Participant.ActorType.USERS &&
participant.getActorId().equals(userBeingCalled.getUserId())) {
inCallOnDifferentDevice = true;
break;
}
}
}
if (!hasParticipantsInCall || inCallOnDifferentDevice) {
runOnUiThread(() -> hangup());
}
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
runOnUiThread(() -> hangup());
}
});
}
private void handleFromNotification() {
int apiVersion = ApiUtils.getConversationApiVersion(userBeingCalled, new int[]{ApiUtils.APIv4,
ApiUtils.APIv3, 1});
ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, userBeingCalled.getBaseUrl(), roomId))
.subscribeOn(Schedulers.io())
.retry(3)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<RoomOverall>() {
@Override
public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
disposablesList.add(d);
}
@Override
public void onNext(@io.reactivex.annotations.NonNull RoomOverall roomOverall) {
currentConversation = roomOverall.getOcs().data;
setUpAfterConversationIsKnown();
if (apiVersion >= 3) {
boolean hasCallFlags =
CapabilitiesUtil.hasSpreedFeatureCapability(userBeingCalled,
"conversation-call-flags");
if (hasCallFlags) {
if (isInCallWithVideo(currentConversation.callFlag)) {
binding.incomingCallVoiceOrVideoTextView.setText(
String.format(getResources().getString(R.string.nc_call_video),
getResources().getString(R.string.nc_app_product_name)));
} else {
binding.incomingCallVoiceOrVideoTextView.setText(
String.format(getResources().getString(R.string.nc_call_voice),
getResources().getString(R.string.nc_app_product_name)));
}
}
}
}
@Override
public void onError(@io.reactivex.annotations.NonNull Throwable e) {
Log.e(TAG, e.getMessage(), 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 setUpAfterConversationIsKnown() {
binding.conversationNameTextView.setText(currentConversation.getDisplayName());
if(currentConversation.getType() == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL){
setAvatarForOneToOneCall();
} else {
binding.avatarImageView.setImageResource(R.drawable.ic_circular_group);
}
checkIfAnyParticipantsRemainInRoom();
showAnswerControls();
}
private void setAvatarForOneToOneCall() {
ImageRequest imageRequest =
DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatarWithName(userBeingCalled.getBaseUrl(),
currentConversation.getName(),
R.dimen.avatar_size_big), null);
ImagePipeline imagePipeline = Fresco.getImagePipeline();
DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(imageRequest, null);
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
protected void onNewResultImpl(@Nullable Bitmap bitmap) {
binding.avatarImageView.getHierarchy().setImage(
new BitmapDrawable(getResources(), bitmap),
100,
true);
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
Log.e(TAG, "failed to load avatar");
}
}, UiThreadImmediateExecutorService.getInstance());
}
private void endMediaNotifications() {
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
mediaPlayer.release();
mediaPlayer = null;
}
}
@Override
public void onDestroy() {
leavingScreen = true;
if (handler != null) {
handler.removeCallbacksAndMessages(null);
handler = null;
}
dispose();
endMediaNotifications();
super.onDestroy();
}
private void dispose() {
if (disposablesList != null) {
for (Disposable disposable : disposablesList) {
if (!disposable.isDisposed()) {
disposable.dispose();
}
}
}
}
private void playRingtoneSound() {
Uri ringtoneUri = NotificationUtils.INSTANCE.getCallRingtoneUri(getApplicationContext(), appPreferences);
if (ringtoneUri != null) {
mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setDataSource(this, 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");
}
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
isInPipMode = isInPictureInPictureMode;
if (isInPictureInPictureMode) {
updateUiForPipMode();
} else {
updateUiForNormalMode();
}
}
public void updateUiForPipMode() {
binding.callAnswerButtons.setVisibility(View.INVISIBLE);
binding.incomingCallRelativeLayout.setVisibility(View.INVISIBLE);
}
public void updateUiForNormalMode() {
binding.callAnswerButtons.setVisibility(View.VISIBLE);
binding.incomingCallRelativeLayout.setVisibility(View.VISIBLE);
}
@Override
void suppressFitsSystemWindows() {
binding.controllerCallNotificationLayout.setFitsSystemWindows(false);
}
}