| /* |
| * 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.dialer.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.provider.CallLog; |
| import android.provider.CallLog.Calls; |
| import android.provider.ContactsContract; |
| import android.telephony.PhoneNumberUtils; |
| import android.telephony.TelephonyManager; |
| import android.view.LayoutInflater; |
| 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.common.CallUtil; |
| import com.android.contacts.common.GeoUtil; |
| import com.android.dialer.R; |
| import com.android.dialer.util.EmptyLoader; |
| import com.android.dialer.voicemail.VoicemailStatusHelper; |
| import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage; |
| import com.android.dialer.voicemail.VoicemailStatusHelperImpl; |
| import com.android.dialerbind.ObjectFactory; |
| import com.android.internal.telephony.ITelephony; |
| |
| import java.util.List; |
| |
| /** |
| * Displays a list of call log entries. To filter for a particular kind of call |
| * (all, missed or voicemails), specify it in the constructor. |
| */ |
| 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 KeyguardManager mKeyguardManager; |
| |
| private boolean mEmptyLoaderRunning; |
| private boolean mCallLogFetched; |
| private boolean mVoicemailStatusFetched; |
| |
| private final Handler mHandler = new Handler(); |
| |
| private TelephonyManager mTelephonyManager; |
| |
| 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; |
| |
| // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler} |
| // will be used. |
| private int mLogLimit = -1; |
| |
| public CallLogFragment() { |
| this(CallLogQueryHandler.CALL_TYPE_ALL, -1); |
| } |
| |
| public CallLogFragment(int filterType) { |
| this(filterType, -1); |
| } |
| |
| public CallLogFragment(int filterType, int logLimit) { |
| super(); |
| mCallTypeFilter = filterType; |
| mLogLimit = logLimit; |
| } |
| |
| @Override |
| public void onCreate(Bundle state) { |
| super.onCreate(state); |
| |
| mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), |
| this, mLogLimit); |
| 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); |
| updateCallList(mCallTypeFilter); |
| } |
| |
| /** 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); |
| return view; |
| } |
| |
| @Override |
| public void onViewCreated(View view, Bundle savedInstanceState) { |
| super.onViewCreated(view, savedInstanceState); |
| updateEmptyMessage(mCallTypeFilter); |
| String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); |
| mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, new ContactInfoHelper( |
| getActivity(), currentCountryIso), false, true); |
| 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); |
| } |
| |
| @Override |
| public void fetchCalls() { |
| mCallLogQueryHandler.fetchCalls(mCallTypeFilter); |
| } |
| |
| public void startCallsQuery() { |
| mAdapter.setLoading(true); |
| mCallLogQueryHandler.fetchCalls(mCallTypeFilter); |
| } |
| |
| private void startVoicemailStatusQuery() { |
| mCallLogQueryHandler.fetchVoicemailStatus(); |
| } |
| |
| private void updateCallList(int filterType) { |
| mCallLogQueryHandler.fetchCalls(filterType); |
| } |
| |
| private void updateEmptyMessage(int filterType) { |
| final String message; |
| switch (filterType) { |
| case Calls.MISSED_TYPE: |
| message = getString(R.string.recentMissed_empty); |
| break; |
| case CallLogQueryHandler.CALL_TYPE_ALL: |
| message = getString(R.string.recentCalls_empty); |
| break; |
| default: |
| throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: " |
| + filterType); |
| } |
| ((TextView) getListView().getEmptyView()).setText(message); |
| } |
| |
| 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); |
| int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION); |
| if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) { |
| // 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 = CallUtil.getCallIntent( |
| Uri.fromParts(CallUtil.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 = CallUtil.getCallIntent( |
| Uri.fromParts(CallUtil.SCHEME_TEL, number, null)); |
| } |
| intent.setFlags( |
| Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); |
| startActivity(intent); |
| } |
| } |
| |
| 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; |
| } |
| } |
| |
| /** 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); |
| } |
| |
| // TODO: Move to CallLogActivity |
| 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 update the missed call and voicemail notifications. |
| // While exiting we additionally consume all missed calls (by marking them as read). |
| mCallLogQueryHandler.markNewCallsAsOld(); |
| if (!onEntry) { |
| mCallLogQueryHandler.markMissedCallsAsRead(); |
| } |
| CallLogNotificationsHelper.removeMissedCallNotifications(); |
| CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); |
| } |
| } |
| } |