blob: 462f817373fe41ed8c03ca3153f0f9ef7aa2c9ca [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source 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.speeddial;
import static android.app.Activity.RESULT_OK;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.android.dialer.R;
import com.android.dialer.callintent.CallInitiationType;
import com.android.dialer.callintent.CallIntentBuilder;
import com.android.dialer.common.Assert;
import com.android.dialer.common.FragmentUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DefaultFutureCallback;
import com.android.dialer.common.concurrent.DialerExecutorComponent;
import com.android.dialer.common.concurrent.SupportUiListener;
import com.android.dialer.common.concurrent.ThreadUtil;
import com.android.dialer.historyitemactions.DividerModule;
import com.android.dialer.historyitemactions.HistoryItemActionBottomSheet;
import com.android.dialer.historyitemactions.HistoryItemActionModule;
import com.android.dialer.historyitemactions.HistoryItemBottomSheetHeaderInfo;
import com.android.dialer.historyitemactions.IntentModule;
import com.android.dialer.precall.PreCall;
import com.android.dialer.shortcuts.ShortcutRefresher;
import com.android.dialer.speeddial.ContextMenu.ContextMenuItemListener;
import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener;
import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener;
import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener;
import com.android.dialer.speeddial.database.SpeedDialEntry.Channel;
import com.android.dialer.speeddial.draghelper.SpeedDialItemTouchHelperCallback;
import com.android.dialer.speeddial.draghelper.SpeedDialLayoutManager;
import com.android.dialer.speeddial.loader.SpeedDialUiItem;
import com.android.dialer.speeddial.loader.UiItemLoaderComponent;
import com.android.dialer.util.IntentUtil;
import com.android.dialer.util.PermissionsUtil;
import com.android.dialer.widget.EmptyContentView;
import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Fragment for displaying:
*
* <ul>
* <li>Favorite/Starred contacts
* <li>Suggested contacts
* </ul>
*
* <p>Suggested contacts built from {@link android.provider.ContactsContract#STREQUENT_PHONE_ONLY}.
*/
public class SpeedDialFragment extends Fragment {
/**
* Listen to broadcast events about permissions in order to be notified if the READ_CONTACTS
* permission is granted via the UI in another fragment.
*/
private final BroadcastReceiver readContactsPermissionGrantedReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
loadContacts();
}
};
/** Listen for changes to the strequents content observer. */
private final ContentObserver strequentsContentObserver =
new ContentObserver(ThreadUtil.getUiThreadHandler()) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
loadContacts();
}
};
private final SpeedDialHeaderListener headerListener = new SpeedDialFragmentHeaderListener();
private final SpeedDialSuggestedListener suggestedListener = new SpeedDialSuggestedListener();
private SpeedDialAdapter adapter;
private SupportUiListener<ImmutableList<SpeedDialUiItem>> speedDialLoaderListener;
private SpeedDialFavoritesListener favoritesListener;
private EmptyContentView emptyContentView;
/**
* We update the UI every time the fragment is resumed. This boolean suppresses that functionality
* once per onResume call.
*/
private boolean updateSpeedDialItemsOnResume = true;
private final ActivityResultLauncher<String[]> contactPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),
grantResults -> {
if (grantResults.size() >= 1 && grantResults.values().iterator().next()) {
PermissionsUtil.notifyPermissionGranted(getContext(),
Manifest.permission.READ_CONTACTS);
loadContacts();
}
});
private final ActivityResultLauncher<Intent> addFavoriteLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
Intent data = result.getData();
if (result.getResultCode() == RESULT_OK && data != null && data.getData() != null) {
updateSpeedDialItemsOnResume = false;
speedDialLoaderListener.listen(
getContext(),
UiItemLoaderComponent.get(requireContext())
.speedDialUiItemMutator()
.starContact(data.getData()),
this::onSpeedDialUiItemListLoaded,
throwable -> {
throw new RuntimeException(throwable);
});
}
});
public static SpeedDialFragment newInstance() {
return new SpeedDialFragment();
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
LogUtil.enterBlock("SpeedDialFragment.onCreateView");
View rootLayout = inflater.inflate(R.layout.fragment_speed_dial, container, false);
emptyContentView = rootLayout.findViewById(R.id.speed_dial_empty_content_view);
emptyContentView.setImage(R.drawable.empty_speed_dial);
speedDialLoaderListener =
DialerExecutorComponent.get(getContext())
.createUiListener(getChildFragmentManager(), "speed_dial_loader_listener");
// Setup our RecyclerView
SpeedDialLayoutManager layoutManager =
new SpeedDialLayoutManager(getContext(), 3 /* spanCount */);
favoritesListener =
new SpeedDialFavoritesListener(
getActivity(),
getChildFragmentManager(),
layoutManager,
new UpdateSpeedDialAdapterListener(),
speedDialLoaderListener);
adapter =
new SpeedDialAdapter(
getContext(),
favoritesListener,
suggestedListener,
headerListener,
FragmentUtils.getParentUnsafe(this, HostInterface.class));
layoutManager.setSpanSizeLookup(adapter.getSpanSizeLookup());
RecyclerView recyclerView = rootLayout.findViewById(R.id.speed_dial_recycler_view);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
// Setup drag and drop touch helper
ItemTouchHelper.Callback callback = new SpeedDialItemTouchHelperCallback(getContext(), adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(recyclerView);
adapter.setItemTouchHelper(touchHelper);
return rootLayout;
}
@Override
public void onResume() {
super.onResume();
loadContacts();
}
@Override
public void onPause() {
super.onPause();
favoritesListener.hideMenu();
suggestedListener.onPause();
onHidden();
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (hidden) {
onHidden();
} else {
loadContacts();
}
}
private void onHidden() {
if (!PermissionsUtil.hasContactsReadPermissions(getContext())) {
return;
}
Futures.addCallback(
DialerExecutorComponent.get(getContext())
.backgroundExecutor()
.submit(
() -> {
UiItemLoaderComponent.get(getContext())
.speedDialUiItemMutator()
.updatePinnedPosition(adapter.getSpeedDialUiItems());
return null;
}),
new DefaultFutureCallback<>(),
DialerExecutorComponent.get(getContext()).backgroundExecutor());
ShortcutRefresher.refresh(
getContext(),
ShortcutRefresher.speedDialUiItemsToContactEntries(adapter.getSpeedDialUiItems()));
}
private void loadContacts() {
if (!updateSpeedDialItemsOnResume) {
updateSpeedDialItemsOnResume = true;
return;
}
if (showContactsPermissionEmptyContentView()) {
return;
}
speedDialLoaderListener.listen(
getContext(),
UiItemLoaderComponent.get(getContext()).speedDialUiItemMutator().loadSpeedDialUiItems(),
this::onSpeedDialUiItemListLoaded,
throwable -> {
throw new RuntimeException(throwable);
});
}
private void onSpeedDialUiItemListLoaded(ImmutableList<SpeedDialUiItem> speedDialUiItems) {
LogUtil.enterBlock("SpeedDialFragment.onSpeedDialUiItemListLoaded");
// TODO(calderwoodra): Use DiffUtil to properly update and animate the change
adapter.setSpeedDialUiItems(speedDialUiItems);
adapter.notifyDataSetChanged();
maybeShowNoContactsEmptyContentView();
if (getActivity() != null) {
FragmentUtils.getParentUnsafe(this, HostInterface.class)
.setHasFrequents(adapter.hasFrequents());
}
}
/** Returns true if the empty content view was shown. */
private boolean showContactsPermissionEmptyContentView() {
if (PermissionsUtil.hasContactsReadPermissions(getContext())) {
emptyContentView.setVisibility(View.GONE);
return false;
}
emptyContentView.setVisibility(View.VISIBLE);
emptyContentView.setActionLabel(R.string.speed_dial_turn_on_contacts_permission);
emptyContentView.setDescription(R.string.speed_dial_contacts_permission_description);
emptyContentView.setActionClickedListener(
new SpeedDialContactPermissionEmptyViewListener(getContext(), this,
contactPermissionLauncher));
return true;
}
private void maybeShowNoContactsEmptyContentView() {
if (adapter.getItemCount() != 0) {
emptyContentView.setVisibility(View.GONE);
return;
}
emptyContentView.setVisibility(View.VISIBLE);
emptyContentView.setActionLabel(R.string.speed_dial_no_contacts_action_text);
emptyContentView.setDescription(R.string.speed_dial_no_contacts_description);
emptyContentView.setActionClickedListener(
new SpeedDialNoContactsEmptyViewListener(addFavoriteLauncher));
}
@Override
public void onStart() {
super.onStart();
PermissionsUtil.registerPermissionReceiver(
getActivity(), readContactsPermissionGrantedReceiver, Manifest.permission.READ_CONTACTS);
if (PermissionsUtil.hasContactsReadPermissions(getContext())) {
getContext()
.getContentResolver()
.registerContentObserver(Contacts.CONTENT_STREQUENT_URI, true, strequentsContentObserver);
}
}
@Override
public void onStop() {
super.onStop();
PermissionsUtil.unregisterPermissionReceiver(
getContext(), readContactsPermissionGrantedReceiver);
getContext().getContentResolver().unregisterContentObserver(strequentsContentObserver);
}
private class SpeedDialFragmentHeaderListener implements SpeedDialHeaderListener {
@Override
public void onAddFavoriteClicked() {
Intent intent = new Intent(Intent.ACTION_PICK, Phone.CONTENT_URI);
addFavoriteLauncher.launch(intent);
}
}
private static final class SpeedDialFavoritesListener implements FavoriteContactsListener {
private final FragmentActivity activity;
private final FragmentManager childFragmentManager;
private final SpeedDialLayoutManager layoutManager;
private final UpdateSpeedDialAdapterListener updateAdapterListener;
private final SupportUiListener<ImmutableList<SpeedDialUiItem>> speedDialLoaderListener;
private final SpeedDialContextMenuItemListener speedDialContextMenuItemListener =
new SpeedDialContextMenuItemListener();
private ContextMenu contextMenu;
SpeedDialFavoritesListener(
FragmentActivity activity,
FragmentManager childFragmentManager,
SpeedDialLayoutManager layoutManager,
UpdateSpeedDialAdapterListener updateAdapterListener,
SupportUiListener<ImmutableList<SpeedDialUiItem>> speedDialLoaderListener) {
this.activity = activity;
this.childFragmentManager = childFragmentManager;
this.layoutManager = layoutManager;
this.updateAdapterListener = updateAdapterListener;
this.speedDialLoaderListener = speedDialLoaderListener;
}
@Override
public void onAmbiguousContactClicked(SpeedDialUiItem speedDialUiItem) {
// If there is only one channel, skip the menu and place a call directly
if (speedDialUiItem.channels().size() == 1) {
onClick(speedDialUiItem.channels().get(0));
return;
}
DisambigDialog.show(speedDialUiItem, childFragmentManager);
}
@Override
public void onClick(Channel channel) {
PreCall.start(
activity,
new CallIntentBuilder(channel.number(), CallInitiationType.Type.SPEED_DIAL)
.setAllowAssistedDial(true)
.setIsVideoCall(channel.isVideoTechnology()));
}
@Override
public void showContextMenu(View view, SpeedDialUiItem speedDialUiItem) {
layoutManager.setScrollEnabled(false);
contextMenu =
ContextMenu.show(activity, view, speedDialContextMenuItemListener, speedDialUiItem);
}
@Override
public void onTouchFinished(boolean closeContextMenu) {
layoutManager.setScrollEnabled(true);
if (closeContextMenu) {
contextMenu.hide();
contextMenu = null;
}
}
@Override
public void onRequestRemove(SpeedDialUiItem speedDialUiItem) {
speedDialContextMenuItemListener.removeFavoriteContact(speedDialUiItem);
}
void hideMenu() {
if (contextMenu != null) {
contextMenu.hide();
contextMenu = null;
}
}
public SpeedDialContextMenuItemListener getSpeedDialContextMenuItemListener() {
return speedDialContextMenuItemListener;
}
class SpeedDialContextMenuItemListener implements ContextMenuItemListener {
@Override
public void placeCall(Channel channel) {
PreCall.start(
activity,
new CallIntentBuilder(channel.number(), CallInitiationType.Type.SPEED_DIAL)
.setAllowAssistedDial(true)
.setIsVideoCall(channel.isVideoTechnology()));
}
@Override
public void openSmsConversation(String number) {
activity.startActivity(IntentUtil.getSendSmsIntent(number));
}
@Override
public void removeFavoriteContact(SpeedDialUiItem speedDialUiItem) {
speedDialLoaderListener.listen(
activity,
UiItemLoaderComponent.get(activity)
.speedDialUiItemMutator()
.removeSpeedDialUiItem(speedDialUiItem),
updateAdapterListener::updateAdapter,
throwable -> {
throw new RuntimeException(throwable);
});
}
@Override
public void openContactInfo(SpeedDialUiItem speedDialUiItem) {
activity.startActivity(
new Intent(
Intent.ACTION_VIEW,
Uri.withAppendedPath(
Contacts.CONTENT_URI, String.valueOf(speedDialUiItem.contactId()))));
}
}
}
private final class SpeedDialSuggestedListener implements SuggestedContactsListener {
private HistoryItemActionBottomSheet bottomSheet;
@Override
public void onOverFlowMenuClicked(
SpeedDialUiItem speedDialUiItem, HistoryItemBottomSheetHeaderInfo headerInfo) {
List<HistoryItemActionModule> modules = new ArrayList<>();
Channel defaultChannel = speedDialUiItem.defaultChannel();
// Add voice call module
Channel voiceChannel = speedDialUiItem.getDefaultVoiceChannel();
if (voiceChannel != null) {
modules.add(
IntentModule.newCallModule(
getContext(),
new CallIntentBuilder(voiceChannel.number(), CallInitiationType.Type.SPEED_DIAL)
.setAllowAssistedDial(true)));
}
// Add video if we can determine the correct channel
Channel videoChannel = speedDialUiItem.getDefaultVideoChannel();
if (videoChannel != null) {
modules.add(
IntentModule.newCallModule(
getContext(),
new CallIntentBuilder(videoChannel.number(), CallInitiationType.Type.SPEED_DIAL)
.setIsVideoCall(true)
.setAllowAssistedDial(true)));
}
// Add sms module
if (!TextUtils.isEmpty(defaultChannel.number())) {
modules.add(
IntentModule.newModuleForSendingTextMessage(getContext(), defaultChannel.number()));
}
modules.add(new DividerModule());
modules.add(new StarContactModule(speedDialUiItem));
// TODO(calderwoodra): remove from strequent module
// Contact info module
modules.add(
new ContactInfoModule(
getContext(),
new Intent(
Intent.ACTION_VIEW,
Uri.withAppendedPath(
Contacts.CONTENT_URI, String.valueOf(speedDialUiItem.contactId()))),
R.string.contact_menu_contact_info,
R.drawable.quantum_ic_person_vd_theme_24));
bottomSheet = HistoryItemActionBottomSheet.show(getContext(), headerInfo, modules);
}
@Override
public void onRowClicked(Channel channel) {
PreCall.start(
getContext(),
new CallIntentBuilder(channel.number(), CallInitiationType.Type.SPEED_DIAL)
.setAllowAssistedDial(true)
.setIsVideoCall(channel.isVideoTechnology()));
}
private final class StarContactModule implements HistoryItemActionModule {
private final SpeedDialUiItem speedDialUiItem;
StarContactModule(SpeedDialUiItem speedDialUiItem) {
this.speedDialUiItem = speedDialUiItem;
}
@Override
public int getStringId() {
return R.string.suggested_contact_bottom_sheet_add_favorite_option;
}
@Override
public int getDrawableId() {
return R.drawable.quantum_ic_star_vd_theme_24;
}
@Override
public boolean onClick() {
speedDialLoaderListener.listen(
getContext(),
UiItemLoaderComponent.get(getContext())
.speedDialUiItemMutator()
.starContact(
Uri.withAppendedPath(
Phone.CONTENT_FILTER_URI, speedDialUiItem.defaultChannel().number())),
SpeedDialFragment.this::onSpeedDialUiItemListLoaded,
throwable -> {
throw new RuntimeException(throwable);
});
return true;
}
}
private final class ContactInfoModule extends IntentModule {
public ContactInfoModule(Context context, Intent intent, int text, int image) {
super(context, intent, text, image);
}
@Override
public boolean tintDrawable() {
return false;
}
}
public void onPause() {
if (bottomSheet != null && bottomSheet.isShowing()) {
bottomSheet.dismiss();
}
}
}
private static final class SpeedDialContactPermissionEmptyViewListener
implements OnEmptyViewActionButtonClickedListener {
private final Context context;
private final Fragment fragment;
private final ActivityResultLauncher<String[]> contactPermissionLauncher;
private SpeedDialContactPermissionEmptyViewListener(Context context, Fragment fragment,
ActivityResultLauncher<String[]>
contactPermissionLauncher) {
this.context = context;
this.fragment = fragment;
this.contactPermissionLauncher = contactPermissionLauncher;
}
@Override
public void onEmptyViewActionButtonClicked() {
String[] deniedPermissions =
PermissionsUtil.getPermissionsCurrentlyDenied(
context, PermissionsUtil.allContactsGroupPermissionsUsedInDialer);
Assert.checkArgument(deniedPermissions.length > 0);
LogUtil.i(
"OldSpeedDialFragment.onEmptyViewActionButtonClicked",
"Requesting permissions: " + Arrays.toString(deniedPermissions));
contactPermissionLauncher.launch(deniedPermissions);
}
}
private static final class SpeedDialNoContactsEmptyViewListener
implements OnEmptyViewActionButtonClickedListener {
private final ActivityResultLauncher<Intent> addFavoriteLauncher;
SpeedDialNoContactsEmptyViewListener(ActivityResultLauncher<Intent> addFavoriteLauncher) {
this.addFavoriteLauncher = addFavoriteLauncher;
}
@Override
public void onEmptyViewActionButtonClicked() {
Intent intent = new Intent(Intent.ACTION_PICK, Phone.CONTENT_URI);
addFavoriteLauncher.launch(intent);
}
}
/** Listener for when a SpeedDialUiItem is updated. */
class UpdateSpeedDialAdapterListener {
void updateAdapter(ImmutableList<SpeedDialUiItem> speedDialUiItems) {
onSpeedDialUiItemListLoaded(speedDialUiItems);
}
}
/** Interface for {@link SpeedDialFragment} to communicate with its host/parent. */
public interface HostInterface {
void setHasFrequents(boolean hasFrequents);
void dragFavorite(boolean start);
}
}