Home | History | Annotate | Download | only in calllog
      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 
     17 package com.android.dialer.calllog;
     18 
     19 import android.app.Activity;
     20 import android.app.Fragment;
     21 import android.app.KeyguardManager;
     22 import android.content.ContentResolver;
     23 import android.content.Context;
     24 import android.content.pm.PackageManager;
     25 import android.database.ContentObserver;
     26 import android.database.Cursor;
     27 import android.os.Bundle;
     28 import android.os.Handler;
     29 import android.os.Message;
     30 import android.provider.CallLog;
     31 import android.provider.CallLog.Calls;
     32 import android.provider.ContactsContract;
     33 import android.support.annotation.Nullable;
     34 import android.support.v13.app.FragmentCompat;
     35 import android.support.v7.widget.LinearLayoutManager;
     36 import android.support.v7.widget.RecyclerView;
     37 import android.view.LayoutInflater;
     38 import android.view.View;
     39 import android.view.ViewGroup;
     40 
     41 import com.android.contacts.common.GeoUtil;
     42 import com.android.contacts.common.util.PermissionsUtil;
     43 import com.android.dialer.R;
     44 import com.android.dialer.list.ListsFragment;
     45 import com.android.dialer.util.EmptyLoader;
     46 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
     47 import com.android.dialer.widget.EmptyContentView;
     48 import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
     49 import com.android.dialerbind.ObjectFactory;
     50 
     51 import static android.Manifest.permission.READ_CALL_LOG;
     52 
     53 /**
     54  * Displays a list of call log entries. To filter for a particular kind of call
     55  * (all, missed or voicemails), specify it in the constructor.
     56  */
     57 public class CallLogFragment extends Fragment implements CallLogQueryHandler.Listener,
     58         CallLogAdapter.CallFetcher, OnEmptyViewActionButtonClickedListener,
     59         FragmentCompat.OnRequestPermissionsResultCallback {
     60     private static final String TAG = "CallLogFragment";
     61 
     62     /**
     63      * ID of the empty loader to defer other fragments.
     64      */
     65     private static final int EMPTY_LOADER_ID = 0;
     66 
     67     private static final String KEY_FILTER_TYPE = "filter_type";
     68     private static final String KEY_LOG_LIMIT = "log_limit";
     69     private static final String KEY_DATE_LIMIT = "date_limit";
     70     private static final String KEY_IS_CALL_LOG_ACTIVITY = "is_call_log_activity";
     71 
     72     // No limit specified for the number of logs to show; use the CallLogQueryHandler's default.
     73     private static final int NO_LOG_LIMIT = -1;
     74     // No date-based filtering.
     75     private static final int NO_DATE_LIMIT = 0;
     76 
     77     private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1;
     78 
     79     private static final int EVENT_UPDATE_DISPLAY = 1;
     80 
     81     private static final long MILLIS_IN_MINUTE = 60 * 1000;
     82 
     83     private RecyclerView mRecyclerView;
     84     private LinearLayoutManager mLayoutManager;
     85     private CallLogAdapter mAdapter;
     86     private CallLogQueryHandler mCallLogQueryHandler;
     87     private boolean mScrollToTop;
     88 
     89 
     90     private EmptyContentView mEmptyListView;
     91     private KeyguardManager mKeyguardManager;
     92 
     93     private boolean mEmptyLoaderRunning;
     94     private boolean mCallLogFetched;
     95     private boolean mVoicemailStatusFetched;
     96 
     97     private final Handler mDisplayUpdateHandler = new Handler() {
     98         @Override
     99         public void handleMessage(Message msg) {
    100             switch (msg.what) {
    101                 case EVENT_UPDATE_DISPLAY:
    102                     refreshData();
    103                     rescheduleDisplayUpdate();
    104                     break;
    105             }
    106         }
    107     };
    108 
    109     private final Handler mHandler = new Handler();
    110 
    111     protected class CustomContentObserver extends ContentObserver {
    112         public CustomContentObserver() {
    113             super(mHandler);
    114         }
    115         @Override
    116         public void onChange(boolean selfChange) {
    117             mRefreshDataRequired = true;
    118         }
    119     }
    120 
    121     // See issue 6363009
    122     private final ContentObserver mCallLogObserver = new CustomContentObserver();
    123     private final ContentObserver mContactsObserver = new CustomContentObserver();
    124     private boolean mRefreshDataRequired = true;
    125 
    126     private boolean mHasReadCallLogPermission = false;
    127 
    128     // Exactly same variable is in Fragment as a package private.
    129     private boolean mMenuVisible = true;
    130 
    131     // Default to all calls.
    132     private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
    133 
    134     // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler}
    135     // will be used.
    136     private int mLogLimit = NO_LOG_LIMIT;
    137 
    138     // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after
    139     // the date filter are included.  If zero, no date-based filtering occurs.
    140     private long mDateLimit = NO_DATE_LIMIT;
    141 
    142     /*
    143      * True if this instance of the CallLogFragment shown in the CallLogActivity.
    144      */
    145     private boolean mIsCallLogActivity = false;
    146 
    147     public interface HostInterface {
    148         public void showDialpad();
    149     }
    150 
    151     public CallLogFragment() {
    152         this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT);
    153     }
    154 
    155     public CallLogFragment(int filterType) {
    156         this(filterType, NO_LOG_LIMIT);
    157     }
    158 
    159     public CallLogFragment(int filterType, boolean isCallLogActivity) {
    160         this(filterType, NO_LOG_LIMIT);
    161         mIsCallLogActivity = isCallLogActivity;
    162     }
    163 
    164     public CallLogFragment(int filterType, int logLimit) {
    165         this(filterType, logLimit, NO_DATE_LIMIT);
    166     }
    167 
    168     /**
    169      * Creates a call log fragment, filtering to include only calls of the desired type, occurring
    170      * after the specified date.
    171      * @param filterType type of calls to include.
    172      * @param dateLimit limits results to calls occurring on or after the specified date.
    173      */
    174     public CallLogFragment(int filterType, long dateLimit) {
    175         this(filterType, NO_LOG_LIMIT, dateLimit);
    176     }
    177 
    178     /**
    179      * Creates a call log fragment, filtering to include only calls of the desired type, occurring
    180      * after the specified date.  Also provides a means to limit the number of results returned.
    181      * @param filterType type of calls to include.
    182      * @param logLimit limits the number of results to return.
    183      * @param dateLimit limits results to calls occurring on or after the specified date.
    184      */
    185     public CallLogFragment(int filterType, int logLimit, long dateLimit) {
    186         mCallTypeFilter = filterType;
    187         mLogLimit = logLimit;
    188         mDateLimit = dateLimit;
    189     }
    190 
    191     @Override
    192     public void onCreate(Bundle state) {
    193         super.onCreate(state);
    194         if (state != null) {
    195             mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
    196             mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
    197             mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
    198             mIsCallLogActivity = state.getBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity);
    199         }
    200 
    201         final Activity activity = getActivity();
    202         final ContentResolver resolver = activity.getContentResolver();
    203         String currentCountryIso = GeoUtil.getCurrentCountryIso(activity);
    204         mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit);
    205         mKeyguardManager =
    206                 (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
    207         resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver);
    208         resolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true,
    209                 mContactsObserver);
    210         setHasOptionsMenu(true);
    211     }
    212 
    213     /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
    214     @Override
    215     public boolean onCallsFetched(Cursor cursor) {
    216         if (getActivity() == null || getActivity().isFinishing()) {
    217             // Return false; we did not take ownership of the cursor
    218             return false;
    219         }
    220         mAdapter.invalidatePositions();
    221         mAdapter.setLoading(false);
    222         mAdapter.changeCursor(cursor);
    223         // This will update the state of the "Clear call log" menu item.
    224         getActivity().invalidateOptionsMenu();
    225 
    226         boolean showListView = cursor != null && cursor.getCount() > 0;
    227         mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
    228         mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
    229 
    230         if (mScrollToTop) {
    231             // The smooth-scroll animation happens over a fixed time period.
    232             // As a result, if it scrolls through a large portion of the list,
    233             // each frame will jump so far from the previous one that the user
    234             // will not experience the illusion of downward motion.  Instead,
    235             // if we're not already near the top of the list, we instantly jump
    236             // near the top, and animate from there.
    237             if (mLayoutManager.findFirstVisibleItemPosition() > 5) {
    238                 // TODO: Jump to near the top, then begin smooth scroll.
    239                 mRecyclerView.smoothScrollToPosition(0);
    240             }
    241             // Workaround for framework issue: the smooth-scroll doesn't
    242             // occur if setSelection() is called immediately before.
    243             mHandler.post(new Runnable() {
    244                @Override
    245                public void run() {
    246                    if (getActivity() == null || getActivity().isFinishing()) {
    247                        return;
    248                    }
    249                    mRecyclerView.smoothScrollToPosition(0);
    250                }
    251             });
    252 
    253             mScrollToTop = false;
    254         }
    255         mCallLogFetched = true;
    256         destroyEmptyLoaderIfAllDataFetched();
    257         return true;
    258     }
    259 
    260     /**
    261      * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
    262      */
    263     @Override
    264     public void onVoicemailStatusFetched(Cursor statusCursor) {
    265         Activity activity = getActivity();
    266         if (activity == null || activity.isFinishing()) {
    267             return;
    268         }
    269 
    270         mVoicemailStatusFetched = true;
    271         destroyEmptyLoaderIfAllDataFetched();
    272     }
    273 
    274     private void destroyEmptyLoaderIfAllDataFetched() {
    275         if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
    276             mEmptyLoaderRunning = false;
    277             getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
    278         }
    279     }
    280 
    281     @Override
    282     public void onVoicemailUnreadCountFetched(Cursor cursor) {}
    283 
    284     @Override
    285     public void onMissedCallsUnreadCountFetched(Cursor cursor) {}
    286 
    287     @Override
    288     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
    289         View view = inflater.inflate(R.layout.call_log_fragment, container, false);
    290         setupView(view, null);
    291         return view;
    292     }
    293 
    294     protected void setupView(
    295             View view, @Nullable VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
    296         mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
    297         mRecyclerView.setHasFixedSize(true);
    298         mLayoutManager = new LinearLayoutManager(getActivity());
    299         mRecyclerView.setLayoutManager(mLayoutManager);
    300         mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view);
    301         mEmptyListView.setImage(R.drawable.empty_call_log);
    302         mEmptyListView.setActionClickedListener(this);
    303 
    304         int activityType = mIsCallLogActivity ? CallLogAdapter.ACTIVITY_TYPE_CALL_LOG :
    305                 CallLogAdapter.ACTIVITY_TYPE_DIALTACTS;
    306         String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
    307         mAdapter = ObjectFactory.newCallLogAdapter(
    308                         getActivity(),
    309                         this,
    310                         new ContactInfoHelper(getActivity(), currentCountryIso),
    311                         voicemailPlaybackPresenter,
    312                         activityType);
    313         mRecyclerView.setAdapter(mAdapter);
    314         fetchCalls();
    315     }
    316 
    317     @Override
    318     public void onViewCreated(View view, Bundle savedInstanceState) {
    319         super.onViewCreated(view, savedInstanceState);
    320         updateEmptyMessage(mCallTypeFilter);
    321         mAdapter.onRestoreInstanceState(savedInstanceState);
    322     }
    323 
    324     @Override
    325     public void onStart() {
    326         // Start the empty loader now to defer other fragments.  We destroy it when both calllog
    327         // and the voicemail status are fetched.
    328         getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
    329                 new EmptyLoader.Callback(getActivity()));
    330         mEmptyLoaderRunning = true;
    331         super.onStart();
    332     }
    333 
    334     @Override
    335     public void onResume() {
    336         super.onResume();
    337         final boolean hasReadCallLogPermission =
    338                 PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG);
    339         if (!mHasReadCallLogPermission && hasReadCallLogPermission) {
    340             // We didn't have the permission before, and now we do. Force a refresh of the call log.
    341             // Note that this code path always happens on a fresh start, but mRefreshDataRequired
    342             // is already true in that case anyway.
    343             mRefreshDataRequired = true;
    344             updateEmptyMessage(mCallTypeFilter);
    345         }
    346 
    347         mHasReadCallLogPermission = hasReadCallLogPermission;
    348         refreshData();
    349         mAdapter.onResume();
    350 
    351         rescheduleDisplayUpdate();
    352     }
    353 
    354     @Override
    355     public void onPause() {
    356         cancelDisplayUpdate();
    357         mAdapter.onPause();
    358         super.onPause();
    359     }
    360 
    361     @Override
    362     public void onStop() {
    363         updateOnTransition();
    364 
    365         super.onStop();
    366     }
    367 
    368     @Override
    369     public void onDestroy() {
    370         mAdapter.changeCursor(null);
    371 
    372         getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
    373         getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
    374         super.onDestroy();
    375     }
    376 
    377     @Override
    378     public void onSaveInstanceState(Bundle outState) {
    379         super.onSaveInstanceState(outState);
    380         outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
    381         outState.putInt(KEY_LOG_LIMIT, mLogLimit);
    382         outState.putLong(KEY_DATE_LIMIT, mDateLimit);
    383         outState.putBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity);
    384 
    385         mAdapter.onSaveInstanceState(outState);
    386     }
    387 
    388     @Override
    389     public void fetchCalls() {
    390         mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
    391         if (!mIsCallLogActivity) {
    392             ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
    393         }
    394     }
    395 
    396     private void updateEmptyMessage(int filterType) {
    397         final Context context = getActivity();
    398         if (context == null) {
    399             return;
    400         }
    401 
    402         if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) {
    403             mEmptyListView.setDescription(R.string.permission_no_calllog);
    404             mEmptyListView.setActionLabel(R.string.permission_single_turn_on);
    405             return;
    406         }
    407 
    408         final int messageId;
    409         switch (filterType) {
    410             case Calls.MISSED_TYPE:
    411                 messageId = R.string.call_log_missed_empty;
    412                 break;
    413             case Calls.VOICEMAIL_TYPE:
    414                 messageId = R.string.call_log_voicemail_empty;
    415                 break;
    416             case CallLogQueryHandler.CALL_TYPE_ALL:
    417                 messageId = R.string.call_log_all_empty;
    418                 break;
    419             default:
    420                 throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
    421                         + filterType);
    422         }
    423         mEmptyListView.setDescription(messageId);
    424         if (mIsCallLogActivity) {
    425             mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
    426         } else if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) {
    427             mEmptyListView.setActionLabel(R.string.call_log_all_empty_action);
    428         }
    429     }
    430 
    431     CallLogAdapter getAdapter() {
    432         return mAdapter;
    433     }
    434 
    435     @Override
    436     public void setMenuVisibility(boolean menuVisible) {
    437         super.setMenuVisibility(menuVisible);
    438         if (mMenuVisible != menuVisible) {
    439             mMenuVisible = menuVisible;
    440             if (!menuVisible) {
    441                 updateOnTransition();
    442             } else if (isResumed()) {
    443                 refreshData();
    444             }
    445         }
    446     }
    447 
    448     /** Requests updates to the data to be shown. */
    449     private void refreshData() {
    450         // Prevent unnecessary refresh.
    451         if (mRefreshDataRequired) {
    452             // Mark all entries in the contact info cache as out of date, so they will be looked up
    453             // again once being shown.
    454             mAdapter.invalidateCache();
    455             mAdapter.setLoading(true);
    456 
    457             fetchCalls();
    458             mCallLogQueryHandler.fetchVoicemailStatus();
    459             mCallLogQueryHandler.fetchMissedCallsUnreadCount();
    460             updateOnTransition();
    461             mRefreshDataRequired = false;
    462         } else {
    463             // Refresh the display of the existing data to update the timestamp text descriptions.
    464             mAdapter.notifyDataSetChanged();
    465         }
    466     }
    467 
    468     /**
    469      * Updates the voicemail notification state.
    470      *
    471      * TODO: Move to CallLogActivity
    472      */
    473     private void updateOnTransition() {
    474         // We don't want to update any call data when keyguard is on because the user has likely not
    475         // seen the new calls yet.
    476         // This might be called before onCreate() and thus we need to check null explicitly.
    477         if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()
    478                 && mCallTypeFilter == Calls.VOICEMAIL_TYPE) {
    479             CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
    480         }
    481     }
    482 
    483     @Override
    484     public void onEmptyViewActionButtonClicked() {
    485         final Activity activity = getActivity();
    486         if (activity == null) {
    487             return;
    488         }
    489 
    490         if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) {
    491           FragmentCompat.requestPermissions(this, new String[] {READ_CALL_LOG},
    492               READ_CALL_LOG_PERMISSION_REQUEST_CODE);
    493         } else if (!mIsCallLogActivity) {
    494             // Show dialpad if we are not in the call log activity.
    495             ((HostInterface) activity).showDialpad();
    496         }
    497     }
    498 
    499     @Override
    500     public void onRequestPermissionsResult(int requestCode, String[] permissions,
    501             int[] grantResults) {
    502         if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) {
    503             if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
    504                 // Force a refresh of the data since we were missing the permission before this.
    505                 mRefreshDataRequired = true;
    506             }
    507         }
    508     }
    509 
    510     /**
    511      * Schedules an update to the relative call times (X mins ago).
    512      */
    513     private void rescheduleDisplayUpdate() {
    514         if (!mDisplayUpdateHandler.hasMessages(EVENT_UPDATE_DISPLAY)) {
    515             long time = System.currentTimeMillis();
    516             // This value allows us to change the display relatively close to when the time changes
    517             // from one minute to the next.
    518             long millisUtilNextMinute = MILLIS_IN_MINUTE - (time % MILLIS_IN_MINUTE);
    519             mDisplayUpdateHandler.sendEmptyMessageDelayed(
    520                     EVENT_UPDATE_DISPLAY, millisUtilNextMinute);
    521         }
    522     }
    523 
    524     /**
    525      * Cancels any pending update requests to update the relative call times (X mins ago).
    526      */
    527     private void cancelDisplayUpdate() {
    528         mDisplayUpdateHandler.removeMessages(EVENT_UPDATE_DISPLAY);
    529     }
    530 }
    531