CallLogFragment.java revision bb48628db6fd444460df61be7afa7fb633f47f50
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.dialer.calllog;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.app.Activity;
23import android.app.DialogFragment;
24import android.app.Fragment;
25import android.app.KeyguardManager;
26import android.content.ContentResolver;
27import android.content.Context;
28import android.content.Intent;
29import android.database.ContentObserver;
30import android.database.Cursor;
31import android.graphics.Rect;
32import android.os.Bundle;
33import android.os.Handler;
34import android.provider.CallLog;
35import android.provider.CallLog.Calls;
36import android.provider.ContactsContract;
37import android.provider.VoicemailContract.Status;
38import android.support.v7.widget.RecyclerView;
39import android.support.v7.widget.LinearLayoutManager;
40import android.view.LayoutInflater;
41import android.view.View;
42import android.view.ViewGroup;
43import android.view.View.OnClickListener;
44import android.view.ViewGroup.LayoutParams;
45import android.widget.ListView;
46import android.widget.TextView;
47
48import com.android.contacts.common.GeoUtil;
49import com.android.contacts.common.util.ViewUtil;
50import com.android.dialer.R;
51import com.android.dialer.list.ListsFragment.HostInterface;
52import com.android.dialer.util.DialerUtils;
53import com.android.dialer.util.EmptyLoader;
54import com.android.dialer.voicemail.VoicemailStatusHelper;
55import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
56import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
57import com.android.dialerbind.ObjectFactory;
58
59import java.util.List;
60
61/**
62 * Displays a list of call log entries. To filter for a particular kind of call
63 * (all, missed or voicemails), specify it in the constructor.
64 */
65public class CallLogFragment extends Fragment
66        implements CallLogQueryHandler.Listener, CallLogAdapter.OnReportButtonClickListener,
67        CallLogAdapter.CallFetcher {
68    private static final String TAG = "CallLogFragment";
69
70    private static final String REPORT_DIALOG_TAG = "report_dialog";
71
72    /**
73     * ID of the empty loader to defer other fragments.
74     */
75    private static final int EMPTY_LOADER_ID = 0;
76
77    private static final String KEY_FILTER_TYPE = "filter_type";
78    private static final String KEY_LOG_LIMIT = "log_limit";
79    private static final String KEY_DATE_LIMIT = "date_limit";
80
81    // No limit specified for the number of logs to show; use the CallLogQueryHandler's default.
82    private static final int NO_LOG_LIMIT = -1;
83    // No date-based filtering.
84    private static final int NO_DATE_LIMIT = 0;
85
86    private RecyclerView mRecyclerView;
87    private LinearLayoutManager mLayoutManager;
88    private CallLogAdapter mAdapter;
89    private CallLogQueryHandler mCallLogQueryHandler;
90    private boolean mScrollToTop;
91
92    /** Whether there is at least one voicemail source installed. */
93    private boolean mVoicemailSourcesAvailable = false;
94
95    private VoicemailStatusHelper mVoicemailStatusHelper;
96    private View mStatusMessageView;
97    private View mEmptyListView;
98    private TextView mStatusMessageText;
99    private TextView mStatusMessageAction;
100    private KeyguardManager mKeyguardManager;
101
102    private boolean mEmptyLoaderRunning;
103    private boolean mCallLogFetched;
104    private boolean mVoicemailStatusFetched;
105
106    private final Handler mHandler = new Handler();
107
108    private class CustomContentObserver extends ContentObserver {
109        public CustomContentObserver() {
110            super(mHandler);
111        }
112        @Override
113        public void onChange(boolean selfChange) {
114            mRefreshDataRequired = true;
115        }
116    }
117
118    // See issue 6363009
119    private final ContentObserver mCallLogObserver = new CustomContentObserver();
120    private final ContentObserver mContactsObserver = new CustomContentObserver();
121    private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver();
122    private boolean mRefreshDataRequired = true;
123
124    // Exactly same variable is in Fragment as a package private.
125    private boolean mMenuVisible = true;
126
127    // Default to all calls.
128    private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
129
130    // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler}
131    // will be used.
132    private int mLogLimit = NO_LOG_LIMIT;
133
134    // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after
135    // the date filter are included.  If zero, no date-based filtering occurs.
136    private long mDateLimit = NO_DATE_LIMIT;
137
138    public CallLogFragment() {
139        this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT);
140    }
141
142    public CallLogFragment(int filterType) {
143        this(filterType, NO_LOG_LIMIT);
144    }
145
146    public CallLogFragment(int filterType, int logLimit) {
147        super();
148        mCallTypeFilter = filterType;
149        mLogLimit = logLimit;
150    }
151
152    /**
153     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
154     * after the specified date.
155     * @param filterType type of calls to include.
156     * @param dateLimit limits results to calls occurring on or after the specified date.
157     */
158    public CallLogFragment(int filterType, long dateLimit) {
159        this(filterType, NO_LOG_LIMIT, dateLimit);
160    }
161
162    /**
163     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
164     * after the specified date.  Also provides a means to limit the number of results returned.
165     * @param filterType type of calls to include.
166     * @param logLimit limits the number of results to return.
167     * @param dateLimit limits results to calls occurring on or after the specified date.
168     */
169    public CallLogFragment(int filterType, int logLimit, long dateLimit) {
170        this(filterType, logLimit);
171        mDateLimit = dateLimit;
172    }
173
174    @Override
175    public void onCreate(Bundle state) {
176        super.onCreate(state);
177        if (state != null) {
178            mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
179            mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
180            mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
181        }
182
183        final Activity activity = getActivity();
184        final ContentResolver resolver = activity.getContentResolver();
185        String currentCountryIso = GeoUtil.getCurrentCountryIso(activity);
186        mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit);
187        mKeyguardManager =
188                (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
189        resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver);
190        resolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true,
191                mContactsObserver);
192        resolver.registerContentObserver(Status.CONTENT_URI, true, mVoicemailStatusObserver);
193        setHasOptionsMenu(true);
194        fetchCalls();
195    }
196
197    /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
198    @Override
199    public boolean onCallsFetched(Cursor cursor) {
200        if (getActivity() == null || getActivity().isFinishing()) {
201            // Return false; we did not take ownership of the cursor
202            return false;
203        }
204        mAdapter.setLoading(false);
205        mAdapter.changeCursor(cursor);
206        // This will update the state of the "Clear call log" menu item.
207        getActivity().invalidateOptionsMenu();
208
209        boolean showListView = cursor.getCount() > 0;
210        mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
211        mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
212
213        if (mScrollToTop) {
214            // The smooth-scroll animation happens over a fixed time period.
215            // As a result, if it scrolls through a large portion of the list,
216            // each frame will jump so far from the previous one that the user
217            // will not experience the illusion of downward motion.  Instead,
218            // if we're not already near the top of the list, we instantly jump
219            // near the top, and animate from there.
220            if (mLayoutManager.findFirstVisibleItemPosition() > 5) {
221                // TODO: Jump to near the top, then begin smooth scroll.
222                mRecyclerView.smoothScrollToPosition(0);
223            }
224            // Workaround for framework issue: the smooth-scroll doesn't
225            // occur if setSelection() is called immediately before.
226            mHandler.post(new Runnable() {
227               @Override
228               public void run() {
229                   if (getActivity() == null || getActivity().isFinishing()) {
230                       return;
231                   }
232                   mRecyclerView.smoothScrollToPosition(0);
233               }
234            });
235
236            mScrollToTop = false;
237        }
238        mCallLogFetched = true;
239        destroyEmptyLoaderIfAllDataFetched();
240        return true;
241    }
242
243    /**
244     * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
245     */
246    @Override
247    public void onVoicemailStatusFetched(Cursor statusCursor) {
248        Activity activity = getActivity();
249        if (activity == null || activity.isFinishing()) {
250            return;
251        }
252        updateVoicemailStatusMessage(statusCursor);
253
254        // If there are any changes to the presence of active voicemail services, invalidate the
255        // options menu so that it will be updated.
256        boolean hasActiveVoicemailSources =
257                mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) != 0;
258        if (mVoicemailSourcesAvailable != hasActiveVoicemailSources) {
259            mVoicemailSourcesAvailable = hasActiveVoicemailSources;
260            activity.invalidateOptionsMenu();
261        }
262
263        mVoicemailStatusFetched = true;
264        destroyEmptyLoaderIfAllDataFetched();
265    }
266
267    private void destroyEmptyLoaderIfAllDataFetched() {
268        if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
269            mEmptyLoaderRunning = false;
270            getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
271        }
272    }
273
274    @Override
275    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
276        View view = inflater.inflate(R.layout.call_log_fragment, container, false);
277
278        mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
279        mRecyclerView.setHasFixedSize(true);
280        mLayoutManager = new LinearLayoutManager(getActivity());
281        mRecyclerView.setLayoutManager(mLayoutManager);
282
283        String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
284        boolean isShowingRecentsTab = mLogLimit != NO_LOG_LIMIT || mDateLimit != NO_DATE_LIMIT;
285        mAdapter = ObjectFactory.newCallLogAdapter(
286                getActivity(),
287                this,
288                new ContactInfoHelper(getActivity(), currentCountryIso),
289                isShowingRecentsTab,
290                this);
291        mRecyclerView.setAdapter(mAdapter);
292
293        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
294        mStatusMessageView = view.findViewById(R.id.voicemail_status);
295        mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
296        mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
297        return view;
298    }
299
300    @Override
301    public void onViewCreated(View view, Bundle savedInstanceState) {
302        super.onViewCreated(view, savedInstanceState);
303        mEmptyListView = view.findViewById(R.id.empty_list_view);
304
305        updateEmptyMessage(mCallTypeFilter);
306    }
307
308    /**
309     * Based on the new intent, decide whether the list should be configured
310     * to scroll up to display the first item.
311     */
312    public void configureScreenFromIntent(Intent newIntent) {
313        // Typically, when switching to the call-log we want to show the user
314        // the same section of the list that they were most recently looking
315        // at.  However, under some circumstances, we want to automatically
316        // scroll to the top of the list to present the newest call items.
317        // For example, immediately after a call is finished, we want to
318        // display information about that call.
319        mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType());
320    }
321
322    @Override
323    public void onStart() {
324        // Start the empty loader now to defer other fragments.  We destroy it when both calllog
325        // and the voicemail status are fetched.
326        getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
327                new EmptyLoader.Callback(getActivity()));
328        mEmptyLoaderRunning = true;
329        super.onStart();
330    }
331
332    @Override
333    public void onResume() {
334        super.onResume();
335        refreshData();
336    }
337
338    private void updateVoicemailStatusMessage(Cursor statusCursor) {
339        List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
340        if (messages.size() == 0) {
341            mStatusMessageView.setVisibility(View.GONE);
342        } else {
343            mStatusMessageView.setVisibility(View.VISIBLE);
344            // TODO: Change the code to show all messages. For now just pick the first message.
345            final StatusMessage message = messages.get(0);
346            if (message.showInCallLog()) {
347                mStatusMessageText.setText(message.callLogMessageId);
348            }
349            if (message.actionMessageId != -1) {
350                mStatusMessageAction.setText(message.actionMessageId);
351            }
352            if (message.actionUri != null) {
353                mStatusMessageAction.setVisibility(View.VISIBLE);
354                mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
355                    @Override
356                    public void onClick(View v) {
357                        getActivity().startActivity(
358                                new Intent(Intent.ACTION_VIEW, message.actionUri));
359                    }
360                });
361            } else {
362                mStatusMessageAction.setVisibility(View.GONE);
363            }
364        }
365    }
366
367    @Override
368    public void onPause() {
369        super.onPause();
370        mAdapter.pauseCache();
371    }
372
373    @Override
374    public void onStop() {
375        super.onStop();
376
377        updateOnTransition(false /* onEntry */);
378    }
379
380    @Override
381    public void onDestroy() {
382        super.onDestroy();
383        mAdapter.pauseCache();
384        mAdapter.changeCursor(null);
385        getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
386        getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
387        getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
388    }
389
390    @Override
391    public void onSaveInstanceState(Bundle outState) {
392        super.onSaveInstanceState(outState);
393        outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
394        outState.putInt(KEY_LOG_LIMIT, mLogLimit);
395        outState.putLong(KEY_DATE_LIMIT, mDateLimit);
396    }
397
398    @Override
399    public void fetchCalls() {
400        mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
401    }
402
403    private void updateEmptyMessage(int filterType) {
404        final int messageId;
405        switch (filterType) {
406            case Calls.MISSED_TYPE:
407                messageId = R.string.recentMissed_empty;
408                break;
409            case Calls.VOICEMAIL_TYPE:
410                messageId = R.string.recentVoicemails_empty;
411                break;
412            case CallLogQueryHandler.CALL_TYPE_ALL:
413                messageId = R.string.recentCalls_empty;
414                break;
415            default:
416                throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
417                        + filterType);
418        }
419        DialerUtils.configureEmptyListView(
420                mEmptyListView, R.drawable.empty_call_log, messageId, getResources());
421    }
422
423    CallLogAdapter getAdapter() {
424        return mAdapter;
425    }
426
427    @Override
428    public void setMenuVisibility(boolean menuVisible) {
429        super.setMenuVisibility(menuVisible);
430        if (mMenuVisible != menuVisible) {
431            mMenuVisible = menuVisible;
432            if (!menuVisible) {
433                updateOnTransition(false /* onEntry */);
434            } else if (isResumed()) {
435                refreshData();
436            }
437        }
438    }
439
440    /** Requests updates to the data to be shown. */
441    private void refreshData() {
442        // Prevent unnecessary refresh.
443        if (mRefreshDataRequired) {
444            // Mark all entries in the contact info cache as out of date, so they will be looked up
445            // again once being shown.
446            mAdapter.invalidateCache();
447            mAdapter.setLoading(true);
448
449            fetchCalls();
450            mCallLogQueryHandler.fetchVoicemailStatus();
451
452            updateOnTransition(true /* onEntry */);
453            mRefreshDataRequired = false;
454        } else {
455            // Refresh the display of the existing data to update the timestamp text descriptions.
456            mAdapter.notifyDataSetChanged();
457        }
458    }
459
460    /**
461     * Updates the call data and notification state on entering or leaving the call log tab.
462     *
463     * If we are leaving the call log tab, mark all the missed calls as read.
464     *
465     * TODO: Move to CallLogActivity
466     */
467    private void updateOnTransition(boolean onEntry) {
468        // We don't want to update any call data when keyguard is on because the user has likely not
469        // seen the new calls yet.
470        // This might be called before onCreate() and thus we need to check null explicitly.
471        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
472            // On either of the transitions we update the missed call and voicemail notifications.
473            // While exiting we additionally consume all missed calls (by marking them as read).
474            mCallLogQueryHandler.markNewCallsAsOld();
475            if (!onEntry) {
476                mCallLogQueryHandler.markMissedCallsAsRead();
477            }
478            CallLogNotificationsHelper.removeMissedCallNotifications(getActivity());
479            CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
480        }
481    }
482
483    public void onBadDataReported(String number) {
484        if (number == null) {
485            return;
486        }
487        mAdapter.invalidateCache();
488        mAdapter.notifyDataSetChanged();
489    }
490
491    public void onReportButtonClick(String number) {
492        DialogFragment df = ObjectFactory.getReportDialogFragment(number);
493        if (df != null) {
494            df.setTargetFragment(this, 0);
495            df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG);
496        }
497    }
498}
499