Replace answer/decline button and action for incoming calls

Fixes #7199
master
Moxie Marlinspike 2017-11-19 16:24:30 -08:00
parent e14a97cf68
commit 86bd2351bc
17 changed files with 422 additions and 1773 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:clipToPadding="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:parentTag="android.widget.LinearLayout"
tools:orientation="vertical">
<ImageView android:id="@+id/arrow_one"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="-15dp"
android:alpha="0"
android:src="@drawable/ic_keyboard_arrow_up_white_36dp"
android:tint="@color/gray20"
tools:alpha="1"/>
<ImageView android:id="@+id/arrow_two"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="-15dp"
android:alpha="0"
android:src="@drawable/ic_keyboard_arrow_up_white_36dp"
android:tint="@color/gray20"
tools:alpha="1"/>
<ImageView android:id="@+id/arrow_three"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="-15dp"
android:alpha="0"
android:src="@drawable/ic_keyboard_arrow_up_white_36dp"
android:tint="@color/gray20"
tools:alpha="1"/>
<ImageView android:id="@+id/arrow_four"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="10dp"
android:alpha="0"
android:src="@drawable/ic_keyboard_arrow_up_white_36dp"
android:tint="@color/gray20"
tools:alpha="1"/>
<TextView android:id="@+id/swipe_up_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="19dp"
android:textColor="@color/gray20"
android:textStyle="italic"
android:text="@string/webrtc_answer_decline_button__swipe_up_to_answer"/>
<ImageView android:id="@+id/answer"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="center_horizontal"
android:padding="12dp"
android:elevation="5dp"
android:background="@drawable/circle_tintable"
android:src="@drawable/ic_phone_grey600_32dp"
android:tint="@color/green_600"/>
<TextView android:id="@+id/swipe_down_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:textStyle="italic"
android:textColor="@color/gray20"
android:text="@string/webrtc_answer_decline_button__swipe_down_to_reject"/>
</merge>

View File

@ -225,10 +225,12 @@
android:contentDescription="End call"
tools:visibility="visible"/>
<org.thoughtcrime.securesms.components.webrtc.WebRtcIncomingCallOverlay
android:id="@+id/callControls"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<org.thoughtcrime.securesms.components.webrtc.WebRtcAnswerDeclineButton
android:id="@+id/answer_decline_button"
android:layout_gravity="bottom|center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"/>
</android.support.design.widget.CoordinatorLayout>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.thoughtcrime.securesms.components.multiwaveview.MultiWaveView
android:id="@+id/incomingCallWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="-46dp"
android:background="@android:color/black"
android:visibility="gone"
/>
<TextView android:id="@+id/redphone_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignTop="@id/incomingCallWidget"
android:gravity="center"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="#FFFFFF"
android:textAllCaps="true"
android:background="@color/textsecure_primary"
android:text="@string/redphone_call_controls__signal_call"/>
</RelativeLayout>

View File

@ -1251,6 +1251,8 @@
<string name="OutdatedBuildReminder_no_web_browser_installed">No web browser installed!</string>
<string name="ContactsCursorLoader_recent_chats">Recent chats</string>
<string name="ContactsCursorLoader_contacts">Contacts</string>
<string name="webrtc_answer_decline_button__swipe_up_to_answer">Swipe up to answer</string>
<string name="webrtc_answer_decline_button__swipe_down_to_reject">Swipe down to reject</string>
<!-- EOF -->

View File

