blob: da39d086f2b4e56bebdeca4d5fe56b1378df97b0 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
* Copyright (C) 2013 Android Open Kang Project
* Copyright (C) 2023 The LineageOS 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.callstats;
import static android.Manifest.permission.READ_CALL_LOG;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.CallLog;
import android.provider.ContactsContract;
import android.telecom.PhoneAccountHandle;
import android.text.format.DateUtils;
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.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.dialer.R;
import com.android.dialer.app.contactinfo.ExpirableCacheHeadlessFragment;
import com.android.dialer.calllogutils.FilterSpinnerHelper;
import com.android.dialer.contacts.ContactsComponent;
import com.android.dialer.phonenumbercache.ContactInfo;
import com.android.dialer.util.PermissionsUtil;
import com.android.dialer.widget.EmptyContentView;
import java.util.Map;
public class CallStatsFragment extends Fragment implements
CallStatsQueryHandler.Listener, FilterSpinnerHelper.OnFilterChangedListener,
EmptyContentView.OnEmptyViewActionButtonClickedListener,
DoubleDatePickerDialog.OnDateSetListener {
private static final String TAG = "CallStatsFragment";
private PhoneAccountHandle mAccountFilter = null;
private int mCallTypeFilter = -1;
private long mFilterFrom = -1;
private long mFilterTo = -1;
private boolean mSortByDuration = true;
private boolean mDataLoaded = false;
private RecyclerView mRecyclerView;
private EmptyContentView mEmptyListView;
private CallStatsAdapter mAdapter;
private CallStatsQueryHandler mCallStatsQueryHandler;
private TextView mSumHeaderView;
private TextView mDateFilterView;
private boolean mHasReadCallLogPermission = false;
private boolean mRefreshDataRequired = true;
private final ContentObserver mObserver = new ContentObserver(
new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
mRefreshDataRequired = true;
}
};
private final ActivityResultLauncher<String> permissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) {
// Force a refresh of the data since we were missing the permission before this.
mRefreshDataRequired = true;
requireActivity().invalidateOptionsMenu();
}
});
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
final ContentResolver cr = requireActivity().getContentResolver();
mCallStatsQueryHandler = new CallStatsQueryHandler(cr, this);
cr.registerContentObserver(CallLog.CONTENT_URI, true, mObserver);
cr.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, mObserver);
setHasOptionsMenu(true);
ExpirableCacheHeadlessFragment cacheFragment =
ExpirableCacheHeadlessFragment.attach(getChildFragmentManager());
mAdapter = new CallStatsAdapter(getActivity(),
ContactsComponent.get(requireActivity()).contactDisplayPreferences(),
cacheFragment.getRetainedCache());
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
View view = inflater.inflate(R.layout.call_stats_fragment, container, false);
mRecyclerView = view.findViewById(R.id.recycler_view);
mRecyclerView.setHasFixedSize(true);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
mRecyclerView.setLayoutManager(layoutManager);
mEmptyListView = view.findViewById(R.id.empty_list_view);
mEmptyListView.setImage(R.drawable.empty_call_log);
mEmptyListView.setActionClickedListener(this);
mSumHeaderView = view.findViewById(R.id.sum_header);
mDateFilterView = view.findViewById(R.id.date_filter);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mRecyclerView.setAdapter(mAdapter);
updateEmptyVisibilityAndMessage();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (getUserVisibleHint() && PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG)) {
inflater.inflate(R.menu.call_stats_options, menu);
final MenuItem resetItem = menu.findItem(R.id.reset_date_filter);
final MenuItem sortDurationItem = menu.findItem(R.id.sort_by_duration);
final MenuItem sortCountItem = menu.findItem(R.id.sort_by_count);
resetItem.setVisible(mFilterFrom != -1);
sortDurationItem.setVisible(!mSortByDuration);
sortCountItem.setVisible(mSortByDuration);
}
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.date_filter) {
final DoubleDatePickerDialog.Fragment fragment =
new DoubleDatePickerDialog.Fragment();
fragment.setArguments(
DoubleDatePickerDialog.Fragment.createArguments(mFilterFrom, mFilterTo));
fragment.show(getParentFragmentManager(), "filter");
} else if (itemId == R.id.reset_date_filter) {
mFilterFrom = -1;
mFilterTo = -1;
fetchCalls();
updateEmptyVisibilityAndMessage();
requireActivity().invalidateOptionsMenu();
} else if (itemId == R.id.sort_by_duration || itemId == R.id.sort_by_count) {
mSortByDuration = itemId == R.id.sort_by_duration;
mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration);
requireActivity().invalidateOptionsMenu();
}
return true;
}
@Override
public void onFilterChanged(PhoneAccountHandle account, int callType) {
if (account != mAccountFilter) {
mAccountFilter = account;
fetchCalls();
}
if (callType != mCallTypeFilter) {
mCallTypeFilter = callType;
mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration);
if (mDataLoaded) {
updateHeader();
updateEmptyVisibilityAndMessage();
}
}
}
@Override
public void onEmptyViewActionButtonClicked() {
if (!PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG)) {
permissionLauncher.launch(READ_CALL_LOG);
}
}
@Override
public void onDateSet(long from, long to) {
mFilterFrom = from;
mFilterTo = to;
requireActivity().invalidateOptionsMenu();
fetchCalls();
updateEmptyVisibilityAndMessage();
}
/**
* Called by the CallStatsQueryHandler when the list of calls has been
* fetched or updated.
*/
@Override
public void onCallsFetched(Map<ContactInfo, CallStatsDetails> calls) {
if (getActivity() == null || getActivity().isFinishing()) {
return;
}
mDataLoaded = true;
mAdapter.updateData(calls, mFilterFrom, mFilterTo);
mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration);
updateHeader();
updateEmptyVisibilityAndMessage();
}
@Override
public void onResume() {
super.onResume();
final boolean hasReadCallLogPermission =
PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG);
if (!mHasReadCallLogPermission && hasReadCallLogPermission) {
// We didn't have the permission before, and now we do. Force a refresh of the call log.
// Note that this code path always happens on a fresh start, but mRefreshDataRequired
// is already true in that case anyway.
mRefreshDataRequired = true;
mDataLoaded = false;
updateEmptyVisibilityAndMessage();
getActivity().invalidateOptionsMenu();
}
mHasReadCallLogPermission = hasReadCallLogPermission;
refreshData();
mAdapter.startCache();
}
@Override
public void onPause() {
super.onPause();
mAdapter.pauseCache();
}
@Override
public void onDestroy() {
super.onDestroy();
mAdapter.pauseCache();
getActivity().getContentResolver().unregisterContentObserver(mObserver);
}
private void fetchCalls() {
mCallStatsQueryHandler.fetchCalls(mFilterFrom, mFilterTo, mAccountFilter);
}
private void updateHeader() {
final String callCount = mAdapter.getTotalCallCountString();
final String duration = mAdapter.getFullDurationString(false);
if (duration != null) {
mSumHeaderView.setText(getString(R.string.call_stats_header_total, callCount, duration));
} else {
mSumHeaderView.setText(getString(R.string.call_stats_header_total_callsonly, callCount));
}
mSumHeaderView.setVisibility(isListEmpty() ? View.GONE : View.VISIBLE);
if (mFilterFrom == -1) {
mDateFilterView.setVisibility(View.GONE);
} else {
mDateFilterView.setText(
DateUtils.formatDateRange(getActivity(), mFilterFrom, mFilterTo, 0));
mDateFilterView.setVisibility(View.VISIBLE);
}
}
/** 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();
fetchCalls();
mRefreshDataRequired = false;
}
}
private boolean isListEmpty() {
return mDataLoaded && mAdapter.getItemCount() == 0;
}
private void updateEmptyVisibilityAndMessage() {
final Context context = getActivity();
if (context == null) {
return;
}
boolean showListView = !isListEmpty();
if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) {
mEmptyListView.setDescription(R.string.permission_no_calllog);
mEmptyListView.setActionLabel(R.string.permission_single_turn_on);
showListView = false;
} else if (mFilterFrom > 0 || mFilterTo > 0) {
mEmptyListView.setDescription(R.string.recent_calls_no_items_in_range);
mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
} else {
mEmptyListView.setDescription(R.string.call_log_all_empty);
mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
}
mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
}
}