/* * 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 com.android.common.io.MoreCloseables; import com.android.contacts.ContactsUtils; import com.android.contacts.R; import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener; 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 android.app.Activity; import android.app.KeyguardManager; import android.app.ListFragment; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; import android.provider.CallLog.Calls; import android.telephony.PhoneNumberUtils; 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 java.util.List; /** * Displays a list of call log entries. */ public class CallLogFragment extends ListFragment implements ViewPagerVisibilityListener, 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; private boolean mShowOptionsMenu; /** Whether there is at least one voicemail source installed. */ private boolean mVoicemailSourcesAvailable = false; /** Whether we are currently filtering over voicemail. */ private boolean mShowingVoicemailOnly = false; private VoicemailStatusHelper mVoicemailStatusHelper; private View mStatusMessageView; private TextView mStatusMessageText; private TextView mStatusMessageAction; private KeyguardManager mKeyguardManager; private boolean mEmptyLoaderRunning; private boolean mCallLogFetched; private boolean mVoicemailStatusFetched; @Override public void onCreate(Bundle state) { super.onCreate(state); mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this); mKeyguardManager = (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE); 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(); if (listView.getFirstVisiblePosition() > 5) { listView.setSelection(5); } 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); 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); } @Override public void onStart() { mScrollToTop = true; // 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); } @Override public void fetchCalls() { if (mShowingVoicemailOnly) { mCallLogQueryHandler.fetchVoicemailOnly(); } else { mCallLogQueryHandler.fetchAllCalls(); } } public void startCallsQuery() { mAdapter.setLoading(true); mCallLogQueryHandler.fetchAllCalls(); if (mShowingVoicemailOnly) { mShowingVoicemailOnly = false; getActivity().invalidateOptionsMenu(); } } private void startVoicemailStatusQuery() { mCallLogQueryHandler.fetchVoicemailStatus(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); if (mShowOptionsMenu) { inflater.inflate(R.menu.call_log_options, menu); } } @Override public void onPrepareOptionsMenu(Menu menu) { if (mShowOptionsMenu) { 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()); menu.findItem(R.id.show_voicemails_only).setVisible( mVoicemailSourcesAvailable && !mShowingVoicemailOnly); menu.findItem(R.id.show_all_calls).setVisible( mVoicemailSourcesAvailable && mShowingVoicemailOnly); } } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.delete_all: ClearCallLogDialog.show(getFragmentManager()); return true; case R.id.show_voicemails_only: mCallLogQueryHandler.fetchVoicemailOnly(); mShowingVoicemailOnly = true; return true; case R.id.show_all_calls: mCallLogQueryHandler.fetchAllCalls(); mShowingVoicemailOnly = false; return true; default: return false; } } 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 = new Intent(Intent.ACTION_CALL_PRIVILEGED, Uri.fromParts("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 = new Intent(Intent.ACTION_CALL_PRIVILEGED, Uri.fromParts("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 onVisibilityChanged(boolean visible) { if (mShowOptionsMenu != visible) { mShowOptionsMenu = visible; // Invalidate the options menu since we are changing the list of options shown in it. Activity activity = getActivity(); if (activity != null) { activity.invalidateOptionsMenu(); } } if (visible && isResumed()) { refreshData(); } if (!visible) { updateOnExit(); } } /** Requests updates to the data to be shown. */ private void refreshData() { // 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(); } /** 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. if (!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); } }