Home | History | Annotate | Download | only in incallui
      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 
     17 package com.android.incallui;
     18 
     19 import android.content.Context;
     20 import android.content.pm.ApplicationInfo;
     21 import android.content.pm.PackageManager;
     22 import android.graphics.drawable.Drawable;
     23 import android.graphics.Bitmap;
     24 import android.telephony.PhoneNumberUtils;
     25 import android.text.TextUtils;
     26 import android.text.format.DateUtils;
     27 
     28 import com.android.incallui.AudioModeProvider.AudioModeListener;
     29 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
     30 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
     31 import com.android.incallui.InCallPresenter.InCallState;
     32 import com.android.incallui.InCallPresenter.InCallStateListener;
     33 import com.android.incallui.InCallPresenter.IncomingCallListener;
     34 import com.android.services.telephony.common.AudioMode;
     35 import com.android.services.telephony.common.Call;
     36 import com.android.services.telephony.common.Call.Capabilities;
     37 import com.android.services.telephony.common.CallIdentification;
     38 import com.google.common.base.Preconditions;
     39 
     40 /**
     41  * Presenter for the Call Card Fragment.
     42  * <p>
     43  * This class listens for changes to InCallState and passes it along to the fragment.
     44  */
     45 public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
     46         implements InCallStateListener, AudioModeListener, IncomingCallListener {
     47 
     48     private static final String TAG = CallCardPresenter.class.getSimpleName();
     49     private static final long CALL_TIME_UPDATE_INTERVAL = 1000; // in milliseconds
     50 
     51     private Call mPrimary;
     52     private Call mSecondary;
     53     private ContactCacheEntry mPrimaryContactInfo;
     54     private ContactCacheEntry mSecondaryContactInfo;
     55     private CallTimer mCallTimer;
     56     private Context mContext;
     57 
     58     public CallCardPresenter() {
     59         // create the call timer
     60         mCallTimer = new CallTimer(new Runnable() {
     61             @Override
     62             public void run() {
     63                 updateCallTime();
     64             }
     65         });
     66     }
     67 
     68 
     69     public void init(Context context, Call call) {
     70         mContext = Preconditions.checkNotNull(context);
     71 
     72         // Call may be null if disconnect happened already.
     73         if (call != null) {
     74             mPrimary = call;
     75 
     76             final CallIdentification identification = call.getIdentification();
     77 
     78             // start processing lookups right away.
     79             if (!call.isConferenceCall()) {
     80                 startContactInfoSearch(identification, true,
     81                         call.getState() == Call.State.INCOMING);
     82             } else {
     83                 updateContactEntry(null, true, true);
     84             }
     85         }
     86     }
     87 
     88     @Override
     89     public void onUiReady(CallCardUi ui) {
     90         super.onUiReady(ui);
     91 
     92         AudioModeProvider.getInstance().addListener(this);
     93 
     94         // Contact search may have completed before ui is ready.
     95         if (mPrimaryContactInfo != null) {
     96             updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(mPrimary));
     97         }
     98 
     99         // Register for call state changes last
    100         InCallPresenter.getInstance().addListener(this);
    101         InCallPresenter.getInstance().addIncomingCallListener(this);
    102     }
    103 
    104     @Override
    105     public void onUiUnready(CallCardUi ui) {
    106         super.onUiUnready(ui);
    107 
    108         // stop getting call state changes
    109         InCallPresenter.getInstance().removeListener(this);
    110         InCallPresenter.getInstance().removeIncomingCallListener(this);
    111 
    112         AudioModeProvider.getInstance().removeListener(this);
    113 
    114         mPrimary = null;
    115         mPrimaryContactInfo = null;
    116         mSecondaryContactInfo = null;
    117     }
    118 
    119     @Override
    120     public void onIncomingCall(InCallState state, Call call) {
    121         // same logic should happen as with onStateChange()
    122         onStateChange(state, CallList.getInstance());
    123     }
    124 
    125     @Override
    126     public void onStateChange(InCallState state, CallList callList) {
    127         Log.d(this, "onStateChange() " + state);
    128         final CallCardUi ui = getUi();
    129         if (ui == null) {
    130             return;
    131         }
    132 
    133         Call primary = null;
    134         Call secondary = null;
    135 
    136         if (state == InCallState.INCOMING) {
    137             primary = callList.getIncomingCall();
    138         } else if (state == InCallState.OUTGOING) {
    139             primary = callList.getOutgoingCall();
    140 
    141             // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
    142             // highest priority call to display as the secondary call.
    143             secondary = getCallToDisplay(callList, null, true);
    144         } else if (state == InCallState.INCALL) {
    145             primary = getCallToDisplay(callList, null, false);
    146             secondary = getCallToDisplay(callList, primary, true);
    147         }
    148 
    149         Log.d(this, "Primary call: " + primary);
    150         Log.d(this, "Secondary call: " + secondary);
    151 
    152         final boolean primaryChanged = !areCallsSame(mPrimary, primary);
    153         final boolean secondaryChanged = !areCallsSame(mSecondary, secondary);
    154         mSecondary = secondary;
    155         mPrimary = primary;
    156 
    157         if (primaryChanged && mPrimary != null) {
    158             // primary call has changed
    159             mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext,
    160                     mPrimary.getIdentification(), mPrimary.getState() == Call.State.INCOMING);
    161             updatePrimaryDisplayInfo(mPrimaryContactInfo, isConference(mPrimary));
    162             maybeStartSearch(mPrimary, true);
    163         }
    164 
    165         if (mSecondary == null) {
    166             // Secondary call may have ended.  Update the ui.
    167             mSecondaryContactInfo = null;
    168             updateSecondaryDisplayInfo(false);
    169         } else if (secondaryChanged) {
    170             // secondary call has changed
    171             mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext,
    172                     mSecondary.getIdentification(), mSecondary.getState() == Call.State.INCOMING);
    173             updateSecondaryDisplayInfo(mSecondary.isConferenceCall());
    174             maybeStartSearch(mSecondary, false);
    175         }
    176 
    177         // Start/Stop the call time update timer
    178         if (mPrimary != null && mPrimary.getState() == Call.State.ACTIVE) {
    179             Log.d(this, "Starting the calltime timer");
    180             mCallTimer.start(CALL_TIME_UPDATE_INTERVAL);
    181         } else {
    182             Log.d(this, "Canceling the calltime timer");
    183             mCallTimer.cancel();
    184             ui.setPrimaryCallElapsedTime(false, null);
    185         }
    186 
    187         // Set the call state
    188         if (mPrimary != null) {
    189             final boolean bluetoothOn =
    190                     (AudioModeProvider.getInstance().getAudioMode() == AudioMode.BLUETOOTH);
    191             ui.setCallState(mPrimary.getState(), mPrimary.getDisconnectCause(), bluetoothOn,
    192                     getGatewayLabel(), getGatewayNumber());
    193         } else {
    194             ui.setCallState(Call.State.IDLE, Call.DisconnectCause.UNKNOWN, false, null, null);
    195         }
    196     }
    197 
    198     @Override
    199     public void onAudioMode(int mode) {
    200         if (mPrimary != null && getUi() != null) {
    201             final boolean bluetoothOn = (AudioMode.BLUETOOTH == mode);
    202 
    203             getUi().setCallState(mPrimary.getState(), mPrimary.getDisconnectCause(), bluetoothOn,
    204                     getGatewayLabel(), getGatewayNumber());
    205         }
    206     }
    207 
    208     @Override
    209     public void onSupportedAudioMode(int mask) {
    210     }
    211 
    212     @Override
    213     public void onMute(boolean muted) {
    214     }
    215 
    216     public void updateCallTime() {
    217         final CallCardUi ui = getUi();
    218 
    219         if (ui == null || mPrimary == null || mPrimary.getState() != Call.State.ACTIVE) {
    220             if (ui != null) {
    221                 ui.setPrimaryCallElapsedTime(false, null);
    222             }
    223             mCallTimer.cancel();
    224         } else {
    225             final long callStart = mPrimary.getConnectTime();
    226             final long duration = System.currentTimeMillis() - callStart;
    227             ui.setPrimaryCallElapsedTime(true, DateUtils.formatElapsedTime(duration / 1000));
    228         }
    229     }
    230 
    231     private boolean areCallsSame(Call call1, Call call2) {
    232         if (call1 == null && call2 == null) {
    233             return true;
    234         } else if (call1 == null || call2 == null) {
    235             return false;
    236         }
    237 
    238         // otherwise compare call Ids
    239         return call1.getCallId() == call2.getCallId();
    240     }
    241 
    242     private void maybeStartSearch(Call call, boolean isPrimary) {
    243         // no need to start search for conference calls which show generic info.
    244         if (call != null && !call.isConferenceCall()) {
    245             startContactInfoSearch(call.getIdentification(), isPrimary,
    246                     call.getState() == Call.State.INCOMING);
    247         }
    248     }
    249 
    250     /**
    251      * Starts a query for more contact data for the save primary and secondary calls.
    252      */
    253     private void startContactInfoSearch(final CallIdentification identification,
    254             final boolean isPrimary, boolean isIncoming) {
    255         final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
    256 
    257         cache.findInfo(identification, isIncoming, new ContactInfoCacheCallback() {
    258                 @Override
    259                 public void onContactInfoComplete(int callId, ContactCacheEntry entry) {
    260                     updateContactEntry(entry, isPrimary, false);
    261                     if (entry.name != null) {
    262                         Log.d(TAG, "Contact found: " + entry);
    263                     }
    264                     if (entry.personUri != null) {
    265                         CallerInfoUtils.sendViewNotification(mContext, entry.personUri);
    266                     }
    267                 }
    268 
    269                 @Override
    270                 public void onImageLoadComplete(int callId, ContactCacheEntry entry) {
    271                     if (getUi() == null) {
    272                         return;
    273                     }
    274                     if (entry.photo != null) {
    275                         if (mPrimary != null && callId == mPrimary.getCallId()) {
    276                             getUi().setPrimaryImage(entry.photo);
    277                         } else if (mSecondary != null && callId == mSecondary.getCallId()) {
    278                             getUi().setSecondaryImage(entry.photo);
    279                         }
    280                     }
    281                 }
    282             });
    283     }
    284 
    285     private static boolean isConference(Call call) {
    286         return call != null && call.isConferenceCall();
    287     }
    288 
    289     private static boolean isGenericConference(Call call) {
    290         return call != null && call.can(Capabilities.GENERIC_CONFERENCE);
    291     }
    292 
    293     private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary,
    294             boolean isConference) {
    295         if (isPrimary) {
    296             mPrimaryContactInfo = entry;
    297             updatePrimaryDisplayInfo(entry, isConference);
    298         } else {
    299             mSecondaryContactInfo = entry;
    300             updateSecondaryDisplayInfo(isConference);
    301         }
    302     }
    303 
    304     /**
    305      * Get the highest priority call to display.
    306      * Goes through the calls and chooses which to return based on priority of which type of call
    307      * to display to the user. Callers can use the "ignore" feature to get the second best call
    308      * by passing a previously found primary call as ignore.
    309      *
    310      * @param ignore A call to ignore if found.
    311      */
    312     private Call getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected) {
    313 
    314         // Active calls come second.  An active call always gets precedent.
    315         Call retval = callList.getActiveCall();
    316         if (retval != null && retval != ignore) {
    317             return retval;
    318         }
    319 
    320         // Disconnected calls get primary position if there are no active calls
    321         // to let user know quickly what call has disconnected. Disconnected
    322         // calls are very short lived.
    323         if (!skipDisconnected) {
    324             retval = callList.getDisconnectingCall();
    325             if (retval != null && retval != ignore) {
    326                 return retval;
    327             }
    328             retval = callList.getDisconnectedCall();
    329             if (retval != null && retval != ignore) {
    330                 return retval;
    331             }
    332         }
    333 
    334         // Then we go to background call (calls on hold)
    335         retval = callList.getBackgroundCall();
    336         if (retval != null && retval != ignore) {
    337             return retval;
    338         }
    339 
    340         // Lastly, we go to a second background call.
    341         retval = callList.getSecondBackgroundCall();
    342 
    343         return retval;
    344     }
    345 
    346     private void updatePrimaryDisplayInfo(ContactCacheEntry entry, boolean isConference) {
    347         Log.d(TAG, "Update primary display " + entry);
    348         final CallCardUi ui = getUi();
    349         if (ui == null) {
    350             // TODO: May also occur if search result comes back after ui is destroyed. Look into
    351             // removing that case completely.
    352             Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!");
    353             return;
    354         }
    355 
    356         final boolean isGenericConf = isGenericConference(mPrimary);
    357         if (entry != null) {
    358             final String name = getNameForCall(entry);
    359             final String number = getNumberForCall(entry);
    360             final boolean nameIsNumber = name != null && name.equals(entry.number);
    361             ui.setPrimary(number, name, nameIsNumber, entry.label,
    362                     entry.photo, isConference, isGenericConf, entry.isSipCall);
    363         } else {
    364             ui.setPrimary(null, null, false, null, null, isConference, isGenericConf, false);
    365         }
    366 
    367     }
    368 
    369     private void updateSecondaryDisplayInfo(boolean isConference) {
    370 
    371         final CallCardUi ui = getUi();
    372         if (ui == null) {
    373             return;
    374         }
    375 
    376         final boolean isGenericConf = isGenericConference(mSecondary);
    377         if (mSecondaryContactInfo != null) {
    378             Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo);
    379             final String nameForCall = getNameForCall(mSecondaryContactInfo);
    380 
    381             final boolean nameIsNumber = nameForCall != null && nameForCall.equals(
    382                     mSecondaryContactInfo.number);
    383             ui.setSecondary(true, nameForCall, nameIsNumber, mSecondaryContactInfo.label,
    384                     mSecondaryContactInfo.photo, isConference, isGenericConf);
    385         } else {
    386             // reset to nothing so that it starts off blank next time we use it.
    387             ui.setSecondary(false, null, false, null, null, isConference, isGenericConf);
    388         }
    389     }
    390 
    391     /**
    392      * Returns the gateway number for any existing outgoing call.
    393      */
    394     private String getGatewayNumber() {
    395         if (hasOutgoingGatewayCall()) {
    396             return mPrimary.getGatewayNumber();
    397         }
    398 
    399         return null;
    400     }
    401 
    402     /**
    403      * Returns the label for the gateway app for any existing outgoing call.
    404      */
    405     private String getGatewayLabel() {
    406         if (hasOutgoingGatewayCall() && getUi() != null) {
    407             final PackageManager pm = mContext.getPackageManager();
    408             try {
    409                 final ApplicationInfo info = pm.getApplicationInfo(mPrimary.getGatewayPackage(), 0);
    410                 return mContext.getString(R.string.calling_via_template,
    411                         pm.getApplicationLabel(info).toString());
    412             } catch (PackageManager.NameNotFoundException e) {
    413             }
    414         }
    415         return null;
    416     }
    417 
    418     private boolean hasOutgoingGatewayCall() {
    419         // We only display the gateway information while DIALING so return false for any othe
    420         // call state.
    421         // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
    422         // is also called after a contact search completes (call is not present yet).  Split the
    423         // UI update so it can receive independent updates.
    424         if (mPrimary == null) {
    425             return false;
    426         }
    427         return (Call.State.isDialing(mPrimary.getState()) &&
    428                 !TextUtils.isEmpty(mPrimary.getGatewayNumber()) &&
    429                 !TextUtils.isEmpty(mPrimary.getGatewayPackage()));
    430     }
    431 
    432     /**
    433      * Gets the name to display for the call.
    434      */
    435     private static String getNameForCall(ContactCacheEntry contactInfo) {
    436         if (TextUtils.isEmpty(contactInfo.name)) {
    437             return contactInfo.number;
    438         }
    439         return contactInfo.name;
    440     }
    441 
    442     /**
    443      * Gets the number to display for a call.
    444      */
    445     private static String getNumberForCall(ContactCacheEntry contactInfo) {
    446         // If the name is empty, we use the number for the name...so dont show a second
    447         // number in the number field
    448         if (TextUtils.isEmpty(contactInfo.name)) {
    449             return contactInfo.location;
    450         }
    451         return contactInfo.number;
    452     }
    453 
    454     public void secondaryPhotoClicked() {
    455         CallCommandClient.getInstance().swap();
    456     }
    457 
    458     public interface CallCardUi extends Ui {
    459         void setVisible(boolean on);
    460         void setPrimary(String number, String name, boolean nameIsNumber, String label,
    461                 Drawable photo, boolean isConference, boolean isGeneric, boolean isSipCall);
    462         void setSecondary(boolean show, String name, boolean nameIsNumber, String label,
    463                 Drawable photo, boolean isConference, boolean isGeneric);
    464         void setSecondaryImage(Drawable image);
    465         void setCallState(int state, Call.DisconnectCause cause, boolean bluetoothOn,
    466                 String gatewayLabel, String gatewayNumber);
    467         void setPrimaryCallElapsedTime(boolean show, String duration);
    468         void setPrimaryName(String name, boolean nameIsNumber);
    469         void setPrimaryImage(Drawable image);
    470         void setPrimaryPhoneNumber(String phoneNumber);
    471         void setPrimaryLabel(String label);
    472     }
    473 }
    474