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