mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 09:49:30 +01:00
Implement new call screen UI/UX.
This commit is contained in:
committed by
Greyson Parrelli
parent
33e3f78be6
commit
d5419ec9fa
@@ -3,14 +3,20 @@ package org.thoughtcrime.securesms.util;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.bumptech.glide.request.target.CustomViewTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
@@ -29,6 +35,35 @@ public final class AvatarUtil {
|
||||
private AvatarUtil() {
|
||||
}
|
||||
|
||||
public static void loadBlurredIconIntoViewBackground(@NonNull Recipient recipient, @NonNull View target) {
|
||||
Context context = target.getContext();
|
||||
|
||||
if (recipient.getContactPhoto() == null) {
|
||||
target.setBackgroundColor(ContextCompat.getColor(target.getContext(), R.color.black));
|
||||
return;
|
||||
}
|
||||
|
||||
GlideApp.with(target)
|
||||
.load(recipient.getContactPhoto())
|
||||
.transform(new CenterCrop(), new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS))
|
||||
.into(new CustomViewTarget<View, Drawable>(target) {
|
||||
@Override
|
||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||
target.setBackgroundColor(ContextCompat.getColor(target.getContext(), R.color.black));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
|
||||
target.setBackground(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResourceCleared(@Nullable Drawable placeholder) {
|
||||
target.setBackground(placeholder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void loadIconIntoImageView(@NonNull Recipient recipient, @NonNull ImageView target) {
|
||||
Context context = target.getContext();
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.renderscript.Allocation;
|
||||
import android.renderscript.Element;
|
||||
import android.renderscript.RenderScript;
|
||||
import android.renderscript.ScriptIntrinsicBlur;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class BlurTransformation extends BitmapTransformation {
|
||||
|
||||
public static final float MAX_RADIUS = 25f;
|
||||
|
||||
private final RenderScript rs;
|
||||
private final float bitmapScaleFactor;
|
||||
private final float blurRadius;
|
||||
|
||||
public BlurTransformation(@NonNull Context context, float bitmapScaleFactor, float blurRadius) {
|
||||
rs = RenderScript.create(context);
|
||||
|
||||
Preconditions.checkArgument(blurRadius >= 0 && blurRadius <= 25, "Blur radius must be a non-negative value less than or equal to 25.");
|
||||
Preconditions.checkArgument(bitmapScaleFactor > 0, "Bitmap scale factor must be a non-negative value");
|
||||
|
||||
this.bitmapScaleFactor = bitmapScaleFactor;
|
||||
this.blurRadius = blurRadius;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
|
||||
Matrix scaleMatrix = new Matrix();
|
||||
scaleMatrix.setScale(bitmapScaleFactor, bitmapScaleFactor);
|
||||
|
||||
Bitmap blurredBitmap = Bitmap.createBitmap(toTransform, 0, 0, outWidth, outHeight, scaleMatrix, true);
|
||||
Allocation input = Allocation.createFromBitmap(rs, blurredBitmap, Allocation.MipmapControl.MIPMAP_FULL, Allocation.USAGE_SHARED);
|
||||
Allocation output = Allocation.createTyped(rs, input.getType());
|
||||
ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
|
||||
|
||||
script.setInput(input);
|
||||
script.setRadius(blurRadius);
|
||||
script.forEach(output);
|
||||
output.copyTo(blurredBitmap);
|
||||
|
||||
return blurredBitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
||||
messageDigest.update(String.format(Locale.US, "blur-%f-%f", bitmapScaleFactor, blurRadius).getBytes());
|
||||
}
|
||||
}
|
||||
@@ -18,23 +18,26 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestDialogFragment;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||
|
||||
public class CommunicationActions {
|
||||
|
||||
private static final String TAG = Log.tag(CommunicationActions.class);
|
||||
|
||||
public static void startVoiceCall(@NonNull Activity activity, @NonNull Recipient recipient) {
|
||||
public static void startVoiceCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient) {
|
||||
if (TelephonyUtil.isAnyPstnLineBusy(activity)) {
|
||||
Toast.makeText(activity,
|
||||
R.string.CommunicationActions_a_cellular_call_is_already_in_progress,
|
||||
@@ -60,7 +63,7 @@ public class CommunicationActions {
|
||||
});
|
||||
}
|
||||
|
||||
public static void startVideoCall(@NonNull Activity activity, @NonNull Recipient recipient) {
|
||||
public static void startVideoCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient) {
|
||||
if (TelephonyUtil.isAnyPstnLineBusy(activity)) {
|
||||
Toast.makeText(activity,
|
||||
R.string.CommunicationActions_a_cellular_call_is_already_in_progress,
|
||||
@@ -173,29 +176,69 @@ public class CommunicationActions {
|
||||
}
|
||||
}
|
||||
|
||||
private static void startCallInternal(@NonNull Activity activity, @NonNull Recipient recipient, boolean isVideo) {
|
||||
private static void startCallInternal(@NonNull FragmentActivity activity, @NonNull Recipient recipient, boolean isVideo) {
|
||||
if (isVideo) startVideoCallInternal(activity, recipient);
|
||||
else startAudioCallInternal(activity, recipient);
|
||||
}
|
||||
|
||||
private static void startAudioCallInternal(@NonNull FragmentActivity activity, @NonNull Recipient recipient) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity__to_call_s_signal_needs_access_to_your_microphone, recipient.getDisplayName(activity)),
|
||||
R.drawable.ic_mic_solid_24)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.ConversationActivity__to_call_s_signal_needs_access_to_your_microphone, recipient.getDisplayName(activity)))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(activity, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()))
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.AUDIO_CALL.getCode());
|
||||
activity.startService(intent);
|
||||
|
||||
MessageSender.onMessageSent();
|
||||
|
||||
if (FeatureFlags.profileForCalling() && recipient.resolve().getProfileKey() == null) {
|
||||
CalleeMustAcceptMessageRequestDialogFragment.create(recipient.getId())
|
||||
.show(activity.getSupportFragmentManager(), null);
|
||||
} else {
|
||||
Intent activityIntent = new Intent(activity, WebRtcCallActivity.class);
|
||||
|
||||
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
activity.startActivity(activityIntent);
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
private static void startVideoCallInternal(@NonNull FragmentActivity activity, @NonNull Recipient recipient) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera, recipient.getDisplayName(activity)),
|
||||
.withRationaleDialog(activity.getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(activity)),
|
||||
R.drawable.ic_mic_solid_24,
|
||||
R.drawable.ic_video_solid_24_tinted)
|
||||
.withPermanentDenialDialog(activity.getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(activity)))
|
||||
.onAllGranted(() -> {
|
||||
Intent intent = new Intent(activity, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()))
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.VIDEO_CALL.getCode());
|
||||
activity.startService(intent);
|
||||
|
||||
Intent activityIntent = new Intent(activity, WebRtcCallActivity.class);
|
||||
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
if (isVideo) {
|
||||
activityIntent.putExtra(WebRtcCallActivity.EXTRA_ENABLE_VIDEO_IF_AVAILABLE, true);
|
||||
}
|
||||
|
||||
MessageSender.onMessageSent();
|
||||
activity.startActivity(activityIntent);
|
||||
|
||||
if (FeatureFlags.profileForCalling() && recipient.resolve().getProfileKey() == null) {
|
||||
CalleeMustAcceptMessageRequestDialogFragment.create(recipient.getId())
|
||||
.show(activity.getSupportFragmentManager(), null);
|
||||
} else {
|
||||
Intent activityIntent = new Intent(activity, WebRtcCallActivity.class);
|
||||
|
||||
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(WebRtcCallActivity.EXTRA_ENABLE_VIDEO_IF_AVAILABLE, true);
|
||||
|
||||
activity.startActivity(activityIntent);
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class EllapsedTimeFormatter {
|
||||
private final long hours;
|
||||
private final long minutes;
|
||||
private final long seconds;
|
||||
|
||||
private EllapsedTimeFormatter(long durationMillis) {
|
||||
hours = durationMillis / 3600;
|
||||
minutes = durationMillis % 3600 / 60;
|
||||
seconds = durationMillis % 3600 % 60;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
if (hours > 0) {
|
||||
return String.format(Locale.US, "%02d:%02d:%02d", hours, minutes, seconds);
|
||||
} else {
|
||||
return String.format(Locale.US, "%02d:%02d", minutes, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable EllapsedTimeFormatter fromDurationMillis(long durationMillis) {
|
||||
if (durationMillis == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new EllapsedTimeFormatter(durationMillis);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,9 @@ public final class FeatureFlags {
|
||||
private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone";
|
||||
private static final String ATTACHMENTS_V3 = "android.attachmentsV3";
|
||||
private static final String REMOTE_DELETE = "android.remoteDelete";
|
||||
private static final String PROFILE_FOR_CALLING = "android.profileForCalling";
|
||||
private static final String CALLING_PIP = "android.callingPip";
|
||||
private static final String NEW_GROUP_UI = "android.newGroupUI";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -70,7 +73,10 @@ public final class FeatureFlags {
|
||||
PROFILE_NAMES_MEGAPHONE,
|
||||
MESSAGE_REQUESTS,
|
||||
ATTACHMENTS_V3,
|
||||
REMOTE_DELETE
|
||||
REMOTE_DELETE,
|
||||
PROFILE_FOR_CALLING,
|
||||
CALLING_PIP,
|
||||
NEW_GROUP_UI
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -226,6 +232,16 @@ public final class FeatureFlags {
|
||||
return getValue(REMOTE_DELETE, false);
|
||||
}
|
||||
|
||||
/** Whether or not profile sharing is required for calling */
|
||||
public static boolean profileForCalling() {
|
||||
return messageRequests() && getValue(PROFILE_FOR_CALLING, false);
|
||||
}
|
||||
|
||||
/** Whether or not to display Calling PIP */
|
||||
public static boolean callingPip() {
|
||||
return getValue(CALLING_PIP, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Boolean> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
@@ -198,6 +198,10 @@ public class ViewUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static float pxToDp(float px) {
|
||||
return px / Resources.getSystem().getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
public static int dpToPx(Context context, int dp) {
|
||||
return (int)((dp * context.getResources().getDisplayMetrics().density) + 0.5);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.util.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class TouchInterceptingFrameLayout extends FrameLayout {
|
||||
|
||||
private OnInterceptTouchEventListener listener;
|
||||
|
||||
public TouchInterceptingFrameLayout(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public TouchInterceptingFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public TouchInterceptingFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
if (listener != null) {
|
||||
return listener.onInterceptTouchEvent(ev);
|
||||
} else {
|
||||
return super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnInterceptTouchEventListener(@Nullable OnInterceptTouchEventListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public interface OnInterceptTouchEventListener {
|
||||
boolean onInterceptTouchEvent(MotionEvent ev);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user