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