diff options
Diffstat (limited to 'java/com/android/incallui/answer/impl/hint')
16 files changed, 1060 insertions, 0 deletions
diff --git a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml new file mode 100644 index 000000000..b5fa6da8f --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml @@ -0,0 +1,13 @@ +<manifest + package="com.android.incallui.answer.impl.hint" + xmlns:android="http://schemas.android.com/apk/res/android"> + + <application> + <receiver android:name=".EventSecretCodeListener"> + <intent-filter> + <action android:name="android.provider.Telephony.SECRET_CODE" /> + <data android:scheme="android_secret_code" /> + </intent-filter> + </receiver> + </application> +</manifest> diff --git a/java/com/android/incallui/answer/impl/hint/AnswerHint.java b/java/com/android/incallui/answer/impl/hint/AnswerHint.java new file mode 100644 index 000000000..dd3b8228a --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/AnswerHint.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 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 com.android.incallui.answer.impl.hint; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +/** Interface to overlay a hint of how to answer the call. */ +public interface AnswerHint { + + /** + * Inflates the hint's layout into the container. + * + * <p>TODO: if the hint becomes more dependent on other UI elements of the AnswerFragment, + * should put put and hintText into another data structure. + */ + void onCreateView(LayoutInflater inflater, ViewGroup container, View puck, TextView hintText); + + /** Called when the puck bounce animation begins. */ + void onBounceStart(); + + /** + * Called when the bounce animation has ended (transitioned into other animations). The hint + * should reset itself. + */ + void onBounceEnd(); + + /** Called when the call is accepted or rejected through user interaction. */ + void onAnswered(); +} diff --git a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java new file mode 100644 index 000000000..45395a71f --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016 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 com.android.incallui.answer.impl.hint; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import com.android.dialer.common.Assert; +import com.android.dialer.common.ConfigProvider; +import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.common.LogUtil; +import com.android.incallui.util.AccessibilityUtil; +import java.util.Calendar; + +/** + * Selects a AnswerHint to show. If there's no suitable hints {@link EmptyAnswerHint} will be used, + * which does nothing. + */ +public class AnswerHintFactory { + + private static final String CONFIG_ANSWER_HINT_ANSWERED_THRESHOLD_KEY = + "answer_hint_answered_threshold"; + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String CONFIG_ANSWER_HINT_WHITELISTED_DEVICES_KEY = + "answer_hint_whitelisted_devices"; + // Most popular devices released before NDR1 is whitelisted. Their user are likely to have seen + // the legacy UI. + private static final String DEFAULT_WHITELISTED_DEVICES_CSV = + "/hammerhead//bullhead//angler//shamu//gm4g//gm4g_s//AQ4501//gce_x86_phone//gm4gtkc_s/" + + "/Sparkle_V//Mi-498//AQ4502//imobileiq2//A65//H940//m8_google//m0xx//A10//ctih220/" + + "/Mi438S//bacon/"; + + @VisibleForTesting + static final String ANSWERED_COUNT_PREFERENCE_KEY = "answer_hint_answered_count"; + + private final EventPayloadLoader eventPayloadLoader; + + public AnswerHintFactory(@NonNull EventPayloadLoader eventPayloadLoader) { + this.eventPayloadLoader = Assert.isNotNull(eventPayloadLoader); + } + + @NonNull + public AnswerHint create(Context context, long puckUpDuration, long puckUpDelay) { + + if (shouldShowAnswerHint( + context, + ConfigProviderBindings.get(context), + getDeviceProtectedPreferences(context), + Build.PRODUCT)) { + return new DotAnswerHint(context, puckUpDuration, puckUpDelay); + } + + // Display the event answer hint if the payload is available. + Drawable eventPayload = + eventPayloadLoader.loadPayload( + context, System.currentTimeMillis(), Calendar.getInstance().getTimeZone()); + if (eventPayload != null) { + return new EventAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay); + } + + return new EmptyAnswerHint(); + } + + public static void increaseAnsweredCount(Context context) { + SharedPreferences sharedPreferences = getDeviceProtectedPreferences(context); + int answeredCount = sharedPreferences.getInt(ANSWERED_COUNT_PREFERENCE_KEY, 0); + sharedPreferences.edit().putInt(ANSWERED_COUNT_PREFERENCE_KEY, answeredCount + 1).apply(); + } + + @VisibleForTesting + static boolean shouldShowAnswerHint( + Context context, + ConfigProvider configProvider, + SharedPreferences sharedPreferences, + String device) { + if (AccessibilityUtil.isTouchExplorationEnabled(context)) { + return false; + } + // Devices that has the legacy dialer installed are whitelisted as they are likely to go through + // a UX change during updates. + if (!isDeviceWhitelisted(device, configProvider)) { + return false; + } + + // If the user has gone through the process a few times we can assume they have learnt the + // method. + int answeredCount = sharedPreferences.getInt(ANSWERED_COUNT_PREFERENCE_KEY, 0); + long threshold = configProvider.getLong(CONFIG_ANSWER_HINT_ANSWERED_THRESHOLD_KEY, 3); + LogUtil.i( + "AnswerHintFactory.shouldShowAnswerHint", + "answerCount: %d, threshold: %d", + answeredCount, + threshold); + return answeredCount < threshold; + } + + /** + * @param device should be the value of{@link Build#PRODUCT}. + * @param configProvider should provide a list of devices quoted with '/' concatenated to a + * string. + */ + private static boolean isDeviceWhitelisted(String device, ConfigProvider configProvider) { + return configProvider + .getString(CONFIG_ANSWER_HINT_WHITELISTED_DEVICES_KEY, DEFAULT_WHITELISTED_DEVICES_CSV) + .contains("/" + device + "/"); + } + + private static SharedPreferences getDeviceProtectedPreferences(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return PreferenceManager.getDefaultSharedPreferences(context); + } + return PreferenceManager.getDefaultSharedPreferences( + context.createDeviceProtectedStorageContext()); + } +} diff --git a/java/com/android/incallui/answer/impl/hint/DotAnswerHint.java b/java/com/android/incallui/answer/impl/hint/DotAnswerHint.java new file mode 100644 index 000000000..394fe5808 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/DotAnswerHint.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2016 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 com.android.incallui.answer.impl.hint; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.support.annotation.DimenRes; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.widget.TextView; + +/** An Answer hint that uses a green swiping dot. */ +public class DotAnswerHint implements AnswerHint { + + private static final float ANSWER_HINT_SMALL_ALPHA = 0.8f; + private static final float ANSWER_HINT_MID_ALPHA = 0.5f; + private static final float ANSWER_HINT_LARGE_ALPHA = 0.2f; + + private static final long FADE_IN_DELAY_SCALE_MILLIS = 380; + private static final long FADE_IN_DURATION_SCALE_MILLIS = 200; + private static final long FADE_IN_DELAY_ALPHA_MILLIS = 340; + private static final long FADE_IN_DURATION_ALPHA_MILLIS = 50; + + private static final long SWIPE_UP_DURATION_ALPHA_MILLIS = 500; + + private static final long FADE_OUT_DELAY_SCALE_SMALL_MILLIS = 90; + private static final long FADE_OUT_DELAY_SCALE_MID_MILLIS = 70; + private static final long FADE_OUT_DELAY_SCALE_LARGE_MILLIS = 10; + private static final long FADE_OUT_DURATION_SCALE_MILLIS = 100; + private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130; + private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170; + + private final Context context; + private final long puckUpDurationMillis; + private final long puckUpDelayMillis; + + private View puck; + + private View answerHintSmall; + private View answerHintMid; + private View answerHintLarge; + private View answerHintContainer; + private AnimatorSet answerGestureHintAnim; + + public DotAnswerHint(Context context, long puckUpDurationMillis, long puckUpDelayMillis) { + this.context = context; + this.puckUpDurationMillis = puckUpDurationMillis; + this.puckUpDelayMillis = puckUpDelayMillis; + } + + @Override + public void onCreateView( + LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) { + this.puck = puck; + View view = inflater.inflate(R.layout.dot_hint, container, true); + answerHintContainer = view.findViewById(R.id.answer_hint_container); + answerHintSmall = view.findViewById(R.id.answer_hint_small); + answerHintMid = view.findViewById(R.id.answer_hint_mid); + answerHintLarge = view.findViewById(R.id.answer_hint_large); + hintText.setTextSize( + TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size)); + } + + @Override + public void onBounceStart() { + if (answerGestureHintAnim == null) { + answerGestureHintAnim = new AnimatorSet(); + answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset)); + + Animator fadeIn = createFadeIn(); + + Animator swipeUp = + ObjectAnimator.ofFloat( + answerHintContainer, + View.TRANSLATION_Y, + puck.getY() - getDimension(R.dimen.hint_offset)); + swipeUp.setInterpolator(new FastOutSlowInInterpolator()); + swipeUp.setDuration(SWIPE_UP_DURATION_ALPHA_MILLIS); + + Animator fadeOut = createFadeOut(); + + answerGestureHintAnim.play(fadeIn).after(puckUpDelayMillis); + answerGestureHintAnim.play(swipeUp).after(fadeIn); + // The fade out should start fading the alpha just as the puck is dropping. Scaling will start + // a bit earlier. + answerGestureHintAnim + .play(fadeOut) + .after(puckUpDelayMillis + puckUpDurationMillis - FADE_OUT_DELAY_ALPHA_MILLIS); + + fadeIn.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + answerHintSmall.setAlpha(0); + answerHintSmall.setScaleX(1); + answerHintSmall.setScaleY(1); + answerHintMid.setAlpha(0); + answerHintMid.setScaleX(1); + answerHintMid.setScaleY(1); + answerHintLarge.setAlpha(0); + answerHintLarge.setScaleX(1); + answerHintLarge.setScaleY(1); + answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset)); + answerHintContainer.setVisibility(View.VISIBLE); + } + }); + } + + answerGestureHintAnim.start(); + } + + private Animator createFadeIn() { + AnimatorSet set = new AnimatorSet(); + set.play( + createFadeInScaleAndAlpha( + answerHintSmall, + R.dimen.hint_small_begin_size, + R.dimen.hint_small_end_size, + ANSWER_HINT_SMALL_ALPHA)) + .with( + createFadeInScaleAndAlpha( + answerHintMid, + R.dimen.hint_mid_begin_size, + R.dimen.hint_mid_end_size, + ANSWER_HINT_MID_ALPHA)) + .with( + createFadeInScaleAndAlpha( + answerHintLarge, + R.dimen.hint_large_begin_size, + R.dimen.hint_large_end_size, + ANSWER_HINT_LARGE_ALPHA)); + return set; + } + + private Animator createFadeInScaleAndAlpha( + View target, @DimenRes int beginSize, @DimenRes int endSize, float endAlpha) { + Animator scale = + createUniformScaleAnimator( + target, + getDimension(beginSize), + getDimension(beginSize), + getDimension(endSize), + FADE_IN_DURATION_SCALE_MILLIS, + FADE_IN_DELAY_SCALE_MILLIS, + new LinearInterpolator()); + Animator alpha = + createAlphaAnimator( + target, + 0f, + endAlpha, + FADE_IN_DURATION_ALPHA_MILLIS, + FADE_IN_DELAY_ALPHA_MILLIS, + new LinearInterpolator()); + AnimatorSet set = new AnimatorSet(); + set.play(scale).with(alpha); + return set; + } + + private Animator createFadeOut() { + AnimatorSet set = new AnimatorSet(); + set.play( + createFadeOutScaleAndAlpha( + answerHintSmall, + R.dimen.hint_small_begin_size, + R.dimen.hint_small_end_size, + FADE_OUT_DELAY_SCALE_SMALL_MILLIS, + ANSWER_HINT_SMALL_ALPHA)) + .with( + createFadeOutScaleAndAlpha( + answerHintMid, + R.dimen.hint_mid_begin_size, + R.dimen.hint_mid_end_size, + FADE_OUT_DELAY_SCALE_MID_MILLIS, + ANSWER_HINT_MID_ALPHA)) + .with( + createFadeOutScaleAndAlpha( + answerHintLarge, + R.dimen.hint_large_begin_size, + R.dimen.hint_large_end_size, + FADE_OUT_DELAY_SCALE_LARGE_MILLIS, + ANSWER_HINT_LARGE_ALPHA)); + return set; + } + + private Animator createFadeOutScaleAndAlpha( + View target, + @DimenRes int beginSize, + @DimenRes int endSize, + long scaleDelay, + float endAlpha) { + Animator scale = + createUniformScaleAnimator( + target, + getDimension(beginSize), + getDimension(endSize), + getDimension(beginSize), + FADE_OUT_DURATION_SCALE_MILLIS, + scaleDelay, + new LinearInterpolator()); + Animator alpha = + createAlphaAnimator( + target, + endAlpha, + 0.0f, + FADE_OUT_DURATION_ALPHA_MILLIS, + FADE_OUT_DELAY_ALPHA_MILLIS, + new LinearInterpolator()); + AnimatorSet set = new AnimatorSet(); + set.play(scale).with(alpha); + return set; + } + + @Override + public void onBounceEnd() { + if (answerGestureHintAnim != null) { + answerGestureHintAnim.end(); + answerGestureHintAnim = null; + answerHintContainer.setVisibility(View.GONE); + } + } + + @Override + public void onAnswered() { + AnswerHintFactory.increaseAnsweredCount(context); + } + + private float getDimension(@DimenRes int id) { + return context.getResources().getDimension(id); + } + + private static Animator createUniformScaleAnimator( + View target, + float original, + float begin, + float end, + long duration, + long delay, + Interpolator interpolator) { + float scaleBegin = begin / original; + float scaleEnd = end / original; + Animator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, scaleBegin, scaleEnd); + Animator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, scaleBegin, scaleEnd); + scaleX.setDuration(duration); + scaleY.setDuration(duration); + scaleX.setInterpolator(interpolator); + scaleY.setInterpolator(interpolator); + AnimatorSet set = new AnimatorSet(); + set.play(scaleX).with(scaleY).after(delay); + return set; + } + + private static Animator createAlphaAnimator( + View target, float begin, float end, long duration, long delay, Interpolator interpolator) { + Animator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, begin, end); + alpha.setDuration(duration); + alpha.setInterpolator(interpolator); + alpha.setStartDelay(delay); + return alpha; + } +} diff --git a/java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java b/java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java new file mode 100644 index 000000000..e52b4ee36 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016 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 com.android.incallui.answer.impl.hint; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +/** Does nothing. Used to avoid null checks on AnswerHint. */ +public class EmptyAnswerHint implements AnswerHint { + + @Override + public void onCreateView( + LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {} + + @Override + public void onBounceStart() {} + + @Override + public void onBounceEnd() {} + + @Override + public void onAnswered() {} +} diff --git a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java b/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java new file mode 100644 index 000000000..7ee327d50 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2016 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 com.android.incallui.answer.impl.hint; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.DimenRes; +import android.support.annotation.NonNull; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.dialer.common.Assert; + +/** + * An Answer hint that animates a {@link Drawable} payload with animation similar to {@link + * DotAnswerHint}. + */ +public final class EventAnswerHint implements AnswerHint { + + private static final long FADE_IN_DELAY_SCALE_MILLIS = 380; + private static final long FADE_IN_DURATION_SCALE_MILLIS = 200; + private static final long FADE_IN_DELAY_ALPHA_MILLIS = 340; + private static final long FADE_IN_DURATION_ALPHA_MILLIS = 50; + + private static final long SWIPE_UP_DURATION_ALPHA_MILLIS = 500; + + private static final long FADE_OUT_DELAY_SCALE_SMALL_MILLIS = 90; + private static final long FADE_OUT_DURATION_SCALE_MILLIS = 100; + private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130; + private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170; + + private static final float FADE_SCALE = 1.2f; + + private final Context context; + private final Drawable payload; + private final long puckUpDurationMillis; + private final long puckUpDelayMillis; + + private View puck; + private View payloadView; + private View answerHintContainer; + private AnimatorSet answerGestureHintAnim; + + public EventAnswerHint( + @NonNull Context context, + @NonNull Drawable payload, + long puckUpDurationMillis, + long puckUpDelayMillis) { + this.context = Assert.isNotNull(context); + this.payload = Assert.isNotNull(payload); + this.puckUpDurationMillis = puckUpDurationMillis; + this.puckUpDelayMillis = puckUpDelayMillis; + } + + @Override + public void onCreateView( + LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) { + this.puck = puck; + View view = inflater.inflate(R.layout.event_hint, container, true); + answerHintContainer = view.findViewById(R.id.answer_hint_container); + payloadView = view.findViewById(R.id.payload); + hintText.setTextSize( + TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size)); + ((ImageView) payloadView).setImageDrawable(payload); + } + + @Override + public void onBounceStart() { + if (answerGestureHintAnim == null) { + + answerGestureHintAnim = new AnimatorSet(); + answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset)); + + Animator fadeIn = createFadeIn(); + + Animator swipeUp = + ObjectAnimator.ofFloat( + answerHintContainer, + View.TRANSLATION_Y, + puck.getY() - getDimension(R.dimen.hint_offset)); + swipeUp.setInterpolator(new FastOutSlowInInterpolator()); + swipeUp.setDuration(SWIPE_UP_DURATION_ALPHA_MILLIS); + + Animator fadeOut = createFadeOut(); + + answerGestureHintAnim.play(fadeIn).after(puckUpDelayMillis); + answerGestureHintAnim.play(swipeUp).after(fadeIn); + // The fade out should start fading the alpha just as the puck is dropping. Scaling will start + // a bit earlier. + answerGestureHintAnim + .play(fadeOut) + .after(puckUpDelayMillis + puckUpDurationMillis - FADE_OUT_DELAY_ALPHA_MILLIS); + + fadeIn.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + payloadView.setAlpha(0); + payloadView.setScaleX(1); + payloadView.setScaleY(1); + answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset)); + answerHintContainer.setVisibility(View.VISIBLE); + } + }); + } + + answerGestureHintAnim.start(); + } + + private Animator createFadeIn() { + AnimatorSet set = new AnimatorSet(); + set.play(createFadeInScaleAndAlpha(payloadView)); + return set; + } + + private static Animator createFadeInScaleAndAlpha(View target) { + Animator scale = + createUniformScaleAnimator( + target, + FADE_SCALE, + 1.0f, + FADE_IN_DURATION_SCALE_MILLIS, + FADE_IN_DELAY_SCALE_MILLIS, + new LinearInterpolator()); + Animator alpha = + createAlphaAnimator( + target, + 0f, + 1.0f, + FADE_IN_DURATION_ALPHA_MILLIS, + FADE_IN_DELAY_ALPHA_MILLIS, + new LinearInterpolator()); + AnimatorSet set = new AnimatorSet(); + set.play(scale).with(alpha); + return set; + } + + private Animator createFadeOut() { + AnimatorSet set = new AnimatorSet(); + set.play(createFadeOutScaleAndAlpha(payloadView, FADE_OUT_DELAY_SCALE_SMALL_MILLIS)); + return set; + } + + private static Animator createFadeOutScaleAndAlpha(View target, long scaleDelay) { + Animator scale = + createUniformScaleAnimator( + target, + 1.0f, + FADE_SCALE, + FADE_OUT_DURATION_SCALE_MILLIS, + scaleDelay, + new LinearInterpolator()); + Animator alpha = + createAlphaAnimator( + target, + 01.0f, + 0.0f, + FADE_OUT_DURATION_ALPHA_MILLIS, + FADE_OUT_DELAY_ALPHA_MILLIS, + new LinearInterpolator()); + AnimatorSet set = new AnimatorSet(); + set.play(scale).with(alpha); + return set; + } + + @Override + public void onBounceEnd() { + if (answerGestureHintAnim != null) { + answerGestureHintAnim.end(); + answerGestureHintAnim = null; + answerHintContainer.setVisibility(View.GONE); + } + } + + @Override + public void onAnswered() { + // Do nothing + } + + private float getDimension(@DimenRes int id) { + return context.getResources().getDimension(id); + } + + private static Animator createUniformScaleAnimator( + View target, + float scaleBegin, + float scaleEnd, + long duration, + long delay, + Interpolator interpolator) { + Animator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, scaleBegin, scaleEnd); + Animator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, scaleBegin, scaleEnd); + scaleX.setDuration(duration); + scaleY.setDuration(duration); + scaleX.setInterpolator(interpolator); + scaleY.setInterpolator(interpolator); + AnimatorSet set = new AnimatorSet(); + set.play(scaleX).with(scaleY).after(delay); + return set; + } + + private static Animator createAlphaAnimator( + View target, float begin, float end, long duration, long delay, Interpolator interpolator) { + Animator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, begin, end); + alpha.setDuration(duration); + alpha.setInterpolator(interpolator); + alpha.setStartDelay(delay); + return alpha; + } +} diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java new file mode 100644 index 000000000..09e3bedf2 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 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 com.android.incallui.answer.impl.hint; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import java.util.TimeZone; + +/** Loads a {@link Drawable} payload for the {@link EventAnswerHint} if it should be displayed. */ +public interface EventPayloadLoader { + @Nullable + Drawable loadPayload( + @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone); +} diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java new file mode 100644 index 000000000..bd8d73645 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016 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 com.android.incallui.answer.impl.hint; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build.VERSION_CODES; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import com.android.dialer.common.Assert; +import com.android.dialer.common.ConfigProvider; +import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.common.LogUtil; +import java.io.InputStream; +import java.util.TimeZone; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +/** Decrypt the event payload to be shown if in a specific time range and the key is received. */ +@TargetApi(VERSION_CODES.M) +public final class EventPayloadLoaderImpl implements EventPayloadLoader { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String CONFIG_EVENT_KEY = "event_key"; + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String CONFIG_EVENT_BINARY = "event_binary"; + + // Time is stored as a UTC UNIX timestamp in milliseconds, but interpreted as local time. + // For example, 946684800 (2000/1/1 00:00:00 @UTC) is the new year midnight at every timezone. + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS = "event_time_start_millis"; + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS = "event_time_end_millis"; + + @Override + @Nullable + public Drawable loadPayload( + @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone) { + Assert.isNotNull(context); + Assert.isNotNull(timeZone); + ConfigProvider configProvider = ConfigProviderBindings.get(context); + + String pbeKey = configProvider.getString(CONFIG_EVENT_KEY, null); + if (pbeKey == null) { + return null; + } + long timeRangeStart = configProvider.getLong(CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS, 0); + long timeRangeEnd = configProvider.getLong(CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS, 0); + + String eventBinary = configProvider.getString(CONFIG_EVENT_BINARY, null); + if (eventBinary == null) { + return null; + } + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + if (!preferences.getBoolean( + EventSecretCodeListener.EVENT_ENABLED_WITH_SECRET_CODE_KEY, false)) { + long localTimestamp = currentTimeUtcMillis + timeZone.getRawOffset(); + + if (localTimestamp < timeRangeStart) { + return null; + } + + if (localTimestamp > timeRangeEnd) { + return null; + } + } + + // Use openssl aes-128-cbc -in <input> -out <output> -pass <PBEKey> to generate the asset + try (InputStream input = context.getAssets().open(eventBinary)) { + byte[] encryptedFile = new byte[input.available()]; + input.read(encryptedFile); + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", "BC"); + + byte[] salt = new byte[8]; + System.arraycopy(encryptedFile, 8, salt, 0, 8); + SecretKey key = + SecretKeyFactory.getInstance("PBEWITHMD5AND128BITAES-CBC-OPENSSL", "BC") + .generateSecret(new PBEKeySpec(pbeKey.toCharArray(), salt, 100)); + cipher.init(Cipher.DECRYPT_MODE, key); + + byte[] decryptedFile = cipher.doFinal(encryptedFile, 16, encryptedFile.length - 16); + + return new BitmapDrawable( + context.getResources(), + BitmapFactory.decodeByteArray(decryptedFile, 0, decryptedFile.length)); + } catch (Exception e) { + // Avoid crashing dialer for any reason. + LogUtil.e("EventPayloadLoader.loadPayload", "error decrypting payload:", e); + return null; + } + } +} diff --git a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java new file mode 100644 index 000000000..7cf4054a9 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 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 com.android.incallui.answer.impl.hint; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.widget.Toast; +import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; + +/** + * Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint. + */ +public class EventSecretCodeListener extends BroadcastReceiver { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String CONFIG_EVENT_SECRET_CODE = "event_secret_code"; + + public static final String EVENT_ENABLED_WITH_SECRET_CODE_KEY = "event_enabled_with_secret_code"; + + @Override + public void onReceive(Context context, Intent intent) { + String host = intent.getData().getHost(); + String secretCode = + ConfigProviderBindings.get(context).getString(CONFIG_EVENT_SECRET_CODE, null); + if (secretCode == null) { + return; + } + if (!TextUtils.equals(secretCode, host)) { + return; + } + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean wasEnabled = preferences.getBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false); + if (wasEnabled) { + preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false).apply(); + Toast.makeText(context, R.string.event_deactivated, Toast.LENGTH_SHORT).show(); + Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_DEACTIVATED); + LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint disabled"); + } else { + preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, true).apply(); + Toast.makeText(context, R.string.event_activated, Toast.LENGTH_SHORT).show(); + Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_ACTIVATED); + LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint enabled"); + } + } +} diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml new file mode 100644 index 000000000..f585ce5c9 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> + <solid android:color="#00C853"/> +</shape>
\ No newline at end of file diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml new file mode 100644 index 000000000..f585ce5c9 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> + <solid android:color="#00C853"/> +</shape>
\ No newline at end of file diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml new file mode 100644 index 000000000..6a24d6a5f --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> + <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> + <solid android:color="#00C853"/> + <stroke android:color="#00C853" android:width="2dp"/> + </shape>
\ No newline at end of file diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml new file mode 100644 index 000000000..84b10e736 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/answer_hint_container" + android:layout_width="160dp" + android:layout_height="160dp" + android:layout_gravity="center_horizontal" + android:visibility="gone"> + <ImageView + android:id="@+id/answer_hint_large" + android:layout_width="@dimen/hint_large_begin_size" + android:layout_height="@dimen/hint_large_begin_size" + android:layout_gravity="center" + android:alpha="0" + android:src="@drawable/answer_hint_large"/> + <ImageView + android:id="@+id/answer_hint_mid" + android:layout_width="@dimen/hint_mid_begin_size" + android:layout_height="@dimen/hint_mid_begin_size" + android:src="@drawable/answer_hint_mid" + android:alpha="0" + android:layout_gravity="center"/> + <ImageView + android:id="@+id/answer_hint_small" + android:layout_width="@dimen/hint_small_begin_size" + android:layout_height="@dimen/hint_small_begin_size" + android:src="@drawable/answer_hint_small" + android:alpha="0" + android:layout_gravity="center" /> +</FrameLayout>
\ No newline at end of file diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml new file mode 100644 index 000000000..d505014c1 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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 + --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/answer_hint_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center_horizontal" + android:clipChildren="false" + android:clipToPadding="false" + android:visibility="gone"> + <ImageView + android:id="@+id/payload" + android:layout_width="191dp" + android:layout_height="773dp" + android:layout_gravity="center" + android:alpha="0" + android:rotation="-30" + android:transformPivotY="90dp" + android:clipChildren="false" + android:clipToPadding="false"/> +</FrameLayout>
\ No newline at end of file diff --git a/java/com/android/incallui/answer/impl/hint/res/values/dimens.xml b/java/com/android/incallui/answer/impl/hint/res/values/dimens.xml new file mode 100644 index 000000000..d86084b74 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/res/values/dimens.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="hint_text_size">18sp</dimen> + <dimen name="hint_initial_offset">-100dp</dimen> + <dimen name="hint_offset">300dp</dimen> + <dimen name="hint_small_begin_size">50dp</dimen> + <dimen name="hint_small_end_size">42dp</dimen> + <dimen name="hint_mid_begin_size">56dp</dimen> + <dimen name="hint_mid_end_size">64dp</dimen> + <dimen name="hint_large_begin_size">64dp</dimen> + <dimen name="hint_large_end_size">160dp</dimen> +</resources>
\ No newline at end of file diff --git a/java/com/android/incallui/answer/impl/hint/res/values/strings.xml b/java/com/android/incallui/answer/impl/hint/res/values/strings.xml new file mode 100644 index 000000000..d76021ae1 --- /dev/null +++ b/java/com/android/incallui/answer/impl/hint/res/values/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="event_activated">Event Activated</string> + <string name="event_deactivated">Event Deactvated</string> +</resources>
\ No newline at end of file |