@ -35,9 +35,9 @@ import android.view.WindowManager;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAnswerDeclineButton;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
import org.thoughtcrime.securesms.components.webrtc.WebRtcIncomingCallOverlay;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
@ -359,14 +359,14 @@ public class WebRtcCallActivity extends Activity {
}
}
private class IncomingCallActionListener implements WebRtcIncomingCallOverlay.IncomingCallActionListener {
private class IncomingCallActionListener implements WebRtcAnswerDeclineButton.AnswerDeclineListener {
@Override
public void onAcceptClick() {
public void onAnswered() {
WebRtcCallActivity.this.handleAnswerCall();
}
@Override
public void onDenyClick() {
public void onDeclined() {
WebRtcCallActivity.this.handleDenyCall();
}
}

View File

@ -1,134 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thoughtcrime.securesms.components.multiwaveview;
import android.animation.TimeInterpolator;
import android.annotation.SuppressLint;
@SuppressLint("NewApi")
class Ease {
private static final float DOMAIN = 1.0f;
private static final float DURATION = 1.0f;
private static final float START = 0.0f;
static class Linear {
public static final TimeInterpolator easeNone = new TimeInterpolator() {
public float getInterpolation(float input) {
return input;
}
};
}
static class Cubic {
public static final TimeInterpolator easeIn = new TimeInterpolator() {
public float getInterpolation(float input) {
return DOMAIN*(input/=DURATION)*input*input + START;
}
};
public static final TimeInterpolator easeOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return DOMAIN*((input=input/DURATION-1)*input*input + 1) + START;
}
};
public static final TimeInterpolator easeInOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return ((input/=DURATION/2) < 1.0f) ?
(DOMAIN/2*input*input*input + START)
: (DOMAIN/2*((input-=2)*input*input + 2) + START);
}
};
}
static class Quad {
public static final TimeInterpolator easeIn = new TimeInterpolator() {
public float getInterpolation (float input) {
return DOMAIN*(input/=DURATION)*input + START;
}
};
public static final TimeInterpolator easeOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return -DOMAIN *(input/=DURATION)*(input-2) + START;
}
};
public static final TimeInterpolator easeInOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return ((input/=DURATION/2) < 1) ?
(DOMAIN/2*input*input + START)
: (-DOMAIN/2 * ((--input)*(input-2) - 1) + START);
}
};
}
static class Quart {
public static final TimeInterpolator easeIn = new TimeInterpolator() {
public float getInterpolation(float input) {
return DOMAIN*(input/=DURATION)*input*input*input + START;
}
};
public static final TimeInterpolator easeOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return -DOMAIN * ((input=input/DURATION-1)*input*input*input - 1) + START;
}
};
public static final TimeInterpolator easeInOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return ((input/=DURATION/2) < 1) ?
(DOMAIN/2*input*input*input*input + START)
: (-DOMAIN/2 * ((input-=2)*input*input*input - 2) + START);
}
};
}
static class Quint {
public static final TimeInterpolator easeIn = new TimeInterpolator() {
public float getInterpolation(float input) {
return DOMAIN*(input/=DURATION)*input*input*input*input + START;
}
};
public static final TimeInterpolator easeOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return DOMAIN*((input=input/DURATION-1)*input*input*input*input + 1) + START;
}
};
public static final TimeInterpolator easeInOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return ((input/=DURATION/2) < 1) ?
(DOMAIN/2*input*input*input*input*input + START)
: (DOMAIN/2*((input-=2)*input*input*input*input + 2) + START);
}
};
}
static class Sine {
public static final TimeInterpolator easeIn = new TimeInterpolator() {
public float getInterpolation(float input) {
return -DOMAIN * (float) Math.cos(input / DURATION * (Math.PI / 2)) + DOMAIN + START;
}
};
public static final TimeInterpolator easeOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return DOMAIN * (float) Math.sin(input / DURATION * (Math.PI / 2)) + START;
}
};
public static final TimeInterpolator easeInOut = new TimeInterpolator() {
public float getInterpolation(float input) {
return -DOMAIN/2 * ((float) Math.cos(Math.PI * input / DURATION) - 1.0f) + START;
}
};
}
}

View File

