Home | History | Annotate | Download | only in calllog
      1 /*
      2  * Copyright (C) 2011 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.contacts.calllog;
     18 
     19 import android.app.Activity;
     20 import android.app.KeyguardManager;
     21 import android.app.ListFragment;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.database.ContentObserver;
     25 import android.database.Cursor;
     26 import android.net.Uri;
     27 import android.os.Bundle;
     28 import android.os.Handler;
     29 import android.os.RemoteException;
     30 import android.os.ServiceManager;
     31 import android.provider.CallLog;
     32 import android.provider.CallLog.Calls;
     33 import android.provider.ContactsContract;
     34 import android.telephony.PhoneNumberUtils;
     35 import android.telephony.PhoneStateListener;
     36 import android.telephony.TelephonyManager;
     37 import android.text.TextUtils;
     38 import android.util.Log;
     39 import android.view.LayoutInflater;
     40 import android.view.Menu;
     41 import android.view.MenuInflater;
     42 import android.view.MenuItem;
     43 import android.view.View;
     44 import android.view.ViewGroup;
     45 import android.widget.ListView;
     46 import android.widget.TextView;
     47 
     48 import com.android.common.io.MoreCloseables;
     49 import com.android.contacts.ContactsUtils;
     50 import com.android.contacts.R;
     51 import com.android.contacts.util.Constants;
     52 import com.android.contacts.util.EmptyLoader;
     53 import com.android.contacts.voicemail.VoicemailStatusHelper;
     54 import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage;
     55 import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
     56 import com.android.internal.telephony.CallerInfo;
     57 import com.android.internal.telephony.ITelephony;
     58 import com.google.common.annotations.VisibleForTesting;
     59 
     60 import java.util.List;
     61 
     62 /**
     63  * Displays a list of call log entries.
     64  */
     65 public class CallLogFragment extends ListFragment
     66         implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
     67     private static final String TAG = "CallLogFragment";
     68 
     69     /**
     70      * ID of the empty loader to defer other fragments.
     71      */
     72     private static final int EMPTY_LOADER_ID = 0;
     73 
     74     private CallLogAdapter mAdapter;
     75     private CallLogQueryHandler mCallLogQueryHandler;
     76     private boolean mScrollToTop;
     77 
     78     /** Whether there is at least one voicemail source installed. */
     79     private boolean mVoicemailSourcesAvailable = false;
     80     /** Whether we are currently filtering over voicemail. */
     81     private boolean mShowingVoicemailOnly = false;
     82 
     83     private VoicemailStatusHelper mVoicemailStatusHelper;
     84     private View mStatusMessageView;
     85     private TextView mStatusMessageText;
     86     private TextView mStatusMessageAction;
     87     private TextView mFilterStatusView;
     88     private KeyguardManager mKeyguardManager;
     89 
     90     private boolean mEmptyLoaderRunning;
     91     private boolean mCallLogFetched;
     92     private boolean mVoicemailStatusFetched;
     93 
     94     private final Handler mHandler = new Handler();
     95 
     96     private TelephonyManager mTelephonyManager;
     97     private PhoneStateListener mPhoneStateListener;
     98 
     99     private class CustomContentObserver extends ContentObserver {
    100         public CustomContentObserver() {
    101             super(mHandler);
    102         }
    103         @Override
    104         public void onChange(boolean selfChange) {
    105             mRefreshDataRequired = true;
    106         }
    107     }
    108 
    109     // See issue 6363009
    110     private final ContentObserver mCallLogObserver = new CustomContentObserver();
    111     private final ContentObserver mContactsObserver = new CustomContentObserver();
    112     private boolean mRefreshDataRequired = true;
    113 
    114     // Exactly same variable is in Fragment as a package private.
    115     private boolean mMenuVisible = true;
    116 
    117     // Default to all calls.
    118     private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
    119 
    120     @Override
    121     public void onCreate(Bundle state) {
    122         super.onCreate(state);
    123 
    124         mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this);
    125         mKeyguardManager =
    126                 (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
    127         getActivity().getContentResolver().registerContentObserver(
    128                 CallLog.CONTENT_URI, true, mCallLogObserver);
    129         getActivity().getContentResolver().registerContentObserver(
    130                 ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
    131         setHasOptionsMenu(true);
    132     }
    133 
    134     /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
    135     @Override
    136     public void onCallsFetched(Cursor cursor) {
    137         if (getActivity() == null || getActivity().isFinishing()) {
    138             return;
    139         }
    140         mAdapter.setLoading(false);
    141         mAdapter.changeCursor(cursor);
    142         // This will update the state of the "Clear call log" menu item.
    143         getActivity().invalidateOptionsMenu();
    144         if (mScrollToTop) {
    145             final ListView listView = getListView();
    146             // The smooth-scroll animation happens over a fixed time period.
    147             // As a result, if it scrolls through a large portion of the list,
    148             // each frame will jump so far from the previous one that the user
    149             // will not experience the illusion of downward motion.  Instead,
    150             // if we're not already near the top of the list, we instantly jump
    151             // near the top, and animate from there.
    152             if (listView.getFirstVisiblePosition() > 5) {
    153                 listView.setSelection(5);
    154             }
    155             // Workaround for framework issue: the smooth-scroll doesn't
    156             // occur if setSelection() is called immediately before.
    157             mHandler.post(new Runnable() {
    158                @Override
    159                public void run() {
    160                    if (getActivity() == null || getActivity().isFinishing()) {
    161                        return;
    162                    }
    163                    listView.smoothScrollToPosition(0);
    164                }
    165             });
    166 
    167             mScrollToTop = false;
    168         }
    169         mCallLogFetched = true;
    170         destroyEmptyLoaderIfAllDataFetched();
    171     }
    172 
    173     /**
    174      * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
    175      */
    176     @Override
    177     public void onVoicemailStatusFetched(Cursor statusCursor) {
    178         if (getActivity() == null || getActivity().isFinishing()) {
    179             return;
    180         }
    181         updateVoicemailStatusMessage(statusCursor);
    182 
    183         int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor);
    184         setVoicemailSourcesAvailable(activeSources != 0);
    185         MoreCloseables.closeQuietly(statusCursor);
    186         mVoicemailStatusFetched = true;
    187         destroyEmptyLoaderIfAllDataFetched();
    188     }
    189 
    190     private void destroyEmptyLoaderIfAllDataFetched() {
    191         if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
    192             mEmptyLoaderRunning = false;
    193             getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
    194         }
    195     }
    196 
    197     /** Sets whether there are any voicemail sources available in the platform. */
    198     private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) {
    199         if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return;
    200         mVoicemailSourcesAvailable = voicemailSourcesAvailable;
    201 
    202         Activity activity = getActivity();
    203         if (activity != null) {
    204             // This is so that the options menu content is updated.
    205             activity.invalidateOptionsMenu();
    206         }
    207     }
    208 
    209     @Override
    210     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
    211         View view = inflater.inflate(R.layout.call_log_fragment, container, false);
    212         mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
    213         mStatusMessageView = view.findViewById(R.id.voicemail_status);
    214         mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
    215         mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
    216         mFilterStatusView = (TextView) view.findViewById(R.id.filter_status);
    217         return view;
    218     }
    219 
    220     @Override
    221     public void onViewCreated(View view, Bundle savedInstanceState) {
    222         super.onViewCreated(view, savedInstanceState);
    223         String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
    224         mAdapter = new CallLogAdapter(getActivity(), this,
    225                 new ContactInfoHelper(getActivity(), currentCountryIso));
    226         setListAdapter(mAdapter);
    227         getListView().setItemsCanFocus(true);
    228     }
    229 
    230     /**
    231      * Based on the new intent, decide whether the list should be configured
    232      * to scroll up to display the first item.
    233      */
    234     public void configureScreenFromIntent(Intent newIntent) {
    235         // Typically, when switching to the call-log we want to show the user
    236         // the same section of the list that they were most recently looking
    237         // at.  However, under some circumstances, we want to automatically
    238         // scroll to the top of the list to present the newest call items.
    239         // For example, immediately after a call is finished, we want to
    240         // display information about that call.
    241         mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType());
    242     }
    243 
    244     @Override
    245     public void onStart() {
    246         // Start the empty loader now to defer other fragments.  We destroy it when both calllog
    247         // and the voicemail status are fetched.
    248         getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
    249                 new EmptyLoader.Callback(getActivity()));
    250         mEmptyLoaderRunning = true;
    251         super.onStart();
    252     }
    253 
    254     @Override
    255     public void onResume() {
    256         super.onResume();
    257         refreshData();
    258     }
    259 
    260     private void updateVoicemailStatusMessage(Cursor statusCursor) {
    261         List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
    262         if (messages.size() == 0) {
    263             mStatusMessageView.setVisibility(View.GONE);
    264         } else {
    265             mStatusMessageView.setVisibility(View.VISIBLE);
    266             // TODO: Change the code to show all messages. For now just pick the first message.
    267             final StatusMessage message = messages.get(0);
    268             if (message.showInCallLog()) {
    269                 mStatusMessageText.setText(message.callLogMessageId);
    270             }
    271             if (message.actionMessageId != -1) {
    272                 mStatusMessageAction.setText(message.actionMessageId);
    273             }
    274             if (message.actionUri != null) {
    275                 mStatusMessageAction.setVisibility(View.VISIBLE);
    276                 mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
    277                     @Override
    278                     public void onClick(View v) {
    279                         getActivity().startActivity(
    280                                 new Intent(Intent.ACTION_VIEW, message.actionUri));
    281                     }
    282                 });
    283             } else {
    284                 mStatusMessageAction.setVisibility(View.GONE);
    285             }
    286         }
    287     }
    288 
    289     @Override
    290     public void onPause() {
    291         super.onPause();
    292         // Kill the requests thread
    293         mAdapter.stopRequestProcessing();
    294     }
    295 
    296     @Override
    297     public void onStop() {
    298         super.onStop();
    299         updateOnExit();
    300     }
    301 
    302     @Override
    303     public void onDestroy() {
    304         super.onDestroy();
    305         mAdapter.stopRequestProcessing();
    306         mAdapter.changeCursor(null);
    307         getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
    308         getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
    309         unregisterPhoneCallReceiver();
    310     }
    311 
    312     @Override
    313     public void fetchCalls() {
    314         mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
    315     }
    316 
    317     public void startCallsQuery() {
    318         mAdapter.setLoading(true);
    319         mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
    320         if (mShowingVoicemailOnly) {
    321             mShowingVoicemailOnly = false;
    322             getActivity().invalidateOptionsMenu();
    323         }
    324     }
    325 
    326     private void startVoicemailStatusQuery() {
    327         mCallLogQueryHandler.fetchVoicemailStatus();
    328     }
    329 
    330     @Override
    331     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    332         super.onCreateOptionsMenu(menu, inflater);
    333         inflater.inflate(R.menu.call_log_options, menu);
    334     }
    335 
    336     @Override
    337     public void onPrepareOptionsMenu(Menu menu) {
    338         final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all);
    339         // Check if all the menu items are inflated correctly. As a shortcut, we assume all
    340         // menu items are ready if the first item is non-null.
    341         if (itemDeleteAll != null) {
    342             itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty());
    343             menu.findItem(R.id.show_voicemails_only).setVisible(mVoicemailSourcesAvailable);
    344         }
    345     }
    346 
    347     @Override
    348     public boolean onOptionsItemSelected(MenuItem item) {
    349         switch (item.getItemId()) {
    350             case R.id.delete_all:
    351                 ClearCallLogDialog.show(getFragmentManager());
    352                 return true;
    353 
    354             case R.id.show_outgoing_only:
    355                 // We only need the phone call receiver when there is an active call type filter.
    356                 // Not many people may use the filters so don't register the receiver until now .
    357                 registerPhoneCallReceiver();
    358                 mCallLogQueryHandler.fetchCalls(Calls.OUTGOING_TYPE);
    359                 updateFilterTypeAndHeader(Calls.OUTGOING_TYPE);
    360                 return true;
    361 
    362             case R.id.show_incoming_only:
    363                 registerPhoneCallReceiver();
    364                 mCallLogQueryHandler.fetchCalls(Calls.INCOMING_TYPE);
    365                 updateFilterTypeAndHeader(Calls.INCOMING_TYPE);
    366                 return true;
    367 
    368             case R.id.show_missed_only:
    369                 registerPhoneCallReceiver();
    370                 mCallLogQueryHandler.fetchCalls(Calls.MISSED_TYPE);
    371                 updateFilterTypeAndHeader(Calls.MISSED_TYPE);
    372                 return true;
    373 
    374             case R.id.show_voicemails_only:
    375                 registerPhoneCallReceiver();
    376                 mCallLogQueryHandler.fetchCalls(Calls.VOICEMAIL_TYPE);
    377                 updateFilterTypeAndHeader(Calls.VOICEMAIL_TYPE);
    378                 mShowingVoicemailOnly = true;
    379                 return true;
    380 
    381             case R.id.show_all_calls:
    382                 // Filter is being turned off, receiver no longer needed.
    383                 unregisterPhoneCallReceiver();
    384                 mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL);
    385                 updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL);
    386                 mShowingVoicemailOnly = false;
    387                 return true;
    388 
    389             default:
    390                 return false;
    391         }
    392     }
    393 
    394     private void updateFilterTypeAndHeader(int filterType) {
    395         mCallTypeFilter = filterType;
    396 
    397         switch (filterType) {
    398             case CallLogQueryHandler.CALL_TYPE_ALL:
    399                 mFilterStatusView.setVisibility(View.GONE);
    400                 break;
    401             case Calls.INCOMING_TYPE:
    402                 showFilterStatus(R.string.call_log_incoming_header);
    403                 break;
    404             case Calls.OUTGOING_TYPE:
    405                 showFilterStatus(R.string.call_log_outgoing_header);
    406                 break;
    407             case Calls.MISSED_TYPE:
    408                 showFilterStatus(R.string.call_log_missed_header);
    409                 break;
    410             case Calls.VOICEMAIL_TYPE:
    411                 showFilterStatus(R.string.call_log_voicemail_header);
    412                 break;
    413         }
    414     }
    415 
    416     private void showFilterStatus(int resId) {
    417         mFilterStatusView.setText(resId);
    418         mFilterStatusView.setVisibility(View.VISIBLE);
    419     }
    420 
    421     public void callSelectedEntry() {
    422         int position = getListView().getSelectedItemPosition();
    423         if (position < 0) {
    424             // In touch mode you may often not have something selected, so
    425             // just call the first entry to make sure that [send] [send] calls the
    426             // most recent entry.
    427             position = 0;
    428         }
    429         final Cursor cursor = (Cursor)mAdapter.getItem(position);
    430         if (cursor != null) {
    431             String number = cursor.getString(CallLogQuery.NUMBER);
    432             if (TextUtils.isEmpty(number)
    433                     || number.equals(CallerInfo.UNKNOWN_NUMBER)
    434                     || number.equals(CallerInfo.PRIVATE_NUMBER)
    435                     || number.equals(CallerInfo.PAYPHONE_NUMBER)) {
    436                 // This number can't be called, do nothing
    437                 return;
    438             }
    439             Intent intent;
    440             // If "number" is really a SIP address, construct a sip: URI.
    441             if (PhoneNumberUtils.isUriNumber(number)) {
    442                 intent = ContactsUtils.getCallIntent(
    443                         Uri.fromParts(Constants.SCHEME_SIP, number, null));
    444             } else {
    445                 // We're calling a regular PSTN phone number.
    446                 // Construct a tel: URI, but do some other possible cleanup first.
    447                 int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
    448                 if (!number.startsWith("+") &&
    449                        (callType == Calls.INCOMING_TYPE
    450                                 || callType == Calls.MISSED_TYPE)) {
    451                     // If the caller-id matches a contact with a better qualified number, use it
    452                     String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
    453                     number = mAdapter.getBetterNumberFromContacts(number, countryIso);
    454                 }
    455                 intent = ContactsUtils.getCallIntent(
    456                         Uri.fromParts(Constants.SCHEME_TEL, number, null));
    457             }
    458             intent.setFlags(
    459                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
    460             startActivity(intent);
    461         }
    462     }
    463 
    464     @VisibleForTesting
    465     CallLogAdapter getAdapter() {
    466         return mAdapter;
    467     }
    468 
    469     @Override
    470     public void setMenuVisibility(boolean menuVisible) {
    471         super.setMenuVisibility(menuVisible);
    472         if (mMenuVisible != menuVisible) {
    473             mMenuVisible = menuVisible;
    474             if (!menuVisible) {
    475                 updateOnExit();
    476             } else if (isResumed()) {
    477                 refreshData();
    478             }
    479         }
    480     }
    481 
    482     /** Requests updates to the data to be shown. */
    483     private void refreshData() {
    484         // Prevent unnecessary refresh.
    485         if (mRefreshDataRequired) {
    486             // Mark all entries in the contact info cache as out of date, so they will be looked up
    487             // again once being shown.
    488             mAdapter.invalidateCache();
    489             startCallsQuery();
    490             startVoicemailStatusQuery();
    491             updateOnEntry();
    492             mRefreshDataRequired = false;
    493         }
    494     }
    495 
    496     /** Removes the missed call notifications. */
    497     private void removeMissedCallNotifications() {
    498         try {
    499             ITelephony telephony =
    500                     ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
    501             if (telephony != null) {
    502                 telephony.cancelMissedCallsNotification();
    503             } else {
    504                 Log.w(TAG, "Telephony service is null, can't call " +
    505                         "cancelMissedCallsNotification");
    506             }
    507         } catch (RemoteException e) {
    508             Log.e(TAG, "Failed to clear missed calls notification due to remote exception");
    509         }
    510     }
    511 
    512     /** Updates call data and notification state while leaving the call log tab. */
    513     private void updateOnExit() {
    514         updateOnTransition(false);
    515     }
    516 
    517     /** Updates call data and notification state while entering the call log tab. */
    518     private void updateOnEntry() {
    519         updateOnTransition(true);
    520     }
    521 
    522     private void updateOnTransition(boolean onEntry) {
    523         // We don't want to update any call data when keyguard is on because the user has likely not
    524         // seen the new calls yet.
    525         // This might be called before onCreate() and thus we need to check null explicitly.
    526         if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
    527             // On either of the transitions we reset the new flag and update the notifications.
    528             // While exiting we additionally consume all missed calls (by marking them as read).
    529             // This will ensure that they no more appear in the "new" section when we return back.
    530             mCallLogQueryHandler.markNewCallsAsOld();
    531             if (!onEntry) {
    532                 mCallLogQueryHandler.markMissedCallsAsRead();
    533             }
    534             removeMissedCallNotifications();
    535             updateVoicemailNotifications();
    536         }
    537     }
    538 
    539     private void updateVoicemailNotifications() {
    540         Intent serviceIntent = new Intent(getActivity(), CallLogNotificationsService.class);
    541         serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
    542         getActivity().startService(serviceIntent);
    543     }
    544 
    545     /**
    546      * Register a phone call filter to reset the call type when a phone call is place.
    547      */
    548     private void registerPhoneCallReceiver() {
    549         if (mPhoneStateListener != null) {
    550             return; // Already registered.
    551         }
    552         mTelephonyManager = (TelephonyManager) getActivity().getSystemService(
    553                 Context.TELEPHONY_SERVICE);
    554         mPhoneStateListener = new PhoneStateListener() {
    555             @Override
    556             public void onCallStateChanged(int state, String incomingNumber) {
    557                 if (state != TelephonyManager.CALL_STATE_OFFHOOK &&
    558                         state != TelephonyManager.CALL_STATE_RINGING) {
    559                     return;
    560                 }
    561                 mHandler.post(new Runnable() {
    562                     @Override
    563                     public void run() {
    564                         if (getActivity() == null || getActivity().isFinishing()) {
    565                             return;
    566                         }
    567                         updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL);
    568                     }
    569                  });
    570             }
    571         };
    572         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
    573     }
    574 
    575     /**
    576      * Un-registers the phone call receiver.
    577      */
    578     private void unregisterPhoneCallReceiver() {
    579         if (mPhoneStateListener != null) {
    580             mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
    581             mPhoneStateListener = null;
    582         }
    583     }
    584 }
    585