CallCardFragment.java revision d9e9c76c4828a56c31ee2dcc3164f5c760395b47
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.incallui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.animation.ValueAnimator;
24import android.app.Activity;
25import android.content.Context;
26import android.graphics.Point;
27import android.graphics.drawable.Drawable;
28import android.os.Bundle;
29import android.telephony.DisconnectCause;
30import android.text.TextUtils;
31import android.view.Display;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.ViewGroup;
35import android.view.ViewTreeObserver;
36import android.view.ViewTreeObserver.OnGlobalLayoutListener;
37import android.view.accessibility.AccessibilityEvent;
38import android.view.animation.Interpolator;
39import android.view.animation.PathInterpolator;
40import android.widget.ImageButton;
41import android.widget.ImageView;
42import android.widget.TextView;
43
44import com.android.contacts.common.animation.AnimUtils;
45import com.android.contacts.common.util.ViewUtil;
46
47import java.util.List;
48
49/**
50 * Fragment for call card.
51 */
52public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPresenter.CallCardUi>
53        implements CallCardPresenter.CallCardUi {
54
55    private static final int REVEAL_ANIMATION_DURATION = 500;
56    private static final int SHRINK_ANIMATION_DURATION = 700;
57
58    // Primary caller info
59    private TextView mPhoneNumber;
60    private TextView mNumberLabel;
61    private TextView mPrimaryName;
62    private TextView mCallStateLabel;
63    private TextView mCallTypeLabel;
64    private View mCallNumberAndLabel;
65    private ImageView mPhoto;
66    private TextView mElapsedTime;
67
68    // Container view that houses the entire primary call card, including the call buttons
69    private View mPrimaryCallCardContainer;
70    // Container view that houses the primary call information
71    private View mPrimaryCallInfo;
72    private View mCallButtonsContainer;
73
74    // Secondary caller info
75    private View mSecondaryCallInfo;
76    private TextView mSecondaryCallName;
77
78    private View mEndCallButton;
79    private ImageButton mHandoffButton;
80
81    private final Interpolator mAnimationInterpolator = new PathInterpolator(0.4f, 0, 0.2f, 1);
82
83    // Cached DisplayMetrics density.
84    private float mDensity;
85
86    private float mTranslationOffset;
87
88    @Override
89    CallCardPresenter.CallCardUi getUi() {
90        return this;
91    }
92
93    @Override
94    CallCardPresenter createPresenter() {
95        return new CallCardPresenter();
96    }
97
98    @Override
99    public void onCreate(Bundle savedInstanceState) {
100        super.onCreate(savedInstanceState);
101    }
102
103
104    @Override
105    public void onActivityCreated(Bundle savedInstanceState) {
106        super.onActivityCreated(savedInstanceState);
107
108        final CallList calls = CallList.getInstance();
109        final Call call = calls.getFirstCall();
110        getPresenter().init(getActivity(), call);
111    }
112
113    @Override
114    public View onCreateView(LayoutInflater inflater, ViewGroup container,
115            Bundle savedInstanceState) {
116        super.onCreateView(inflater, container, savedInstanceState);
117
118        mDensity = getResources().getDisplayMetrics().density;
119        mTranslationOffset =
120                getResources().getDimensionPixelSize(R.dimen.call_card_anim_translate_y_offset);
121
122        return inflater.inflate(R.layout.call_card, container, false);
123    }
124
125    @Override
126    public void onViewCreated(View view, Bundle savedInstanceState) {
127        super.onViewCreated(view, savedInstanceState);
128
129        mPhoneNumber = (TextView) view.findViewById(R.id.phoneNumber);
130        mPrimaryName = (TextView) view.findViewById(R.id.name);
131        mNumberLabel = (TextView) view.findViewById(R.id.label);
132        mSecondaryCallInfo = (View) view.findViewById(R.id.secondary_call_info);
133        mPhoto = (ImageView) view.findViewById(R.id.photo);
134        mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel);
135        mCallNumberAndLabel = view.findViewById(R.id.labelAndNumber);
136        mCallTypeLabel = (TextView) view.findViewById(R.id.callTypeLabel);
137        mElapsedTime = (TextView) view.findViewById(R.id.elapsedTime);
138        mPrimaryCallCardContainer = view.findViewById(R.id.primary_call_info_container);
139        mPrimaryCallInfo = view.findViewById(R.id.primary_call_banner);
140        mCallButtonsContainer = view.findViewById(R.id.callButtonFragment);
141
142        mEndCallButton = view.findViewById(R.id.endButton);
143        mEndCallButton.setOnClickListener(new View.OnClickListener() {
144            @Override
145            public void onClick(View v) {
146                getPresenter().endCallClicked();
147            }
148        });
149        ViewUtil.setupFloatingActionButton(mEndCallButton, getResources());
150
151        mHandoffButton = (ImageButton) view.findViewById(R.id.handoffButton);
152        mHandoffButton.setOnClickListener(new View.OnClickListener() {
153            @Override public void onClick(View v) {
154                getPresenter().connectionHandoffClicked();
155            }
156        });
157        ViewUtil.setupFloatingActionButton(mHandoffButton, getResources());
158    }
159
160    @Override
161    public void setVisible(boolean on) {
162        if (on) {
163            getView().setVisibility(View.VISIBLE);
164        } else {
165            getView().setVisibility(View.INVISIBLE);
166        }
167    }
168
169    public void setShowConnectionHandoff(boolean showConnectionHandoff) {
170        Log.v(this, "setShowConnectionHandoff: " + showConnectionHandoff);
171    }
172
173    @Override
174    public void setPrimaryName(String name, boolean nameIsNumber) {
175        if (TextUtils.isEmpty(name)) {
176            mPrimaryName.setText("");
177        } else {
178            mPrimaryName.setText(name);
179
180            // Set direction of the name field
181            int nameDirection = View.TEXT_DIRECTION_INHERIT;
182            if (nameIsNumber) {
183                nameDirection = View.TEXT_DIRECTION_LTR;
184            }
185            mPrimaryName.setTextDirection(nameDirection);
186        }
187    }
188
189    @Override
190    public void setPrimaryImage(Drawable image) {
191        if (image != null) {
192            setDrawableToImageView(mPhoto, image);
193        }
194    }
195
196    @Override
197    public void setPrimaryPhoneNumber(String number) {
198        // Set the number
199        if (TextUtils.isEmpty(number)) {
200            mPhoneNumber.setText("");
201            mPhoneNumber.setVisibility(View.GONE);
202        } else {
203            mPhoneNumber.setText(number);
204            mPhoneNumber.setVisibility(View.VISIBLE);
205            mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR);
206        }
207    }
208
209    @Override
210    public void setPrimaryLabel(String label) {
211        if (!TextUtils.isEmpty(label)) {
212            mNumberLabel.setText(label);
213            mNumberLabel.setVisibility(View.VISIBLE);
214        } else {
215            mNumberLabel.setVisibility(View.GONE);
216        }
217
218    }
219
220    @Override
221    public void setPrimary(String number, String name, boolean nameIsNumber, String label,
222            Drawable photo, boolean isConference, boolean isGeneric, boolean isSipCall) {
223        Log.d(this, "Setting primary call");
224
225        if (isConference) {
226            name = getConferenceString(isGeneric);
227            photo = getConferencePhoto(isGeneric);
228            nameIsNumber = false;
229        }
230
231        // set the name field.
232        setPrimaryName(name, nameIsNumber);
233
234        if (TextUtils.isEmpty(number) && TextUtils.isEmpty(label)) {
235            mCallNumberAndLabel.setVisibility(View.GONE);
236        } else {
237            mCallNumberAndLabel.setVisibility(View.VISIBLE);
238        }
239
240        setPrimaryPhoneNumber(number);
241
242        // Set the label (Mobile, Work, etc)
243        setPrimaryLabel(label);
244
245        showInternetCallLabel(isSipCall);
246
247        setDrawableToImageView(mPhoto, photo);
248    }
249
250    @Override
251    public void setSecondary(boolean show, String name, boolean nameIsNumber, String label,
252            boolean isConference, boolean isGeneric) {
253
254        if (show) {
255            if (isConference) {
256                name = getConferenceString(isGeneric);
257                nameIsNumber = false;
258            }
259
260            showAndInitializeSecondaryCallInfo();
261            mSecondaryCallName.setText(name);
262
263            int nameDirection = View.TEXT_DIRECTION_INHERIT;
264            if (nameIsNumber) {
265                nameDirection = View.TEXT_DIRECTION_LTR;
266            }
267            mSecondaryCallName.setTextDirection(nameDirection);
268        } else {
269            mSecondaryCallInfo.setVisibility(View.GONE);
270        }
271    }
272
273    @Override
274    public void setCallState(int state, int cause, boolean bluetoothOn, String gatewayLabel,
275            String gatewayNumber, boolean isWiFi, boolean isHandoffCapable,
276            boolean isHandoffPending) {
277        String callStateLabel = null;
278
279        if (Call.State.isDialing(state) && !TextUtils.isEmpty(gatewayLabel)) {
280            // Provider info: (e.g. "Calling via <gatewayLabel>")
281            callStateLabel = gatewayLabel;
282        } else {
283            callStateLabel = getCallStateLabelFromState(state, cause);
284        }
285
286        Log.v(this, "setCallState " + callStateLabel);
287        Log.v(this, "DisconnectCause " + DisconnectCause.toString(cause));
288        Log.v(this, "bluetooth on " + bluetoothOn);
289        Log.v(this, "gateway " + gatewayLabel + gatewayNumber);
290        Log.v(this, "isWiFi " + isWiFi);
291        Log.v(this, "isHandoffCapable " + isHandoffCapable);
292        Log.v(this, "isHandoffPending " + isHandoffPending);
293
294        // Update the call state label.
295        if (!TextUtils.isEmpty(callStateLabel)) {
296            mCallStateLabel.setText(callStateLabel);
297            mCallStateLabel.setVisibility(View.VISIBLE);
298        } else {
299            mCallStateLabel.setVisibility(View.GONE);
300        }
301
302        if (Call.State.INCOMING == state) {
303            setBluetoothOn(bluetoothOn);
304        }
305
306        mHandoffButton.setEnabled(isHandoffCapable && !isHandoffPending);
307        mHandoffButton.setVisibility(mHandoffButton.isEnabled() ? View.VISIBLE : View.GONE);
308        mHandoffButton.setImageResource(isWiFi ?
309                R.drawable.ic_in_call_wifi : R.drawable.ic_in_call_pstn);
310    }
311
312    private void showInternetCallLabel(boolean show) {
313        if (show) {
314            final String label = getView().getContext().getString(
315                    R.string.incall_call_type_label_sip);
316            mCallTypeLabel.setVisibility(View.VISIBLE);
317            mCallTypeLabel.setText(label);
318        } else {
319            mCallTypeLabel.setVisibility(View.GONE);
320        }
321    }
322
323    @Override
324    public void setPrimaryCallElapsedTime(boolean show, String callTimeElapsed) {
325        if (show) {
326            if (mElapsedTime.getVisibility() != View.VISIBLE) {
327                AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION);
328            }
329            mElapsedTime.setText(callTimeElapsed);
330        } else {
331            // hide() animation has no effect if it is already hidden.
332            AnimUtils.fadeOut(mElapsedTime, AnimUtils.DEFAULT_DURATION);
333        }
334    }
335
336    private void setDrawableToImageView(ImageView view, Drawable photo) {
337        if (photo == null) {
338            photo = view.getResources().getDrawable(R.drawable.picture_unknown);
339        }
340
341        final Drawable current = view.getDrawable();
342        if (current == null) {
343            view.setImageDrawable(photo);
344            AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION);
345        } else {
346            AnimationUtils.startCrossFade(view, current, photo);
347            view.setVisibility(View.VISIBLE);
348        }
349    }
350
351    private String getConferenceString(boolean isGeneric) {
352        Log.v(this, "isGenericString: " + isGeneric);
353        final int resId = isGeneric ? R.string.card_title_in_call : R.string.card_title_conf_call;
354        return getView().getResources().getString(resId);
355    }
356
357    private Drawable getConferencePhoto(boolean isGeneric) {
358        Log.v(this, "isGenericPhoto: " + isGeneric);
359        final int resId = isGeneric ? R.drawable.picture_dialing : R.drawable.picture_conference;
360        return getView().getResources().getDrawable(resId);
361    }
362
363    private void setBluetoothOn(boolean onOff) {
364        // Also, display a special icon (alongside the "Incoming call"
365        // label) if there's an incoming call and audio will be routed
366        // to bluetooth when you answer it.
367        final int bluetoothIconId = R.drawable.ic_in_call_bt_dk;
368
369        if (onOff) {
370            mCallStateLabel.setCompoundDrawablesWithIntrinsicBounds(bluetoothIconId, 0, 0, 0);
371            mCallStateLabel.setCompoundDrawablePadding((int) (mDensity * 5));
372        } else {
373            // Clear out any icons
374            mCallStateLabel.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
375        }
376    }
377
378    /**
379     * Gets the call state label based on the state of the call and
380     * cause of disconnect
381     */
382    private String getCallStateLabelFromState(int state, int cause) {
383        final Context context = getView().getContext();
384        String callStateLabel = null;  // Label to display as part of the call banner
385
386        if (Call.State.IDLE == state) {
387            // "Call state" is meaningless in this state.
388
389        } else if (Call.State.ACTIVE == state) {
390            // We normally don't show a "call state label" at all in
391            // this state (but see below for some special cases).
392
393        } else if (Call.State.ONHOLD == state) {
394            callStateLabel = context.getString(R.string.card_title_on_hold);
395        } else if (Call.State.DIALING == state) {
396            callStateLabel = context.getString(R.string.card_title_dialing);
397        } else if (Call.State.REDIALING == state) {
398            callStateLabel = context.getString(R.string.card_title_redialing);
399        } else if (Call.State.INCOMING == state || Call.State.CALL_WAITING == state) {
400            callStateLabel = context.getString(R.string.card_title_incoming_call);
401
402        } else if (Call.State.DISCONNECTING == state) {
403            // While in the DISCONNECTING state we display a "Hanging up"
404            // message in order to make the UI feel more responsive.  (In
405            // GSM it's normal to see a delay of a couple of seconds while
406            // negotiating the disconnect with the network, so the "Hanging
407            // up" state at least lets the user know that we're doing
408            // something.  This state is currently not used with CDMA.)
409            callStateLabel = context.getString(R.string.card_title_hanging_up);
410
411        } else if (Call.State.DISCONNECTED == state) {
412            callStateLabel = getCallFailedString(cause);
413
414        } else {
415            Log.wtf(this, "updateCallStateWidgets: unexpected call: " + state);
416        }
417
418        return callStateLabel;
419    }
420
421    /**
422     * Maps the disconnect cause to a resource string.
423     *
424     * @param cause disconnect cause as defined in {@link DisconnectCause}
425     */
426    private String getCallFailedString(int cause) {
427        int resID = R.string.card_title_call_ended;
428
429        // TODO: The card *title* should probably be "Call ended" in all
430        // cases, but if the DisconnectCause was an error condition we should
431        // probably also display the specific failure reason somewhere...
432
433        switch (cause) {
434            case DisconnectCause.BUSY:
435                resID = R.string.callFailed_userBusy;
436                break;
437
438            case DisconnectCause.CONGESTION:
439                resID = R.string.callFailed_congestion;
440                break;
441
442            case DisconnectCause.TIMED_OUT:
443                resID = R.string.callFailed_timedOut;
444                break;
445
446            case DisconnectCause.SERVER_UNREACHABLE:
447                resID = R.string.callFailed_server_unreachable;
448                break;
449
450            case DisconnectCause.NUMBER_UNREACHABLE:
451                resID = R.string.callFailed_number_unreachable;
452                break;
453
454            case DisconnectCause.INVALID_CREDENTIALS:
455                resID = R.string.callFailed_invalid_credentials;
456                break;
457
458            case DisconnectCause.SERVER_ERROR:
459                resID = R.string.callFailed_server_error;
460                break;
461
462            case DisconnectCause.OUT_OF_NETWORK:
463                resID = R.string.callFailed_out_of_network;
464                break;
465
466            case DisconnectCause.LOST_SIGNAL:
467            case DisconnectCause.CDMA_DROP:
468                resID = R.string.callFailed_noSignal;
469                break;
470
471            case DisconnectCause.LIMIT_EXCEEDED:
472                resID = R.string.callFailed_limitExceeded;
473                break;
474
475            case DisconnectCause.POWER_OFF:
476                resID = R.string.callFailed_powerOff;
477                break;
478
479            case DisconnectCause.ICC_ERROR:
480                resID = R.string.callFailed_simError;
481                break;
482
483            case DisconnectCause.OUT_OF_SERVICE:
484                resID = R.string.callFailed_outOfService;
485                break;
486
487            case DisconnectCause.INVALID_NUMBER:
488            case DisconnectCause.UNOBTAINABLE_NUMBER:
489                resID = R.string.callFailed_unobtainable_number;
490                break;
491
492            default:
493                resID = R.string.card_title_call_ended;
494                break;
495        }
496        return this.getView().getContext().getString(resID);
497    }
498
499    private void showAndInitializeSecondaryCallInfo() {
500        mSecondaryCallInfo.setVisibility(View.VISIBLE);
501
502        // mSecondaryCallName is initialized here (vs. onViewCreated) because it is inaccesible
503        // until mSecondaryCallInfo is inflated in the call above.
504        if (mSecondaryCallName == null) {
505            mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName);
506        }
507        mSecondaryCallInfo.setOnClickListener(new View.OnClickListener() {
508            @Override
509            public void onClick(View v) {
510                getPresenter().secondaryInfoClicked();
511            }
512        });
513    }
514
515    public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
516        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
517            dispatchPopulateAccessibilityEvent(event, mPrimaryName);
518            dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
519            return;
520        }
521        dispatchPopulateAccessibilityEvent(event, mCallStateLabel);
522        dispatchPopulateAccessibilityEvent(event, mPrimaryName);
523        dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
524        dispatchPopulateAccessibilityEvent(event, mCallTypeLabel);
525        dispatchPopulateAccessibilityEvent(event, mSecondaryCallName);
526
527        return;
528    }
529
530    @Override
531    public void setEndCallButtonEnabled(boolean enabled) {
532        mEndCallButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
533    }
534
535    private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) {
536        if (view == null) return;
537        final List<CharSequence> eventText = event.getText();
538        int size = eventText.size();
539        view.dispatchPopulateAccessibilityEvent(event);
540        // if no text added write null to keep relative position
541        if (size == eventText.size()) {
542            eventText.add(null);
543        }
544    }
545
546    public void animateForNewOutgoingCall() {
547        final ViewGroup parent = (ViewGroup) mPrimaryCallCardContainer.getParent();
548
549        final ViewTreeObserver observer = getView().getViewTreeObserver();
550        observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
551            @Override
552            public void onGlobalLayout() {
553                final ViewTreeObserver observer = getView().getViewTreeObserver();
554                if (!observer.isAlive()) {
555                    return;
556                }
557                observer.removeOnGlobalLayoutListener(this);
558
559                final int originalHeight = mPrimaryCallCardContainer.getHeight();
560                final LayoutIgnoringListener listener = new LayoutIgnoringListener();
561                mPrimaryCallCardContainer.addOnLayoutChangeListener(listener);
562
563                // Prepare the state of views before the circular reveal animation
564                mPrimaryCallCardContainer.setBottom(parent.getHeight());
565                mEndCallButton.setTranslationY(200);
566                mCallButtonsContainer.setAlpha(0);
567                mCallStateLabel.setAlpha(0);
568                mPrimaryName.setAlpha(0);
569                mCallTypeLabel.setAlpha(0);
570                mCallNumberAndLabel.setAlpha(0);
571
572                final Animator revealAnimator = getRevealAnimator();
573                final Animator shrinkAnimator =
574                        getShrinkAnimator(parent.getHeight(), originalHeight);
575
576                final AnimatorSet set = new AnimatorSet();
577                set.playSequentially(revealAnimator, shrinkAnimator);
578                set.addListener(new AnimatorListenerAdapter() {
579                    @Override
580                    public void onAnimationCancel(Animator animation) {
581                        mPrimaryCallCardContainer.removeOnLayoutChangeListener(listener);
582                    }
583
584                    @Override
585                    public void onAnimationEnd(Animator animation) {
586                        mPrimaryCallCardContainer.removeOnLayoutChangeListener(listener);
587                    }
588                });
589                set.start();
590            }
591        });
592    }
593
594    /**
595     * Animator that performs the upwards shrinking animation of the blue call card scrim.
596     * At the start of the animation, each child view is moved downwards by a pre-specified amount
597     * and then translated upwards together with the scrim.
598     */
599    private Animator getShrinkAnimator(int startHeight, int endHeight) {
600        final Animator shrinkAnimator =
601                ObjectAnimator.ofInt(mPrimaryCallCardContainer, "bottom",
602                        startHeight, endHeight);
603        shrinkAnimator.setDuration(SHRINK_ANIMATION_DURATION);
604        shrinkAnimator.addListener(new AnimatorListenerAdapter() {
605            @Override
606            public void onAnimationStart(Animator animation) {
607                assignTranslateAnimation(mCallStateLabel, 1);
608                assignTranslateAnimation(mPrimaryName, 2);
609                assignTranslateAnimation(mCallNumberAndLabel, 3);
610                assignTranslateAnimation(mCallTypeLabel, 4);
611                assignTranslateAnimation(mCallButtonsContainer, 5);
612
613                mEndCallButton.animate().translationY(0)
614                        .setDuration(SHRINK_ANIMATION_DURATION);
615            }
616        });
617        shrinkAnimator.setInterpolator(mAnimationInterpolator);
618        return shrinkAnimator;
619    }
620
621    private Animator getRevealAnimator() {
622        final Activity activity = getActivity();
623        final View view  = activity.getWindow().getDecorView();
624        final Display display = activity.getWindowManager().getDefaultDisplay();
625        final Point size = new Point();
626        display.getSize(size);
627
628        final ValueAnimator valueAnimator = view.createRevealAnimator(size.x / 2, size.y / 2,
629                0, Math.max(size.x, size.y));
630        valueAnimator.setDuration(REVEAL_ANIMATION_DURATION);
631        return valueAnimator;
632    }
633
634    private void assignTranslateAnimation(View view, int offset) {
635        view.setTranslationY(mTranslationOffset * offset);
636        view.animate().translationY(0).alpha(1).withLayer()
637                .setDuration(SHRINK_ANIMATION_DURATION).setInterpolator(mAnimationInterpolator);
638    }
639
640    private final class LayoutIgnoringListener implements View.OnLayoutChangeListener {
641        @Override
642        public void onLayoutChange(View v,
643                int left,
644                int top,
645                int right,
646                int bottom,
647                int oldLeft,
648                int oldTop,
649                int oldRight,
650                int oldBottom) {
651            v.setLeft(oldLeft);
652            v.setRight(oldRight);
653            v.setTop(oldTop);
654            v.setBottom(oldBottom);
655        }
656    }
657}
658