@ -1,225 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thoughtcrime.securesms.components.multiwaveview;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
public class TargetDrawable {
private static final String TAG = "TargetDrawable";
private static final boolean DEBUG = false;
public static final int[] STATE_ACTIVE =
{ android.R.attr.state_enabled, android.R.attr.state_active };
public static final int[] STATE_INACTIVE =
{ android.R.attr.state_enabled, -android.R.attr.state_active };
public static final int[] STATE_FOCUSED =
{ android.R.attr.state_enabled, android.R.attr.state_focused };
private float mTranslationX = 0.0f;
private float mTranslationY = 0.0f;
private float mScaleX = 1.0f;
private float mScaleY = 1.0f;
private float mAlpha = 1.0f;
private Drawable mDrawable;
/* package */ static class DrawableWithAlpha extends Drawable {
private float mAlpha = 1.0f;
private Drawable mRealDrawable;
public DrawableWithAlpha(Drawable realDrawable) {
mRealDrawable = realDrawable;
}
public void setAlpha(float alpha) {
mAlpha = alpha;
}
public int getAlpha() {
return (int)(mAlpha * 255);
}
@Override
public void draw(Canvas canvas) {
mRealDrawable.setAlpha((int) Math.round(mAlpha * 255f));
mRealDrawable.draw(canvas);
}
@Override
public void setAlpha(int alpha) {
mRealDrawable.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
mRealDrawable.setColorFilter(cf);
}
@Override
public int getOpacity() {
return mRealDrawable.getOpacity();
}
}
public TargetDrawable(Resources res, int resId) {
this(res, resId == 0 ? null : res.getDrawable(resId));
}
public TargetDrawable(Resources res, Drawable drawable) {
// Mutate the drawable so we can animate shared drawable properties.
mDrawable = drawable != null ? drawable.mutate() : null;
resizeDrawables();
setState(STATE_INACTIVE);
}
public void setState(int [] state) {
if (mDrawable instanceof StateListDrawable) {
StateListDrawable d = (StateListDrawable) mDrawable;
d.setState(state);
}
}
// public boolean hasState(int [] state) {
// if (mDrawable instanceof StateListDrawable) {
// StateListDrawable d = (StateListDrawable) mDrawable;
// // TODO: this doesn't seem to work
// return d.getStateDrawableIndex(state) != -1;
// }
// return false;
// }
/**
* Returns true if the drawable is a StateListDrawable and is in the focused state.
*
* @return
*/
public boolean isActive() {
if (mDrawable instanceof StateListDrawable) {
StateListDrawable d = (StateListDrawable) mDrawable;
int[] states = d.getState();
for (int i = 0; i < states.length; i++) {
if (states[i] == android.R.attr.state_focused) {
return true;
}
}
}
return false;
}
/**
* Returns true if this target is enabled. Typically an enabled target contains a valid
* drawable in a valid state. Currently all targets with valid drawables are valid.
*
* @return
*/
public boolean isValid() {
return mDrawable != null;
}
/**
* Makes drawables in a StateListDrawable all the same dimensions.
* If not a StateListDrawable, then justs sets the bounds to the intrinsic size of the
* drawable.
*/
private void resizeDrawables() {
if (mDrawable != null)
mDrawable.setBounds(0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
}
// private void resizeDrawables() {
// if (mDrawable instanceof StateListDrawable) {
// StateListDrawable d = (StateListDrawable) mDrawable;
// int maxWidth = 0;
// int maxHeight = 0;
//
// for (int i = 0; i < d.getStateCount(); i++) {
// Drawable childDrawable = d.getStateDrawable(i);
// maxWidth = Math.max(maxWidth, childDrawable.getIntrinsicWidth());
// maxHeight = Math.max(maxHeight, childDrawable.getIntrinsicHeight());
// }
// if (DEBUG) Log.v(TAG, "union of childDrawable rects " + d + " to: "
// + maxWidth + "x" + maxHeight);
// d.setBounds(0, 0, maxWidth, maxHeight);
// for (int i = 0; i < d.getStateCount(); i++) {
// Drawable childDrawable = d.getStateDrawable(i);
// if (DEBUG) Log.v(TAG, "sizing drawable " + childDrawable + " to: "
// + maxWidth + "x" + maxHeight);
// childDrawable.setBounds(0, 0, maxWidth, maxHeight);
// }
// } else if (mDrawable != null) {
// mDrawable.setBounds(0, 0,
// mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
// }
// }
public void setX(float x) {
mTranslationX = x;
}
public void setY(float y) {
mTranslationY = y;
}
public void setScaleX(float x) {
mScaleX = x;
}
public void setScaleY(float y) {
mScaleY = y;
}
public void setAlpha(float alpha) {
mAlpha = alpha;
}
public float getX() {
return mTranslationX;
}
public float getY() {
return mTranslationY;
}
public float getScaleX() {
return mScaleX;
}
public float getScaleY() {
return mScaleY;
}
public float getAlpha() {
return mAlpha;
}
public int getWidth() {
return mDrawable != null ? mDrawable.getIntrinsicWidth() : 0;
}
public int getHeight() {
return mDrawable != null ? mDrawable.getIntrinsicHeight() : 0;
}
public void draw(Canvas canvas) {
if (mDrawable == null) {
return;
}
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.translate(mTranslationX, mTranslationY);
canvas.scale(mScaleX, mScaleY);
canvas.translate(-0.5f * getWidth(), -0.5f * getHeight());
mDrawable.setAlpha((int) Math.round(mAlpha * 255f));
mDrawable.draw(canvas);
canvas.restore();
}
}

View File

@ -1,180 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thoughtcrime.securesms.components.multiwaveview;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.SuppressLint;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
class Tweener {
private static final String TAG = "Tweener";
private static final boolean DEBUG = false;
ObjectAnimator animator;
private static HashMap<Object, Tweener> sTweens = new HashMap<Object, Tweener>();
public Tweener(ObjectAnimator anim) {
animator = anim;
}
private static void remove(Animator animator) {
Iterator<Entry<Object, Tweener>> iter = sTweens.entrySet().iterator();
while (iter.hasNext()) {
Entry<Object, Tweener> entry = iter.next();
if (entry.getValue().animator == animator) {
if (DEBUG) Log.v(TAG, "Removing tweener " + sTweens.get(entry.getKey())
+ " sTweens.size() = " + sTweens.size());
iter.remove();
break; // an animator can only be attached to one object
}
}
}
@SuppressLint("NewApi")
public static Tweener to(Object object, long duration, Object... vars) {
long delay = 0;
AnimatorUpdateListener updateListener = null;
AnimatorListener listener = null;
TimeInterpolator interpolator = null;
// Iterate through arguments and discover properties to animate
ArrayList<PropertyValuesHolder> props = new ArrayList<PropertyValuesHolder>(vars.length/2);
for (int i = 0; i < vars.length; i+=2) {
if (!(vars[i] instanceof String)) {
throw new IllegalArgumentException("Key must be a string: " + vars[i]);
}
String key = (String) vars[i];
Object value = vars[i+1];
if ("simultaneousTween".equals(key)) {
// TODO
} else if ("ease".equals(key)) {
interpolator = (TimeInterpolator) value; // TODO: multiple interpolators?
} else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) {
updateListener = (AnimatorUpdateListener) value;
} else if ("onCodeComplete".equals(key) || "onCompleteListener".equals(key)) {
listener = (AnimatorListener) value;
} else if ("delay".equals(key)) {
delay = ((Number) value).longValue();
} else if ("syncWith".equals(key)) {
// TODO
} else if (value instanceof float[]) {
props.add(PropertyValuesHolder.ofFloat(key,
((float[]) value)[0], ((float[]) value)[1]));
} else if (value instanceof Number) {
float floatValue = ((Number)value).floatValue();
props.add(PropertyValuesHolder.ofFloat(key, floatValue));
} else {
throw new IllegalArgumentException(
"Bad argument for key \"" + key + "\" with value " + value.getClass());
}
}
// Re-use existing tween, if present
Tweener tween = sTweens.get(object);
ObjectAnimator anim = null;
if (tween == null) {
anim = ObjectAnimator.ofPropertyValuesHolder(object,
props.toArray(new PropertyValuesHolder[props.size()]));
tween = new Tweener(anim);
sTweens.put(object, tween);
if (DEBUG) Log.v(TAG, "Added new Tweener " + tween);
} else {
anim = sTweens.get(object).animator;
replace(props, object); // Cancel all animators for given object
}
if (interpolator != null) {
anim.setInterpolator(interpolator);
}
// Update animation with properties discovered in loop above
anim.setStartDelay(delay);
anim.setDuration(duration);
if (updateListener != null) {
anim.removeAllUpdateListeners(); // There should be only one
anim.addUpdateListener(updateListener);
}
if (listener != null) {
anim.removeAllListeners(); // There should be only one.
anim.addListener(listener);
}
anim.addListener(mCleanupListener);
anim.start();
return tween;
}
Tweener from(Object object, long duration, Object... vars) {
// TODO: for v of vars
// toVars[v] = object[v]
// object[v] = vars[v]
return Tweener.to(object, duration, vars);
}
// Listener to watch for completed animations and remove them.
@SuppressLint("NewApi")
private static AnimatorListener mCleanupListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
remove(animation);
}
@Override
public void onAnimationCancel(Animator animation) {
remove(animation);
}
};
public static void reset() {
if (DEBUG) {
Log.v(TAG, "Reset()");
if (sTweens.size() > 0) {
Log.v(TAG, "Cleaning up " + sTweens.size() + " animations");
}
}
sTweens.clear();
}
@SuppressLint("NewApi")
private static void replace(ArrayList<PropertyValuesHolder> props, Object... args) {
for (final Object killobject : args) {
Tweener tween = sTweens.get(killobject);
if (tween != null) {
tween.animator.cancel();
if (props != null) {
tween.animator.setValues(
props.toArray(new PropertyValuesHolder[props.size()]));
} else {
sTweens.remove(tween);
}
}
}
}
}

