Implement new call screen UI/UX.

This commit is contained in:
Alex Hart
2020-04-23 16:20:59 -03:00
committed by Greyson Parrelli
parent 33e3f78be6
commit d5419ec9fa
73 changed files with 2793 additions and 1142 deletions

View File

@@ -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();

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);
}
}