blob: 36d9bb6ea451dbc679c2d9c211bba707d0494ddb [file] [log] [blame]
/*
* 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.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.KeyguardManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract;
import android.provider.VoicemailContract.Status;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.ListView;
import android.widget.TextView;
import com.android.contacts.common.GeoUtil;
import com.android.contacts.common.util.ViewUtil;
import com.android.dialer.R;
import com.android.dialer.list.ListsFragment.HostInterface;
import com.android.dialer.util.DialerUtils;
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 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 Fragment
implements CallLogQueryHandler.Listener, CallLogAdapter.OnReportButtonClickListener,
CallLogAdapter.CallFetcher {
private static final String TAG = "CallLogFragment";
private static final String REPORT_DIALOG_TAG = "report_dialog";
/**
* ID of the empty loader to defer other fragments.
*/
private static final int EMPTY_LOADER_ID = 0;
private static final String KEY_FILTER_TYPE = "filter_type";
private static final String KEY_LOG_LIMIT = "log_limit";
private static final String KEY_DATE_LIMIT = "date_limit";
// No limit specified for the number of logs to show; use the CallLogQueryHandler's default.
private static final int NO_LOG_LIMIT = -1;
// No date-based filtering.
private static final int NO_DATE_LIMIT = 0;
private RecyclerView mRecyclerView;
private LinearLayoutManager mLayoutManager;
private CallLogAdapter mAdapter;
private CallLogQueryHandler mCallLogQueryHandler;
private boolean mScrollToTop;
/** Whether there is at least one voicemail source installed. */
private boolean mVoicemailSourcesAvailable = false;
private View mEmptyListView;
private KeyguardManager mKeyguardManager;
private boolean mEmptyLoaderRunning;
private boolean mCallLogFetched;
private boolean mVoicemailStatusFetched;
private final Handler mHandler = new Handler();
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 final ContentObserver mVoicemailStatusObserver = 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 = NO_LOG_LIMIT;
// Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after
// the date filter are included. If zero, no date-based filtering occurs.
private long mDateLimit = NO_DATE_LIMIT;
public CallLogFragment() {
this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT);
}
public CallLogFragment(int filterType) {
this(filterType, NO_LOG_LIMIT);
}
public CallLogFragment(int filterType, int logLimit) {
super();
mCallTypeFilter = filterType;
mLogLimit = logLimit;
}
/**
* Creates a call log fragment, filtering to include only calls of the desired type, occurring
* after the specified date.
* @param filterType type of calls to include.
* @param dateLimit limits results to calls occurring on or after the specified date.
*/
public CallLogFragment(int filterType, long dateLimit) {
this(filterType, NO_LOG_LIMIT, dateLimit);
}
/**
* Creates a call log fragment, filtering to include only calls of the desired type, occurring
* after the specified date. Also provides a means to limit the number of results returned.
* @param filterType type of calls to include.
* @param logLimit limits the number of results to return.
* @param dateLimit limits results to calls occurring on or after the specified date.
*/
public CallLogFragment(int filterType, int logLimit, long dateLimit) {
this(filterType, logLimit);
mDateLimit = dateLimit;
}
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
if (state != null) {
mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
}
final Activity activity = getActivity();
final ContentResolver resolver = activity.getContentResolver();
String currentCountryIso = GeoUtil.getCurrentCountryIso(activity);
mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit);
mKeyguardManager =
(KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver);
resolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true,
mContactsObserver);
resolver.registerContentObserver(Status.CONTENT_URI, true, mVoicemailStatusObserver);
setHasOptionsMenu(true);
}
/** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
@Override
public boolean onCallsFetched(Cursor cursor) {
if (getActivity() == null || getActivity().isFinishing()) {
// Return false; we did not take ownership of the cursor
return false;
}
mAdapter.setLoading(false);
mAdapter.changeCursor(cursor);
// This will update the state of the "Clear call log" menu item.
getActivity().invalidateOptionsMenu();
boolean showListView = cursor != null && cursor.getCount() > 0;
mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
if (mScrollToTop) {
// 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 (mLayoutManager.findFirstVisibleItemPosition() > 5) {
// TODO: Jump to near the top, then begin smooth scroll.
mRecyclerView.smoothScrollToPosition(0);
}
// 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;
}
mRecyclerView.smoothScrollToPosition(0);
}
});
mScrollToTop = false;
}
mCallLogFetched = true;
destroyEmptyLoaderIfAllDataFetched();
return true;
}
/**
* Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
*/
@Override
public void onVoicemailStatusFetched(Cursor statusCursor) {
Activity activity = getActivity();
if (activity == null || activity.isFinishing()) {
return;
}
mVoicemailStatusFetched = true;
destroyEmptyLoaderIfAllDataFetched();
}
private void destroyEmptyLoaderIfAllDataFetched() {
if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
mEmptyLoaderRunning = false;
getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
View view = inflater.inflate(R.layout.call_log_fragment, container, false);
mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(getActivity());
mRecyclerView.setLayoutManager(mLayoutManager);
mEmptyListView = view.findViewById(R.id.empty_list_view);
String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
boolean isShowingRecentsTab = mLogLimit != NO_LOG_LIMIT || mDateLimit != NO_DATE_LIMIT;
mAdapter = ObjectFactory.newCallLogAdapter(
getActivity(),
this,
new ContactInfoHelper(getActivity(), currentCountryIso),
isShowingRecentsTab,
this);
mRecyclerView.setAdapter(mAdapter);
fetchCalls();
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
updateEmptyMessage(mCallTypeFilter);
}
/**
* 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();
}
@Override
public void onPause() {
super.onPause();
mAdapter.pauseCache();
}
@Override
public void onStop() {
super.onStop();
updateOnTransition(false /* onEntry */);
}
@Override
public void onDestroy() {
super.onDestroy();
mAdapter.pauseCache();
mAdapter.changeCursor(null);
getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
outState.putInt(KEY_LOG_LIMIT, mLogLimit);
outState.putLong(KEY_DATE_LIMIT, mDateLimit);
}
@Override
public void fetchCalls() {
mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
}
private void updateEmptyMessage(int filterType) {
final int messageId;
switch (filterType) {
case Calls.MISSED_TYPE:
messageId = R.string.recentMissed_empty;
break;
case Calls.VOICEMAIL_TYPE:
messageId = R.string.recentVoicemails_empty;
break;
case CallLogQueryHandler.CALL_TYPE_ALL:
messageId = R.string.recentCalls_empty;
break;
default:
throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
+ filterType);
}
DialerUtils.configureEmptyListView(
mEmptyListView, R.drawable.empty_call_log, messageId, getResources());
}
CallLogAdapter getAdapter() {
return mAdapter;
}
@Override
public void setMenuVisibility(boolean menuVisible) {
super.setMenuVisibility(menuVisible);
if (mMenuVisible != menuVisible) {
mMenuVisible = menuVisible;
if (!menuVisible) {
updateOnTransition(false /* onEntry */);
} 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();
mAdapter.setLoading(true);
fetchCalls();
mCallLogQueryHandler.fetchVoicemailStatus();
updateOnTransition(true /* onEntry */);
mRefreshDataRequired = false;
} else {
// Refresh the display of the existing data to update the timestamp text descriptions.
mAdapter.notifyDataSetChanged();
}
}
/**
* Updates the call data and notification state on entering or leaving the call log tab.
*
* If we are leaving the call log tab, mark all the missed calls as read.
*
* 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(getActivity());
CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
}
}
public void onBadDataReported(String number) {
if (number == null) {
return;
}
mAdapter.invalidateCache();
mAdapter.notifyDataSetChanged();
}
public void onReportButtonClick(String number) {
DialogFragment df = ObjectFactory.getReportDialogFragment(number);
if (df != null) {
df.setTargetFragment(this, 0);
df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG);
}
}
}