| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.contacts.calllog; |
| |
| import android.app.Activity; |
| import android.app.KeyguardManager; |
| import android.app.ListFragment; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.provider.CallLog; |
| import android.provider.CallLog.Calls; |
| import android.provider.ContactsContract; |
| import android.telephony.PhoneNumberUtils; |
| import android.telephony.PhoneStateListener; |
| import android.telephony.TelephonyManager; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.ListView; |
| import android.widget.TextView; |
| |
| import com.android.common.io.MoreCloseables; |
| import com.android.contacts.ContactsUtils; |
| import com.android.contacts.R; |
| import com.android.contacts.util.Constants; |
| import com.android.contacts.util.EmptyLoader; |
| import com.android.contacts.voicemail.VoicemailStatusHelper; |
| import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage; |
| import com.android.contacts.voicemail.VoicemailStatusHelperImpl; |
| import com.android.internal.telephony.CallerInfo; |
| import com.android.internal.telephony.ITelephony; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import java.util.List; |
| |
| /** |
| * Displays a list of call log entries. |
| */ |
| public class CallLogFragment extends ListFragment |
| implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher { |
| private static final String TAG = "CallLogFragment"; |
| |
| /** |
| * ID of the empty loader to defer other fragments. |
| */ |
| private static final int EMPTY_LOADER_ID = 0; |
| |
| private CallLogAdapter mAdapter; |
| private CallLogQueryHandler mCallLogQueryHandler; |
| private boolean mScrollToTop; |
| |
| /** Whether there is at least one voicemail source installed. */ |
| private boolean mVoicemailSourcesAvailable = false; |
| |
| private VoicemailStatusHelper mVoicemailStatusHelper; |
| private View mStatusMessageView; |
| private TextView mStatusMessageText; |
| private TextView mStatusMessageAction; |
| private TextView mFilterStatusView; |
| private KeyguardManager mKeyguardManager; |
| |
| private boolean mEmptyLoaderRunning; |
| private boolean mCallLogFetched; |
| private boolean mVoicemailStatusFetched; |
| |
| private final Handler mHandler = new Handler(); |
| |
| private TelephonyManager mTelephonyManager; |
| private PhoneStateListener mPhoneStateListener; |
| |
| private class CustomContentObserver extends ContentObserver { |
| public CustomContentObserver() { |
| super(mHandler); |
| } |
| @Override |
| public void onChange(boolean selfChange) { |
| mRefreshDataRequired = true; |
| } |
| } |
| |
| // See issue 6363009 |
| private final ContentObserver mCallLogObserver = new CustomContentObserver(); |
| private final ContentObserver mContactsObserver = new CustomContentObserver(); |
| private boolean mRefreshDataRequired = true; |
| |
| // Exactly same variable is in Fragment as a package private. |
| private boolean mMenuVisible = true; |
| |
| // Default to all calls. |
| private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; |
| |
| @Override |
| public void onCreate(Bundle state) { |
| super.onCreate(state); |
| |
| mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this); |
| mKeyguardManager = |
| (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE); |
| getActivity().getContentResolver().registerContentObserver( |
| CallLog.CONTENT_URI, true, mCallLogObserver); |
| getActivity().getContentResolver().registerContentObserver( |
| ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver); |
| setHasOptionsMenu(true); |
| } |
| |
| /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ |
| @Override |
| public void onCallsFetched(Cursor cursor) { |
| if (getActivity() == null || getActivity().isFinishing()) { |
| return; |
| } |
| mAdapter.setLoading(false); |
| mAdapter.changeCursor(cursor); |
| // This will update the state of the "Clear call log" menu item. |
| getActivity().invalidateOptionsMenu(); |
| if (mScrollToTop) { |
| final ListView listView = getListView(); |
| // The smooth-scroll animation happens over a fixed time period. |
| // As a result, if it scrolls through a large portion of the list, |
| // each frame will jump so far from the previous one that the user |
| // will not experience the illusion of downward motion. Instead, |
| // if we're not already near the top of the list, we instantly jump |
| // near the top, and animate from there. |
| if (listView.getFirstVisiblePosition() > 5) { |
| listView.setSelection(5); |
| } |
| // Workaround for framework issue: the smooth-scroll doesn't |
| // occur if setSelection() is called immediately before. |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| if (getActivity() == null || getActivity().isFinishing()) { |
| return; |
| } |
| listView.smoothScrollToPosition(0); |
| } |
| }); |
| |
| mScrollToTop = false; |
| } |
| mCallLogFetched = true; |
| destroyEmptyLoaderIfAllDataFetched(); |
| } |
| |
| /** |
| * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider. |
| */ |
| @Override |
| public void onVoicemailStatusFetched(Cursor statusCursor) { |
| if (getActivity() == null || getActivity().isFinishing()) { |
| return; |
| } |
| updateVoicemailStatusMessage(statusCursor); |
| |
| int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor); |
| setVoicemailSourcesAvailable(activeSources != 0); |
| MoreCloseables.closeQuietly(statusCursor); |
| mVoicemailStatusFetched = true; |
| destroyEmptyLoaderIfAllDataFetched(); |
| } |
| |
| private void destroyEmptyLoaderIfAllDataFetched() { |
| if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) { |
| mEmptyLoaderRunning = false; |
| getLoaderManager().destroyLoader(EMPTY_LOADER_ID); |
| } |
| } |
| |
| /** Sets whether there are any voicemail sources available in the platform. */ |
| private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) { |
| if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return; |
| mVoicemailSourcesAvailable = voicemailSourcesAvailable; |
| |
| Activity activity = getActivity(); |
| if (activity != null) { |
| // This is so that the options menu content is updated. |
| activity.invalidateOptionsMenu(); |
| } |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { |
| View view = inflater.inflate(R.layout.call_log_fragment, container, false); |
| mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); |
| mStatusMessageView = view.findViewById(R.id.voicemail_status); |
| mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message); |
| mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action); |
| mFilterStatusView = (TextView) view.findViewById(R.id.filter_status); |
| return view; |
| } |
| |
| @Override |
| public void onViewCreated(View view, Bundle savedInstanceState) { |
| super.onViewCreated(view, savedInstanceState); |
| String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity()); |
| mAdapter = new CallLogAdapter(getActivity(), this, |
| new ContactInfoHelper(getActivity(), currentCountryIso)); |
| setListAdapter(mAdapter); |
| getListView().setItemsCanFocus(true); |
| } |
| |
| /** |
| * Based on the new intent, decide whether the list should be configured |
| * to scroll up to display the first item. |
| */ |
| public void configureScreenFromIntent(Intent newIntent) { |
| // Typically, when switching to the call-log we want to show the user |
| // the same section of the list that they were most recently looking |
| // at. However, under some circumstances, we want to automatically |
| // scroll to the top of the list to present the newest call items. |
| // For example, immediately after a call is finished, we want to |
| // display information about that call. |
| mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType()); |
| } |
| |
| @Override |
| public void onStart() { |
| // Start the empty loader now to defer other fragments. We destroy it when both calllog |
| // and the voicemail status are fetched. |
| getLoaderManager().initLoader(EMPTY_LOADER_ID, null, |
| new EmptyLoader.Callback(getActivity())); |
| mEmptyLoaderRunning = true; |
| super.onStart(); |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| refreshData(); |
| } |
| |
| private void updateVoicemailStatusMessage(Cursor statusCursor) { |
| List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor); |
| if (messages.size() == 0) { |
| mStatusMessageView.setVisibility(View.GONE); |
| } else { |
| mStatusMessageView.setVisibility(View.VISIBLE); |
| // TODO: Change the code to show all messages. For now just pick the first message. |
| final StatusMessage message = messages.get(0); |
| if (message.showInCallLog()) { |
| mStatusMessageText.setText(message.callLogMessageId); |
| } |
| if (message.actionMessageId != -1) { |
| mStatusMessageAction.setText(message.actionMessageId); |
| } |
| if (message.actionUri != null) { |
| mStatusMessageAction.setVisibility(View.VISIBLE); |
| mStatusMessageAction.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| getActivity().startActivity( |
| new Intent(Intent.ACTION_VIEW, message.actionUri)); |
| } |
| }); |
| } else { |
| mStatusMessageAction.setVisibility(View.GONE); |
| } |
| } |
| } |
| |
| @Override |
| public void onPause() { |
| super.onPause(); |
| // Kill the requests thread |
| mAdapter.stopRequestProcessing(); |
| } |
| |
| @Override |
| public void onStop() { |
| super.onStop(); |
| updateOnExit(); |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| mAdapter.stopRequestProcessing(); |
| mAdapter.changeCursor(null); |
| getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); |
| getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); |
| unregisterPhoneCallReceiver(); |
| } |
| |
| @Override |
| public void fetchCalls() { |
| mCallLogQueryHandler.fetchCalls(mCallTypeFilter); |
| } |
| |
| public void startCallsQuery() { |
| mAdapter.setLoading(true); |
| mCallLogQueryHandler.fetchCalls(mCallTypeFilter); |
| } |
| |
| private void startVoicemailStatusQuery() { |
| mCallLogQueryHandler.fetchVoicemailStatus(); |
| } |
| |
| @Override |
| public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |
| super.onCreateOptionsMenu(menu, inflater); |
| inflater.inflate(R.menu.call_log_options, menu); |
| } |
| |
| @Override |
| public void onPrepareOptionsMenu(Menu menu) { |
| final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all); |
| // Check if all the menu items are inflated correctly. As a shortcut, we assume all |
| // menu items are ready if the first item is non-null. |
| if (itemDeleteAll != null) { |
| itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty()); |
| |
| showAllFilterMenuOptions(menu); |
| hideCurrentFilterMenuOption(menu); |
| |
| // Only hide if not available. Let the above calls handle showing. |
| if (!mVoicemailSourcesAvailable) { |
| menu.findItem(R.id.show_voicemails_only).setVisible(false); |
| } |
| } |
| } |
| |
| private void hideCurrentFilterMenuOption(Menu menu) { |
| MenuItem item = null; |
| switch (mCallTypeFilter) { |
| case CallLogQueryHandler.CALL_TYPE_ALL: |
| item = menu.findItem(R.id.show_all_calls); |
| break; |
| case Calls.INCOMING_TYPE: |
| item = menu.findItem(R.id.show_incoming_only); |
| break; |
| case Calls.OUTGOING_TYPE: |
| item = menu.findItem(R.id.show_outgoing_only); |
| break; |
| case Calls.MISSED_TYPE: |
| item = menu.findItem(R.id.show_missed_only); |
| break; |
| case Calls.VOICEMAIL_TYPE: |
| menu.findItem(R.id.show_voicemails_only); |
| break; |
| } |
| if (item != null) { |
| item.setVisible(false); |
| } |
| } |
| |
| private void showAllFilterMenuOptions(Menu menu) { |
| menu.findItem(R.id.show_all_calls).setVisible(true); |
| menu.findItem(R.id.show_incoming_only).setVisible(true); |
| menu.findItem(R.id.show_outgoing_only).setVisible(true); |
| menu.findItem(R.id.show_missed_only).setVisible(true); |
| menu.findItem(R.id.show_voicemails_only).setVisible(true); |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| switch (item.getItemId()) { |
| case R.id.delete_all: |
| ClearCallLogDialog.show(getFragmentManager()); |
| return true; |
| |
| case R.id.show_outgoing_only: |
| // We only need the phone call receiver when there is an active call type filter. |
| // Not many people may use the filters so don't register the receiver until now . |
| registerPhoneCallReceiver(); |
| mCallLogQueryHandler.fetchCalls(Calls.OUTGOING_TYPE); |
| updateFilterTypeAndHeader(Calls.OUTGOING_TYPE); |
| return true; |
| |
| case R.id.show_incoming_only: |
| registerPhoneCallReceiver(); |
| mCallLogQueryHandler.fetchCalls(Calls.INCOMING_TYPE); |
| updateFilterTypeAndHeader(Calls.INCOMING_TYPE); |
| return true; |
| |
| case R.id.show_missed_only: |
| registerPhoneCallReceiver(); |
| mCallLogQueryHandler.fetchCalls(Calls.MISSED_TYPE); |
| updateFilterTypeAndHeader(Calls.MISSED_TYPE); |
| return true; |
| |
| case R.id.show_voicemails_only: |
| registerPhoneCallReceiver(); |
| mCallLogQueryHandler.fetchCalls(Calls.VOICEMAIL_TYPE); |
| updateFilterTypeAndHeader(Calls.VOICEMAIL_TYPE); |
| return true; |
| |
| case R.id.show_all_calls: |
| // Filter is being turned off, receiver no longer needed. |
| unregisterPhoneCallReceiver(); |
| mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL); |
| updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL); |
| return true; |
| |
| default: |
| return false; |
| } |
| } |
| |
| private void updateFilterTypeAndHeader(int filterType) { |
| mCallTypeFilter = filterType; |
| |
| switch (filterType) { |
| case CallLogQueryHandler.CALL_TYPE_ALL: |
| mFilterStatusView.setVisibility(View.GONE); |
| break; |
| case Calls.INCOMING_TYPE: |
| showFilterStatus(R.string.call_log_incoming_header); |
| break; |
| case Calls.OUTGOING_TYPE: |
| showFilterStatus(R.string.call_log_outgoing_header); |
| break; |
| case Calls.MISSED_TYPE: |
| showFilterStatus(R.string.call_log_missed_header); |
| break; |
| case Calls.VOICEMAIL_TYPE: |
| showFilterStatus(R.string.call_log_voicemail_header); |
| break; |
| } |
| } |
| |
| private void showFilterStatus(int resId) { |
| mFilterStatusView.setText(resId); |
| mFilterStatusView.setVisibility(View.VISIBLE); |
| } |
| |
| public void callSelectedEntry() { |
| int position = getListView().getSelectedItemPosition(); |
| if (position < 0) { |
| // In touch mode you may often not have something selected, so |
| // just call the first entry to make sure that [send] [send] calls the |
| // most recent entry. |
| position = 0; |
| } |
| final Cursor cursor = (Cursor)mAdapter.getItem(position); |
| if (cursor != null) { |
| String number = cursor.getString(CallLogQuery.NUMBER); |
| if (TextUtils.isEmpty(number) |
| || number.equals(CallerInfo.UNKNOWN_NUMBER) |
| || number.equals(CallerInfo.PRIVATE_NUMBER) |
| || number.equals(CallerInfo.PAYPHONE_NUMBER)) { |
| // This number can't be called, do nothing |
| return; |
| } |
| Intent intent; |
| // If "number" is really a SIP address, construct a sip: URI. |
| if (PhoneNumberUtils.isUriNumber(number)) { |
| intent = ContactsUtils.getCallIntent( |
| Uri.fromParts(Constants.SCHEME_SIP, number, null)); |
| } else { |
| // We're calling a regular PSTN phone number. |
| // Construct a tel: URI, but do some other possible cleanup first. |
| int callType = cursor.getInt(CallLogQuery.CALL_TYPE); |
| if (!number.startsWith("+") && |
| (callType == Calls.INCOMING_TYPE |
| || callType == Calls.MISSED_TYPE)) { |
| // If the caller-id matches a contact with a better qualified number, use it |
| String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO); |
| number = mAdapter.getBetterNumberFromContacts(number, countryIso); |
| } |
| intent = ContactsUtils.getCallIntent( |
| Uri.fromParts(Constants.SCHEME_TEL, number, null)); |
| } |
| intent.setFlags( |
| Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); |
| startActivity(intent); |
| } |
| } |
| |
| @VisibleForTesting |
| CallLogAdapter getAdapter() { |
| return mAdapter; |
| } |
| |
| @Override |
| public void setMenuVisibility(boolean menuVisible) { |
| super.setMenuVisibility(menuVisible); |
| if (mMenuVisible != menuVisible) { |
| mMenuVisible = menuVisible; |
| if (!menuVisible) { |
| updateOnExit(); |
| } else if (isResumed()) { |
| refreshData(); |
| } |
| } |
| } |
| |
| /** Requests updates to the data to be shown. */ |
| private void refreshData() { |
| // Prevent unnecessary refresh. |
| if (mRefreshDataRequired) { |
| // Mark all entries in the contact info cache as out of date, so they will be looked up |
| // again once being shown. |
| mAdapter.invalidateCache(); |
| startCallsQuery(); |
| startVoicemailStatusQuery(); |
| updateOnEntry(); |
| mRefreshDataRequired = false; |
| } |
| } |
| |
| /** Removes the missed call notifications. */ |
| private void removeMissedCallNotifications() { |
| try { |
| ITelephony telephony = |
| ITelephony.Stub.asInterface(ServiceManager.getService("phone")); |
| if (telephony != null) { |
| telephony.cancelMissedCallsNotification(); |
| } else { |
| Log.w(TAG, "Telephony service is null, can't call " + |
| "cancelMissedCallsNotification"); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to clear missed calls notification due to remote exception"); |
| } |
| } |
| |
| /** Updates call data and notification state while leaving the call log tab. */ |
| private void updateOnExit() { |
| updateOnTransition(false); |
| } |
| |
| /** Updates call data and notification state while entering the call log tab. */ |
| private void updateOnEntry() { |
| updateOnTransition(true); |
| } |
| |
| private void updateOnTransition(boolean onEntry) { |
| // We don't want to update any call data when keyguard is on because the user has likely not |
| // seen the new calls yet. |
| // This might be called before onCreate() and thus we need to check null explicitly. |
| if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) { |
| // On either of the transitions we reset the new flag and update the notifications. |
| // While exiting we additionally consume all missed calls (by marking them as read). |
| // This will ensure that they no more appear in the "new" section when we return back. |
| mCallLogQueryHandler.markNewCallsAsOld(); |
| if (!onEntry) { |
| mCallLogQueryHandler.markMissedCallsAsRead(); |
| } |
| removeMissedCallNotifications(); |
| updateVoicemailNotifications(); |
| } |
| } |
| |
| private void updateVoicemailNotifications() { |
| Intent serviceIntent = new Intent(getActivity(), CallLogNotificationsService.class); |
| serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS); |
| getActivity().startService(serviceIntent); |
| } |
| |
| /** |
| * Register a phone call filter to reset the call type when a phone call is place. |
| */ |
| private void registerPhoneCallReceiver() { |
| if (mPhoneStateListener != null) { |
| return; // Already registered. |
| } |
| mTelephonyManager = (TelephonyManager) getActivity().getSystemService( |
| Context.TELEPHONY_SERVICE); |
| mPhoneStateListener = new PhoneStateListener() { |
| @Override |
| public void onCallStateChanged(int state, String incomingNumber) { |
| if (state != TelephonyManager.CALL_STATE_OFFHOOK && |
| state != TelephonyManager.CALL_STATE_RINGING) { |
| return; |
| } |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| if (getActivity() == null || getActivity().isFinishing()) { |
| return; |
| } |
| updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL); |
| } |
| }); |
| } |
| }; |
| mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); |
| } |
| |
| /** |
| * Un-registers the phone call receiver. |
| */ |
| private void unregisterPhoneCallReceiver() { |
| if (mPhoneStateListener != null) { |
| mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); |
| mPhoneStateListener = null; |
| } |
| } |
| } |