/src/com/android/contacts/calllog/CallLogFragment.java
Java | 615 lines | 464 code | 64 blank | 87 comment | 74 complexity | f4d5aa29c0a91dc6461ad9c21d8e94fd MD5 | raw file
- /*
- * 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;
- }
- }
- }