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 com.android.common.io.MoreCloseables;
     20 import com.android.contacts.ContactsUtils;
     21 import com.android.contacts.R;
     22 import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener;
     23 import com.android.contacts.util.EmptyLoader;
     24 import com.android.contacts.voicemail.VoicemailStatusHelper;
     25 import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage;
     26 import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
     27 import com.android.internal.telephony.CallerInfo;
     28 import com.android.internal.telephony.ITelephony;
     29 import com.google.common.annotations.VisibleForTesting;
     30 
     31 import android.app.Activity;
     32 import android.app.KeyguardManager;
     33 import android.app.ListFragment;
     34 import android.content.Context;
     35 import android.content.Intent;
     36 import android.database.Cursor;
     37 import android.net.Uri;
     38 import android.os.Bundle;
     39 import android.os.RemoteException;
     40 import android.os.ServiceManager;
     41 import android.provider.CallLog.Calls;
     42 import android.telephony.PhoneNumberUtils;
     43 import android.text.TextUtils;
     44 import android.util.Log;
     45 import android.view.LayoutInflater;
     46 import android.view.Menu;
     47 import android.view.MenuInflater;
     48 import android.view.MenuItem;
     49 import android.view.View;
     50 import android.view.ViewGroup;
     51 import android.widget.ListView;
     52 import android.widget.TextView;
     53 
     54 import java.util.List;
     55 
     56 /**
     57  * Displays a list of call log entries.
     58  */
     59 public class CallLogFragment extends ListFragment implements ViewPagerVisibilityListener,
     60         CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
     61     private static final String TAG = "CallLogFragment";
     62 
     63     /**
     64      * ID of the empty loader to defer other fragments.
     65      */
     66     private static final int EMPTY_LOADER_ID = 0;
     67 
     68     private CallLogAdapter mAdapter;
     69     private CallLogQueryHandler mCallLogQueryHandler;
     70     private boolean mScrollToTop;
     71 
     72     private boolean mShowOptionsMenu;
     73     /** Whether there is at least one voicemail source installed. */
     74     private boolean mVoicemailSourcesAvailable = false;
     75     /** Whether we are currently filtering over voicemail. */
     76     private boolean mShowingVoicemailOnly = false;
     77 
     78     private VoicemailStatusHelper mVoicemailStatusHelper;
     79     private View mStatusMessageView;
     80     private TextView mStatusMessageText;
     81     private TextView mStatusMessageAction;
     82     private KeyguardManager mKeyguardManager;
     83 
     84     private boolean mEmptyLoaderRunning;
     85     private boolean mCallLogFetched;
     86     private boolean mVoicemailStatusFetched;
     87 
     88     @Override
     89     public void onCreate(Bundle state) {
     90         super.onCreate(state);
     91 
     92         mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this);
     93         mKeyguardManager =
     94                 (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
     95         setHasOptionsMenu(true);
     96     }
     97 
     98     /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
     99     @Override
    100     public void onCallsFetched(Cursor cursor) {
    101         if (getActivity() == null || getActivity().isFinishing()) {
    102             return;
    103         }
    104         mAdapter.setLoading(false);
    105         mAdapter.changeCursor(cursor);
    106         // This will update the state of the "Clear call log" menu item.
    107         getActivity().invalidateOptionsMenu();
    108         if (mScrollToTop) {
    109             final ListView listView = getListView();
    110             if (listView.getFirstVisiblePosition() > 5) {
    111                 listView.setSelection(5);
    112             }
    113             listView.smoothScrollToPosition(0);
    114             mScrollToTop = false;
    115         }
    116         mCallLogFetched = true;
    117         destroyEmptyLoaderIfAllDataFetched();
    118     }
    119 
    120     /**
    121      * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
    122      */
    123     @Override
    124     public void onVoicemailStatusFetched(Cursor statusCursor) {
    125         if (getActivity() == null || getActivity().isFinishing()) {
    126             return;
    127         }
    128         updateVoicemailStatusMessage(statusCursor);
    129 
    130         int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor);
    131         setVoicemailSourcesAvailable(activeSources != 0);
    132         MoreCloseables.closeQuietly(statusCursor);
    133         mVoicemailStatusFetched = true;
    134         destroyEmptyLoaderIfAllDataFetched();
    135     }
    136 
    137     private void destroyEmptyLoaderIfAllDataFetched() {
    138         if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
    139             mEmptyLoaderRunning = false;
    140             getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
    141         }
    142     }
    143 
    144     /** Sets whether there are any voicemail sources available in the platform. */
    145     private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) {
    146         if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return;
    147         mVoicemailSourcesAvailable = voicemailSourcesAvailable;
    148 
    149         Activity activity = getActivity();
    150         if (activity != null) {
    151             // This is so that the options menu content is updated.
    152             activity.invalidateOptionsMenu();
    153         }
    154     }
    155 
    156     @Override
    157     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
    158         View view = inflater.inflate(R.layout.call_log_fragment, container, false);
    159         mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
    160         mStatusMessageView = view.findViewById(R.id.voicemail_status);
    161         mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
    162         mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
    163         return view;
    164     }
    165 
    166     @Override
    167     public void onViewCreated(View view, Bundle savedInstanceState) {
    168         super.onViewCreated(view, savedInstanceState);
    169         String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
    170         mAdapter = new CallLogAdapter(getActivity(), this,
    171                 new ContactInfoHelper(getActivity(), currentCountryIso));
    172         setListAdapter(mAdapter);
    173         getListView().setItemsCanFocus(true);
    174     }
    175 
    176     @Override
    177     public void onStart() {
    178         mScrollToTop = true;
    179 
    180         // Start the empty loader now to defer other fragments.  We destroy it when both calllog
    181         // and the voicemail status are fetched.
    182         getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
    183                 new EmptyLoader.Callback(getActivity()));
    184         mEmptyLoaderRunning = true;
    185         super.onStart();
    186     }
    187 
    188     @Override
    189     public void onResume() {
    190         super.onResume();
    191         refreshData();
    192     }
    193 
    194     private void updateVoicemailStatusMessage(Cursor statusCursor) {
    195         List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
    196         if (messages.size() == 0) {
    197             mStatusMessageView.setVisibility(View.GONE);
    198         } else {
    199             mStatusMessageView.setVisibility(View.VISIBLE);
    200             // TODO: Change the code to show all messages. For now just pick the first message.
    201             final StatusMessage message = messages.get(0);
    202             if (message.showInCallLog()) {
    203                 mStatusMessageText.setText(message.callLogMessageId);
    204             }
    205             if (message.actionMessageId != -1) {
    206                 mStatusMessageAction.setText(message.actionMessageId);
    207             }
    208             if (message.actionUri != null) {
    209                 mStatusMessageAction.setVisibility(View.VISIBLE);
    210                 mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
    211                     @Override
    212                     public void onClick(View v) {
    213                         getActivity().startActivity(
    214                                 new Intent(Intent.ACTION_VIEW, message.actionUri));
    215                     }
    216                 });
    217             } else {
    218                 mStatusMessageAction.setVisibility(View.GONE);
    219             }
    220         }
    221     }
    222 
    223     @Override
    224     public void onPause() {
    225         super.onPause();
    226         // Kill the requests thread
    227         mAdapter.stopRequestProcessing();
    228     }
    229 
    230     @Override
    231     public void onStop() {
    232         super.onStop();
    233         updateOnExit();
    234     }
    235 
    236     @Override
    237     public void onDestroy() {
    238         super.onDestroy();
    239         mAdapter.stopRequestProcessing();
    240         mAdapter.changeCursor(null);
    241     }
    242 
    243     @Override
    244     public void fetchCalls() {
    245         if (mShowingVoicemailOnly) {
    246             mCallLogQueryHandler.fetchVoicemailOnly();
    247         } else {
    248             mCallLogQueryHandler.fetchAllCalls();
    249         }
    250     }
    251 
    252     public void startCallsQuery() {
    253         mAdapter.setLoading(true);
    254         mCallLogQueryHandler.fetchAllCalls();
    255         if (mShowingVoicemailOnly) {
    256             mShowingVoicemailOnly = false;
    257             getActivity().invalidateOptionsMenu();
    258         }
    259     }
    260 
    261     private void startVoicemailStatusQuery() {
    262         mCallLogQueryHandler.fetchVoicemailStatus();
    263     }
    264 
    265     @Override
    266     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    267         super.onCreateOptionsMenu(menu, inflater);
    268         if (mShowOptionsMenu) {
    269             inflater.inflate(R.menu.call_log_options, menu);
    270         }
    271     }
    272 
    273     @Override
    274     public void onPrepareOptionsMenu(Menu menu) {
    275         if (mShowOptionsMenu) {
    276             final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all);
    277             // Check if all the menu items are inflated correctly. As a shortcut, we assume all
    278             // menu items are ready if the first item is non-null.
    279             if (itemDeleteAll != null) {
    280                 itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty());
    281                 menu.findItem(R.id.show_voicemails_only).setVisible(
    282                         mVoicemailSourcesAvailable && !mShowingVoicemailOnly);
    283                 menu.findItem(R.id.show_all_calls).setVisible(
    284                         mVoicemailSourcesAvailable && mShowingVoicemailOnly);
    285             }
    286         }
    287     }
    288 
    289     @Override
    290     public boolean onOptionsItemSelected(MenuItem item) {
    291         switch (item.getItemId()) {
    292             case R.id.delete_all:
    293                 ClearCallLogDialog.show(getFragmentManager());
    294                 return true;
    295 
    296             case R.id.show_voicemails_only:
    297                 mCallLogQueryHandler.fetchVoicemailOnly();
    298                 mShowingVoicemailOnly = true;
    299                 return true;
    300 
    301             case R.id.show_all_calls:
    302                 mCallLogQueryHandler.fetchAllCalls();
    303                 mShowingVoicemailOnly = false;
    304                 return true;
    305 
    306             default:
    307                 return false;
    308         }
    309     }
    310 
    311     public void callSelectedEntry() {
    312         int position = getListView().getSelectedItemPosition();
    313         if (position < 0) {
    314             // In touch mode you may often not have something selected, so
    315             // just call the first entry to make sure that [send] [send] calls the
    316             // most recent entry.
    317             position = 0;
    318         }
    319         final Cursor cursor = (Cursor)mAdapter.getItem(position);
    320         if (cursor != null) {
    321             String number = cursor.getString(CallLogQuery.NUMBER);
    322             if (TextUtils.isEmpty(number)
    323                     || number.equals(CallerInfo.UNKNOWN_NUMBER)
    324                     || number.equals(CallerInfo.PRIVATE_NUMBER)
    325                     || number.equals(CallerInfo.PAYPHONE_NUMBER)) {
    326                 // This number can't be called, do nothing
    327                 return;
    328             }
    329             Intent intent;
    330             // If "number" is really a SIP address, construct a sip: URI.
    331             if (PhoneNumberUtils.isUriNumber(number)) {
    332                 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
    333                                     Uri.fromParts("sip", number, null));
    334             } else {
    335                 // We're calling a regular PSTN phone number.
    336                 // Construct a tel: URI, but do some other possible cleanup first.
    337                 int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
    338                 if (!number.startsWith("+") &&
    339                        (callType == Calls.INCOMING_TYPE
    340                                 || callType == Calls.MISSED_TYPE)) {
    341                     // If the caller-id matches a contact with a better qualified number, use it
    342                     String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
    343                     number = mAdapter.getBetterNumberFromContacts(number, countryIso);
    344                 }
    345                 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
    346                                     Uri.fromParts("tel", number, null));
    347             }
    348             intent.setFlags(
    349                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
    350             startActivity(intent);
    351         }
    352     }
    353 
    354     @VisibleForTesting
    355     CallLogAdapter getAdapter() {
    356         return mAdapter;
    357     }
    358 
    359     @Override
    360     public void onVisibilityChanged(boolean visible) {
    361         if (mShowOptionsMenu != visible) {
    362             mShowOptionsMenu = visible;
    363             // Invalidate the options menu since we are changing the list of options shown in it.
    364             Activity activity = getActivity();
    365             if (activity != null) {
    366                 activity.invalidateOptionsMenu();
    367             }
    368         }
    369 
    370         if (visible && isResumed()) {
    371             refreshData();
    372         }
    373 
    374         if (!visible) {
    375             updateOnExit();
    376         }
    377     }
    378 
    379     /** Requests updates to the data to be shown. */
    380     private void refreshData() {
    381         // Mark all entries in the contact info cache as out of date, so they will be looked up
    382         // again once being shown.
    383         mAdapter.invalidateCache();
    384         startCallsQuery();
    385         startVoicemailStatusQuery();
    386         updateOnEntry();
    387     }
    388 
    389     /** Removes the missed call notifications. */
    390     private void removeMissedCallNotifications() {
    391         try {
    392             ITelephony telephony =
    393                     ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
    394             if (telephony != null) {
    395                 telephony.cancelMissedCallsNotification();
    396             } else {
    397                 Log.w(TAG, "Telephony service is null, can't call " +
    398                         "cancelMissedCallsNotification");
    399             }
    400         } catch (RemoteException e) {
    401             Log.e(TAG, "Failed to clear missed calls notification due to remote exception");
    402         }
    403     }
    404 
    405     /** Updates call data and notification state while leaving the call log tab. */
    406     private void updateOnExit() {
    407         updateOnTransition(false);
    408     }
    409 
    410     /** Updates call data and notification state while entering the call log tab. */
    411     private void updateOnEntry() {
    412         updateOnTransition(true);
    413     }
    414 
    415     private void updateOnTransition(boolean onEntry) {
    416         // We don't want to update any call data when keyguard is on because the user has likely not
    417         // seen the new calls yet.
    418         if (!mKeyguardManager.inKeyguardRestrictedInputMode()) {
    419             // On either of the transitions we reset the new flag and update the notifications.
    420             // While exiting we additionally consume all missed calls (by marking them as read).
    421             // This will ensure that they no more appear in the "new" section when we return back.
    422             mCallLogQueryHandler.markNewCallsAsOld();
    423             if (!onEntry) {
    424                 mCallLogQueryHandler.markMissedCallsAsRead();
    425             }
    426             removeMissedCallNotifications();
    427             updateVoicemailNotifications();
    428         }
    429     }
    430 
    431     private void updateVoicemailNotifications() {
    432         Intent serviceIntent = new Intent(getActivity(), CallLogNotificationsService.class);
    433         serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
    434         getActivity().startService(serviceIntent);
    435     }
    436 }
    437