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.dialer.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.provider.CallLog;
     30 import android.provider.CallLog.Calls;
     31 import android.provider.ContactsContract;
     32 import android.telephony.PhoneNumberUtils;
     33 import android.telephony.TelephonyManager;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.widget.ListView;
     38 import android.widget.TextView;
     39 
     40 import com.android.common.io.MoreCloseables;
     41 import com.android.contacts.common.CallUtil;
     42 import com.android.contacts.common.GeoUtil;
     43 import com.android.dialer.R;
     44 import com.android.dialer.util.EmptyLoader;
     45 import com.android.dialer.voicemail.VoicemailStatusHelper;
     46 import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
     47 import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
     48 import com.android.dialerbind.ObjectFactory;
     49 import com.android.internal.telephony.ITelephony;
     50 
     51 import java.util.List;
     52 
     53 /**
     54  * Displays a list of call log entries. To filter for a particular kind of call
     55  * (all, missed or voicemails), specify it in the constructor.
     56  */
     57 public class CallLogFragment extends ListFragment
     58         implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
     59     private static final String TAG = "CallLogFragment";
     60 
     61     /**
     62      * ID of the empty loader to defer other fragments.
     63      */
     64     private static final int EMPTY_LOADER_ID = 0;
     65 
     66     private CallLogAdapter mAdapter;
     67     private CallLogQueryHandler mCallLogQueryHandler;
     68     private boolean mScrollToTop;
     69 
     70     /** Whether there is at least one voicemail source installed. */
     71     private boolean mVoicemailSourcesAvailable = false;
     72 
     73     private VoicemailStatusHelper mVoicemailStatusHelper;
     74     private View mStatusMessageView;
     75     private TextView mStatusMessageText;
     76     private TextView mStatusMessageAction;
     77     private KeyguardManager mKeyguardManager;
     78 
     79     private boolean mEmptyLoaderRunning;
     80     private boolean mCallLogFetched;
     81     private boolean mVoicemailStatusFetched;
     82 
     83     private final Handler mHandler = new Handler();
     84 
     85     private TelephonyManager mTelephonyManager;
     86 
     87     private class CustomContentObserver extends ContentObserver {
     88         public CustomContentObserver() {
     89             super(mHandler);
     90         }
     91         @Override
     92         public void onChange(boolean selfChange) {
     93             mRefreshDataRequired = true;
     94         }
     95     }
     96 
     97     // See issue 6363009
     98     private final ContentObserver mCallLogObserver = new CustomContentObserver();
     99     private final ContentObserver mContactsObserver = new CustomContentObserver();
    100     private boolean mRefreshDataRequired = true;
    101 
    102     // Exactly same variable is in Fragment as a package private.
    103     private boolean mMenuVisible = true;
    104 
    105     // Default to all calls.
    106     private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
    107 
    108     // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler}
    109     // will be used.
    110     private int mLogLimit = -1;
    111 
    112     public CallLogFragment() {
    113         this(CallLogQueryHandler.CALL_TYPE_ALL, -1);
    114     }
    115 
    116     public CallLogFragment(int filterType) {
    117         this(filterType, -1);
    118     }
    119 
    120     public CallLogFragment(int filterType, int logLimit) {
    121         super();
    122         mCallTypeFilter = filterType;
    123         mLogLimit = logLimit;
    124     }
    125 
    126     @Override
    127     public void onCreate(Bundle state) {
    128         super.onCreate(state);
    129 
    130         mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
    131                 this, mLogLimit);
    132         mKeyguardManager =
    133                 (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
    134         getActivity().getContentResolver().registerContentObserver(CallLog.CONTENT_URI, true,
    135                 mCallLogObserver);
    136         getActivity().getContentResolver().registerContentObserver(
    137                 ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
    138         setHasOptionsMenu(true);
    139         updateCallList(mCallTypeFilter);
    140     }
    141 
    142     /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
    143     @Override
    144     public void onCallsFetched(Cursor cursor) {
    145         if (getActivity() == null || getActivity().isFinishing()) {
    146             return;
    147         }
    148         mAdapter.setLoading(false);
    149         mAdapter.changeCursor(cursor);
    150         // This will update the state of the "Clear call log" menu item.
    151         getActivity().invalidateOptionsMenu();
    152         if (mScrollToTop) {
    153             final ListView listView = getListView();
    154             // The smooth-scroll animation happens over a fixed time period.
    155             // As a result, if it scrolls through a large portion of the list,
    156             // each frame will jump so far from the previous one that the user
    157             // will not experience the illusion of downward motion.  Instead,
    158             // if we're not already near the top of the list, we instantly jump
    159             // near the top, and animate from there.
    160             if (listView.getFirstVisiblePosition() > 5) {
    161                 listView.setSelection(5);
    162             }
    163             // Workaround for framework issue: the smooth-scroll doesn't
    164             // occur if setSelection() is called immediately before.
    165             mHandler.post(new Runnable() {
    166                @Override
    167                public void run() {
    168                    if (getActivity() == null || getActivity().isFinishing()) {
    169                        return;
    170                    }
    171                    listView.smoothScrollToPosition(0);
    172                }
    173             });
    174 
    175             mScrollToTop = false;
    176         }
    177         mCallLogFetched = true;
    178         destroyEmptyLoaderIfAllDataFetched();
    179     }
    180 
    181     /**
    182      * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
    183      */
    184     @Override
    185     public void onVoicemailStatusFetched(Cursor statusCursor) {
    186         if (getActivity() == null || getActivity().isFinishing()) {
    187             return;
    188         }
    189         updateVoicemailStatusMessage(statusCursor);
    190 
    191         int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor);
    192         setVoicemailSourcesAvailable(activeSources != 0);
    193         MoreCloseables.closeQuietly(statusCursor);
    194         mVoicemailStatusFetched = true;
    195         destroyEmptyLoaderIfAllDataFetched();
    196     }
    197 
    198     private void destroyEmptyLoaderIfAllDataFetched() {
    199         if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
    200             mEmptyLoaderRunning = false;
    201             getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
    202         }
    203     }
    204 
    205     /** Sets whether there are any voicemail sources available in the platform. */
    206     private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) {
    207         if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return;
    208         mVoicemailSourcesAvailable = voicemailSourcesAvailable;
    209 
    210         Activity activity = getActivity();
    211         if (activity != null) {
    212             // This is so that the options menu content is updated.
    213             activity.invalidateOptionsMenu();
    214         }
    215     }
    216 
    217     @Override
    218     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
    219         View view = inflater.inflate(R.layout.call_log_fragment, container, false);
    220         mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
    221         mStatusMessageView = view.findViewById(R.id.voicemail_status);
    222         mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
    223         mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
    224         return view;
    225     }
    226 
    227     @Override
    228     public void onViewCreated(View view, Bundle savedInstanceState) {
    229         super.onViewCreated(view, savedInstanceState);
    230         updateEmptyMessage(mCallTypeFilter);
    231         String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
    232         mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, new ContactInfoHelper(
    233                 getActivity(), currentCountryIso), false, true);
    234         setListAdapter(mAdapter);
    235         getListView().setItemsCanFocus(true);
    236     }
    237 
    238     /**
    239      * Based on the new intent, decide whether the list should be configured
    240      * to scroll up to display the first item.
    241      */
    242     public void configureScreenFromIntent(Intent newIntent) {
    243         // Typically, when switching to the call-log we want to show the user
    244         // the same section of the list that they were most recently looking
    245         // at.  However, under some circumstances, we want to automatically
    246         // scroll to the top of the list to present the newest call items.
    247         // For example, immediately after a call is finished, we want to
    248         // display information about that call.
    249         mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType());
    250     }
    251 
    252     @Override
    253     public void onStart() {
    254         // Start the empty loader now to defer other fragments.  We destroy it when both calllog
    255         // and the voicemail status are fetched.
    256         getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
    257                 new EmptyLoader.Callback(getActivity()));
    258         mEmptyLoaderRunning = true;
    259         super.onStart();
    260     }
    261 
    262     @Override
    263     public void onResume() {
    264         super.onResume();
    265         refreshData();
    266     }
    267 
    268     private void updateVoicemailStatusMessage(Cursor statusCursor) {
    269         List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
    270         if (messages.size() == 0) {
    271             mStatusMessageView.setVisibility(View.GONE);
    272         } else {
    273             mStatusMessageView.setVisibility(View.VISIBLE);
    274             // TODO: Change the code to show all messages. For now just pick the first message.
    275             final StatusMessage message = messages.get(0);
    276             if (message.showInCallLog()) {
    277                 mStatusMessageText.setText(message.callLogMessageId);
    278             }
    279             if (message.actionMessageId != -1) {
    280                 mStatusMessageAction.setText(message.actionMessageId);
    281             }
    282             if (message.actionUri != null) {
    283                 mStatusMessageAction.setVisibility(View.VISIBLE);
    284                 mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
    285                     @Override
    286                     public void onClick(View v) {
    287                         getActivity().startActivity(
    288                                 new Intent(Intent.ACTION_VIEW, message.actionUri));
    289                     }
    290                 });
    291             } else {
    292                 mStatusMessageAction.setVisibility(View.GONE);
    293             }
    294         }
    295     }
    296 
    297     @Override
    298     public void onPause() {
    299         super.onPause();
    300         // Kill the requests thread
    301         mAdapter.stopRequestProcessing();
    302     }
    303 
    304     @Override
    305     public void onStop() {
    306         super.onStop();
    307         updateOnExit();
    308     }
    309 
    310     @Override
    311     public void onDestroy() {
    312         super.onDestroy();
    313         mAdapter.stopRequestProcessing();
    314         mAdapter.changeCursor(null);
    315         getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
    316         getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
    317     }
    318 
    319     @Override
    320     public void fetchCalls() {
    321         mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
    322     }
    323 
    324     public void startCallsQuery() {
    325         mAdapter.setLoading(true);
    326         mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
    327     }
    328 
    329     private void startVoicemailStatusQuery() {
    330         mCallLogQueryHandler.fetchVoicemailStatus();
    331     }
    332 
    333     private void updateCallList(int filterType) {
    334         mCallLogQueryHandler.fetchCalls(filterType);
    335     }
    336 
    337     private void updateEmptyMessage(int filterType) {
    338         final String message;
    339         switch (filterType) {
    340             case Calls.MISSED_TYPE:
    341                 message = getString(R.string.recentMissed_empty);
    342                 break;
    343             case CallLogQueryHandler.CALL_TYPE_ALL:
    344                 message = getString(R.string.recentCalls_empty);
    345                 break;
    346             default:
    347                 throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
    348                         + filterType);
    349         }
    350         ((TextView) getListView().getEmptyView()).setText(message);
    351     }
    352 
    353     public void callSelectedEntry() {
    354         int position = getListView().getSelectedItemPosition();
    355         if (position < 0) {
    356             // In touch mode you may often not have something selected, so
    357             // just call the first entry to make sure that [send] [send] calls the
    358             // most recent entry.
    359             position = 0;
    360         }
    361         final Cursor cursor = (Cursor)mAdapter.getItem(position);
    362         if (cursor != null) {
    363             String number = cursor.getString(CallLogQuery.NUMBER);
    364             int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION);
    365             if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) {
    366                 // This number can't be called, do nothing
    367                 return;
    368             }
    369             Intent intent;
    370             // If "number" is really a SIP address, construct a sip: URI.
    371             if (PhoneNumberUtils.isUriNumber(number)) {
    372                 intent = CallUtil.getCallIntent(
    373                         Uri.fromParts(CallUtil.SCHEME_SIP, number, null));
    374             } else {
    375                 // We're calling a regular PSTN phone number.
    376                 // Construct a tel: URI, but do some other possible cleanup first.
    377                 int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
    378                 if (!number.startsWith("+") &&
    379                        (callType == Calls.INCOMING_TYPE
    380                                 || callType == Calls.MISSED_TYPE)) {
    381                     // If the caller-id matches a contact with a better qualified number, use it
    382                     String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
    383                     number = mAdapter.getBetterNumberFromContacts(number, countryIso);
    384                 }
    385                 intent = CallUtil.getCallIntent(
    386                         Uri.fromParts(CallUtil.SCHEME_TEL, number, null));
    387             }
    388             intent.setFlags(
    389                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
    390             startActivity(intent);
    391         }
    392     }
    393 
    394     CallLogAdapter getAdapter() {
    395         return mAdapter;
    396     }
    397 
    398     @Override
    399     public void setMenuVisibility(boolean menuVisible) {
    400         super.setMenuVisibility(menuVisible);
    401         if (mMenuVisible != menuVisible) {
    402             mMenuVisible = menuVisible;
    403             if (!menuVisible) {
    404                 updateOnExit();
    405             } else if (isResumed()) {
    406                 refreshData();
    407             }
    408         }
    409     }
    410 
    411     /** Requests updates to the data to be shown. */
    412     private void refreshData() {
    413         // Prevent unnecessary refresh.
    414         if (mRefreshDataRequired) {
    415             // Mark all entries in the contact info cache as out of date, so they will be looked up
    416             // again once being shown.
    417             mAdapter.invalidateCache();
    418             startCallsQuery();
    419             startVoicemailStatusQuery();
    420             updateOnEntry();
    421             mRefreshDataRequired = false;
    422         }
    423     }
    424 
    425     /** Updates call data and notification state while leaving the call log tab. */
    426     private void updateOnExit() {
    427         updateOnTransition(false);
    428     }
    429 
    430     /** Updates call data and notification state while entering the call log tab. */
    431     private void updateOnEntry() {
    432         updateOnTransition(true);
    433     }
    434 
    435     // TODO: Move to CallLogActivity
    436     private void updateOnTransition(boolean onEntry) {
    437         // We don't want to update any call data when keyguard is on because the user has likely not
    438         // seen the new calls yet.
    439         // This might be called before onCreate() and thus we need to check null explicitly.
    440         if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
    441             // On either of the transitions we update the missed call and voicemail notifications.
    442             // While exiting we additionally consume all missed calls (by marking them as read).
    443             mCallLogQueryHandler.markNewCallsAsOld();
    444             if (!onEntry) {
    445                 mCallLogQueryHandler.markMissedCallsAsRead();
    446             }
    447             CallLogNotificationsHelper.removeMissedCallNotifications();
    448             CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
    449         }
    450     }
    451 }
    452