View File

@ -0,0 +1,293 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class WebRtcAnswerDeclineButton extends LinearLayout implements View.OnTouchListener {
@SuppressWarnings("unused")
private static final String TAG = WebRtcAnswerDeclineButton.class.getSimpleName();
private static final int TOTAL_TIME = 1000;
private static final int SHAKE_TIME = 200;
private static final int UP_TIME = (TOTAL_TIME - SHAKE_TIME) / 2;
private static final int DOWN_TIME = (TOTAL_TIME - SHAKE_TIME) / 2;
private static final int FADE_OUT_TIME = 300;
private static final int FADE_IN_TIME = 100;
private static final int SHIMMER_TOTAL = UP_TIME + SHAKE_TIME;
private static final int ANSWER_THRESHOLD = 112;
private static final int DECLINE_THRESHOLD = 56;
private TextView swipeUpText;
private ImageView fab;
private TextView swipeDownText;
private ImageView arrowOne;
private ImageView arrowTwo;
private ImageView arrowThree;
private ImageView arrowFour;
private float lastY;
private boolean animating = false;
private AnimatorSet animatorSet;
private AnswerDeclineListener listener;
public WebRtcAnswerDeclineButton(Context context) {
super(context);
initialize();
}
public WebRtcAnswerDeclineButton(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WebRtcAnswerDeclineButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public WebRtcAnswerDeclineButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize();
}
private void initialize() {
setOrientation(LinearLayout.VERTICAL);
setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
inflate(getContext(), R.layout.webrtc_answer_decline_button, this);
this.swipeUpText = findViewById(R.id.swipe_up_text);
this.fab = findViewById(R.id.answer);
this.swipeDownText = findViewById(R.id.swipe_down_text);
this.arrowOne = findViewById(R.id.arrow_one);
this.arrowTwo = findViewById(R.id.arrow_two);
this.arrowThree = findViewById(R.id.arrow_three);
this.arrowFour = findViewById(R.id.arrow_four);
this.fab.setOnTouchListener(this);
}
public void startRingingAnimation() {
if (!animating) {
animating = true;
animateElements(0);
}
}
public void stopRingingAnimation() {
if (animating) {
animating = false;
resetElements();
}
}
public void setAnswerDeclineListener(AnswerDeclineListener listener) {
this.listener = listener;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
resetElements();
swipeUpText.animate().alpha(0).setDuration(200).start();
swipeDownText.animate().alpha(0).setDuration(200).start();
lastY = event.getRawY();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
swipeUpText.clearAnimation();
swipeDownText.clearAnimation();
swipeUpText.setAlpha(1);
swipeDownText.setAlpha(1);
fab.setRotation(0);
if (Build.VERSION.SDK_INT >= 21) {
fab.getDrawable().setTint(getResources().getColor(R.color.green_600));
fab.getBackground().setTint(Color.WHITE);
}
animating = true;
animateElements(0);
break;
case MotionEvent.ACTION_MOVE:
float difference = event.getRawY() - lastY;
float differenceThreshold;
float percentageToThreshold;
int backgroundColor;
int foregroundColor;
if (difference <= 0) {
differenceThreshold = ViewUtil.dpToPx(getContext(), ANSWER_THRESHOLD);
percentageToThreshold = Math.min(1, (difference * -1) / differenceThreshold);
backgroundColor = (int) new ArgbEvaluator().evaluate(percentageToThreshold, getResources().getColor(R.color.green_100), getResources().getColor(R.color.green_600));
if (percentageToThreshold > 0.5) {
foregroundColor = Color.WHITE;
} else {
foregroundColor = getResources().getColor(R.color.green_600);
}
fab.setTranslationY(difference);
if (percentageToThreshold == 1 && listener != null) listener.onAnswered();
} else {
differenceThreshold = ViewUtil.dpToPx(getContext(), DECLINE_THRESHOLD);
percentageToThreshold = Math.min(1, difference / differenceThreshold);
backgroundColor = (int) new ArgbEvaluator().evaluate(percentageToThreshold, getResources().getColor(R.color.red_100), getResources().getColor(R.color.red_600));
if (percentageToThreshold > 0.5) {
foregroundColor = Color.WHITE;
} else {
foregroundColor = getResources().getColor(R.color.green_600);
}
fab.setRotation(135 * percentageToThreshold);
if (percentageToThreshold == 1 && listener != null) listener.onDeclined();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
fab.getBackground().setTint(backgroundColor);
fab.getDrawable().setTint(foregroundColor);
}
break;
}
return true;
}
private void animateElements(int delay) {
ObjectAnimator fabUp = getUpAnimation(fab);
ObjectAnimator fabDown = getDownAnimation(fab);
ObjectAnimator fabShake = getShakeAnimation(fab);
animatorSet = new AnimatorSet();
animatorSet.play(fabUp).with(getUpAnimation(swipeUpText));
animatorSet.play(fabShake).after(fabUp);
animatorSet.play(fabDown).with(getDownAnimation(swipeUpText)).after(fabShake);
animatorSet.play(getFadeOut(swipeDownText)).with(fabUp);
animatorSet.play(getFadeIn(swipeDownText)).after(fabDown);
animatorSet.play(getShimmer(arrowFour, arrowThree, arrowTwo, arrowOne));
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (animating) animateElements(1000);
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
animatorSet.setStartDelay(delay);
animatorSet.start();
}
private Animator getShimmer(View... targets) {
AnimatorSet animatorSet = new AnimatorSet();
int evenDuration = SHIMMER_TOTAL / targets.length;
int interval = 75;
for (int i=0;i<targets.length;i++) {
animatorSet.play(getShimmer(targets[i], evenDuration + (evenDuration - interval)))
.after(interval * i);
}
return animatorSet;
}
private ObjectAnimator getShimmer(View target, int duration) {
ObjectAnimator shimmer = ObjectAnimator.ofFloat(target, "alpha", 0, 1, 0);
shimmer.setDuration(duration);
return shimmer;
}
private ObjectAnimator getShakeAnimation(View target) {
ObjectAnimator animator = ObjectAnimator.ofFloat(target, "translationX", 0, 25, -25, 25, -25,15, -15, 6, -6, 0);
animator.setDuration(SHAKE_TIME);
return animator;
}
private ObjectAnimator getUpAnimation(View target) {
ObjectAnimator animator = ObjectAnimator.ofFloat(target, "translationY", 0, -1 * ViewUtil.dpToPx(getContext(), 32));
animator.setInterpolator(new AccelerateInterpolator());
animator.setDuration(UP_TIME);
return animator;
}
private ObjectAnimator getDownAnimation(View target) {
ObjectAnimator animator = ObjectAnimator.ofFloat(target, "translationY", 0);
animator.setInterpolator(new DecelerateInterpolator());
animator.setDuration(DOWN_TIME);
return animator;
}
private ObjectAnimator getFadeOut(View target) {
ObjectAnimator animator = ObjectAnimator.ofFloat(target, "alpha", 1, 0);
animator.setDuration(FADE_OUT_TIME);
return animator;
}
private ObjectAnimator getFadeIn(View target) {
ObjectAnimator animator = ObjectAnimator.ofFloat(target, "alpha", 0, 1);
animator.setDuration(FADE_IN_TIME);
return animator;
}
private void resetElements() {
animating = false;
animatorSet.cancel();
swipeUpText.setTranslationY(0);
fab.setTranslationY(0);
swipeDownText.setAlpha(1);
}
public interface AnswerDeclineListener {
void onAnswered();
void onDeclined();
}
}

