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