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