View File

@ -58,6 +58,7 @@ import org.whispersystems.libsignal.IdentityKey;
*/
public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedListener {
@SuppressWarnings("unused")
private static final String TAG = WebRtcCallScreen.class.getSimpleName();
private ImageView photo;
@ -77,10 +78,11 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi
private RelativeLayout expandedInfo;
private ViewGroup callHeader;
private WebRtcAnswerDeclineButton incomingCallButton;
private Recipient recipient;
private boolean minimized;
private WebRtcIncomingCallOverlay incomingCallOverlay;
public WebRtcCallScreen(Context context) {
super(context);
@ -100,18 +102,21 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi
public void setActiveCall(@NonNull Recipient personInfo, @NonNull String message, @Nullable String sas) {
setCard(personInfo, message);
setConnected(WebRtcCallService.localRenderer, WebRtcCallService.remoteRenderer);
incomingCallOverlay.setActiveCall(sas);
incomingCallButton.stopRingingAnimation();
incomingCallButton.setVisibility(View.GONE);
}
public void setActiveCall(@NonNull Recipient personInfo, @NonNull String message) {
setCard(personInfo, message);
incomingCallOverlay.setActiveCall();
incomingCallButton.stopRingingAnimation();
incomingCallButton.setVisibility(View.GONE);
}
public void setIncomingCall(Recipient personInfo) {
setCard(personInfo, getContext().getString(R.string.CallScreen_Incoming_call));
incomingCallOverlay.setIncomingCall();
endCallButton.setVisibility(View.INVISIBLE);
incomingCallButton.setVisibility(View.VISIBLE);
incomingCallButton.startRingingAnimation();
}
public void setUntrustedIdentity(Recipient personInfo, IdentityKey untrustedIdentity) {
@ -125,7 +130,8 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi
setPersonInfo(personInfo);
this.incomingCallOverlay.setActiveCall();
incomingCallButton.stopRingingAnimation();
incomingCallButton.setVisibility(View.GONE);
this.status.setText(R.string.WebRtcCallScreen_new_safety_number_title);
this.untrustedIdentityContainer.setVisibility(View.VISIBLE);
this.untrustedIdentityExplanation.setText(spannableString);
@ -134,8 +140,8 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi
this.endCallButton.setVisibility(View.INVISIBLE);
}
public void setIncomingCallActionListener(WebRtcIncomingCallOverlay.IncomingCallActionListener listener) {
incomingCallOverlay.setIncomingCallActionListener(listener);
public void setIncomingCallActionListener(WebRtcAnswerDeclineButton.AnswerDeclineListener listener) {
incomingCallButton.setAnswerDeclineListener(listener);
}
public void setAudioMuteButtonListener(WebRtcCallControls.MuteButtonListener listener) {
@ -155,12 +161,7 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi
}
public void setHangupButtonListener(final HangupButtonListener listener) {
endCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onClick();
}
});
endCallButton.setOnClickListener(v -> listener.onClick());
}
public void setAcceptIdentityListener(OnClickListener listener) {
@ -217,34 +218,29 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.webrtc_call_screen, this, true);
this.elapsedTime = (TextView) findViewById(R.id.elapsedTime);
this.photo = (ImageView) findViewById(R.id.photo);
this.localRenderLayout = (PercentFrameLayout) findViewById(R.id.local_render_layout);
this.remoteRenderLayout = (PercentFrameLayout) findViewById(R.id.remote_render_layout);
this.phoneNumber = (TextView) findViewById(R.id.phoneNumber);
this.name = (TextView) findViewById(R.id.name);
this.label = (TextView) findViewById(R.id.label);
this.status = (TextView) findViewById(R.id.callStateLabel);
this.controls = (WebRtcCallControls) findViewById(R.id.inCallControls);
this.endCallButton = (FloatingActionButton) findViewById(R.id.hangup_fab);
this.incomingCallOverlay = (WebRtcIncomingCallOverlay) findViewById(R.id.callControls);
this.elapsedTime = findViewById(R.id.elapsedTime);
this.photo = findViewById(R.id.photo);
this.localRenderLayout = findViewById(R.id.local_render_layout);
this.remoteRenderLayout = findViewById(R.id.remote_render_layout);
this.phoneNumber = findViewById(R.id.phoneNumber);
this.name = findViewById(R.id.name);
this.label = findViewById(R.id.label);
this.status = findViewById(R.id.callStateLabel);
this.controls = findViewById(R.id.inCallControls);
this.endCallButton = findViewById(R.id.hangup_fab);
this.incomingCallButton = findViewById(R.id.answer_decline_button);
this.untrustedIdentityContainer = findViewById(R.id.untrusted_layout);
this.untrustedIdentityExplanation = (TextView) findViewById(R.id.untrusted_explanation);
this.acceptIdentityButton = (Button)findViewById(R.id.accept_safety_numbers);
this.cancelIdentityButton = (Button)findViewById(R.id.cancel_safety_numbers);
this.expandedInfo = (RelativeLayout)findViewById(R.id.expanded_info);
this.callHeader = (ViewGroup)findViewById(R.id.call_info_1);
this.untrustedIdentityExplanation = findViewById(R.id.untrusted_explanation);
this.acceptIdentityButton = findViewById(R.id.accept_safety_numbers);
this.cancelIdentityButton = findViewById(R.id.cancel_safety_numbers);
this.expandedInfo = findViewById(R.id.expanded_info);
this.callHeader = findViewById(R.id.call_info_1);
this.localRenderLayout.setHidden(true);
this.remoteRenderLayout.setHidden(true);
this.minimized = false;
this.remoteRenderLayout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
setMinimized(!minimized);
}
});
this.remoteRenderLayout.setOnClickListener(v -> setMinimized(!minimized));
}
private void setConnected(SurfaceViewRenderer localRenderer,
@ -300,7 +296,7 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi
setPersonInfo(recipient);
this.status.setText(status);
this.untrustedIdentityContainer.setVisibility(View.GONE);
this.endCallButton.setVisibility(View.VISIBLE);
this.endCallButton.show();
}
private void setMinimized(boolean minimized) {
@ -315,12 +311,9 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi
ViewCompat.animate(callHeader).translationY(0);
ViewCompat.animate(status).alpha(1);
ViewCompat.animate(endCallButton).translationY(0);
ViewCompat.animate(endCallButton).alpha(1).withEndAction(new Runnable() {
@Override
public void run() {
// Note: This is to work around an Android bug, see #6225
endCallButton.requestLayout();
}
ViewCompat.animate(endCallButton).alpha(1).withEndAction(() -> {
// Note: This is to work around an Android bug, see #6225
endCallButton.requestLayout();
});
this.minimized = false;
@ -336,8 +329,8 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedLi
});
}
public static interface HangupButtonListener {
public void onClick();
public interface HangupButtonListener {
void onClick();
}

View File

@ -1,113 +0,0 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.Animation;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.multiwaveview.MultiWaveView;
import org.thoughtcrime.securesms.util.Util;
/**
* Displays the controls at the bottom of the in-call screen.
*
* @author Moxie Marlinspike
*
*/
public class WebRtcIncomingCallOverlay extends RelativeLayout {
private MultiWaveView incomingCallWidget;
private TextView redphoneLabel;
public WebRtcIncomingCallOverlay(Context context) {
super(context);
initialize();
}
public WebRtcIncomingCallOverlay(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WebRtcIncomingCallOverlay(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public void setIncomingCall() {
Animation animation = incomingCallWidget.getAnimation();
if (animation != null) {
animation.reset();
incomingCallWidget.clearAnimation();
}
incomingCallWidget.reset(false);
incomingCallWidget.setVisibility(View.VISIBLE);
redphoneLabel.setVisibility(View.VISIBLE);
Util.runOnMainDelayed(new Runnable() {
@Override
public void run() {
if (incomingCallWidget.getVisibility() == View.VISIBLE) {
incomingCallWidget.ping();
Util.runOnMainDelayed(this, 1200);
}
}
}, 500);
}
public void setActiveCall() {
incomingCallWidget.setVisibility(View.GONE);
redphoneLabel.setVisibility(View.GONE);
}
public void setActiveCall(@Nullable String sas) {
setActiveCall();
}
public void reset() {
incomingCallWidget.setVisibility(View.GONE);
redphoneLabel.setVisibility(View.GONE);
}
public void setIncomingCallActionListener(final IncomingCallActionListener listener) {
incomingCallWidget.setOnTriggerListener(new MultiWaveView.OnTriggerListener() {
@Override
public void onTrigger(View v, int target) {
switch (target) {
case 0: listener.onAcceptClick(); break;
case 2: listener.onDenyClick(); break;
}
}
@Override
public void onReleased(View v, int handle) {}
@Override
public void onGrabbedStateChange(View v, int handle) {}
@Override
public void onGrabbed(View v, int handle) {}
});
}
private void initialize() {
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.webrtc_incoming_call_overlay, this, true);
this.incomingCallWidget = (MultiWaveView)findViewById(R.id.incomingCallWidget);
this.redphoneLabel = (TextView)findViewById(R.id.redphone_banner);
}
public static interface IncomingCallActionListener {
public void onAcceptClick();
public void onDenyClick();
}
}