blob: 27142c8715bbd1e97e6b9c6f67fbf607613c9efc [file] [log] [blame]
/*
* Copyright (C) 2016 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.incallui.contactgrid;
import android.content.Context;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.telephony.PhoneNumberUtils;
import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.Chronometer;
import android.widget.ImageView;
import android.widget.Space;
import android.widget.TextView;
import android.widget.ViewAnimator;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import com.android.dialer.R;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.glidephotomanager.GlidePhotoManagerComponent;
import com.android.dialer.glidephotomanager.PhotoInfo;
import com.android.dialer.widget.BidiTextView;
import com.android.incallui.incall.protocol.ContactPhotoType;
import com.android.incallui.incall.protocol.PrimaryCallState;
import com.android.incallui.incall.protocol.PrimaryInfo;
import java.util.List;
/** Utility to manage the Contact grid */
public class ContactGridManager {
private final Context context;
private final View contactGridLayout;
// Row 0: Captain Holt ON HOLD
// Row 0: Calling...
// Row 0: [Wi-Fi icon] Calling via Starbucks Wi-Fi
// Row 0: [Wi-Fi icon] Starbucks Wi-Fi
// Row 0: Hey Jake, pick up!
private final ImageView connectionIconImageView;
private final TextView statusTextView;
// Row 1: Jake Peralta [Contact photo]
// Row 1: Walgreens
// Row 1: +1 (650) 253-0000
private final TextView contactNameTextView;
@Nullable private ImageView avatarImageView;
// Row 2: Mobile +1 (650) 253-0000
// Row 2: [HD attempting icon]/[HD icon] 00:15
// Row 2: Call ended
// Row 2: Hanging up
// Row 2: [Alert sign] Suspected spam caller
// Row 2: Your emergency callback number: +1 (650) 253-0000
private final ImageView workIconImageView;
private final ImageView hdIconImageView;
private final ImageView forwardIconImageView;
private final TextView forwardedNumberView;
private final ImageView spamIconImageView;
private final ViewAnimator bottomTextSwitcher;
private final BidiTextView bottomTextView;
private final Chronometer bottomTimerView;
private final Space topRowSpace;
private int avatarSize;
private boolean hideAvatar;
private boolean showAnonymousAvatar;
private boolean middleRowVisible = true;
private boolean isTimerStarted;
// Row in emergency call: This phone's number: +1 (650) 253-0000
private final TextView deviceNumberTextView;
private final View deviceNumberDivider;
private PrimaryInfo primaryInfo = PrimaryInfo.empty();
private PrimaryCallState primaryCallState = PrimaryCallState.empty();
private boolean isInMultiWindowMode;
public ContactGridManager(View view, @Nullable ImageView avatarImageView, int avatarSize,
boolean showAnonymousAvatar) {
context = view.getContext();
Assert.isNotNull(context);
this.avatarImageView = avatarImageView;
this.avatarSize = avatarSize;
this.showAnonymousAvatar = showAnonymousAvatar;
connectionIconImageView = view.findViewById(R.id.contactgrid_connection_icon);
statusTextView = view.findViewById(R.id.contactgrid_status_text);
contactNameTextView = view.findViewById(R.id.contactgrid_contact_name);
workIconImageView = view.findViewById(R.id.contactgrid_workIcon);
hdIconImageView = view.findViewById(R.id.contactgrid_hdIcon);
forwardIconImageView = view.findViewById(R.id.contactgrid_forwardIcon);
forwardedNumberView = view.findViewById(R.id.contactgrid_forwardNumber);
spamIconImageView = view.findViewById(R.id.contactgrid_spamIcon);
bottomTextSwitcher = view.findViewById(R.id.contactgrid_bottom_text_switcher);
bottomTextView = view.findViewById(R.id.contactgrid_bottom_text);
bottomTimerView = view.findViewById(R.id.contactgrid_bottom_timer);
topRowSpace = view.findViewById(R.id.contactgrid_top_row_space);
contactGridLayout = (View) contactNameTextView.getParent();
isTimerStarted = false;
deviceNumberTextView = view.findViewById(R.id.contactgrid_device_number_text);
deviceNumberDivider = view.findViewById(R.id.contactgrid_location_divider);
}
public void show() {
contactGridLayout.setVisibility(View.VISIBLE);
}
public void hide() {
contactGridLayout.setVisibility(View.GONE);
}
public void setAvatarHidden(boolean hide) {
if (hide != hideAvatar) {
hideAvatar = hide;
updatePrimaryNameAndPhoto();
}
}
public boolean isAvatarHidden() {
return hideAvatar;
}
public View getContainerView() {
return contactGridLayout;
}
public void setIsMiddleRowVisible(boolean isMiddleRowVisible) {
if (middleRowVisible == isMiddleRowVisible) {
return;
}
middleRowVisible = isMiddleRowVisible;
contactNameTextView.setVisibility(isMiddleRowVisible ? View.VISIBLE : View.GONE);
updateAvatarVisibility();
}
public void setPrimary(PrimaryInfo primaryInfo) {
this.primaryInfo = primaryInfo;
updatePrimaryNameAndPhoto();
updateBottomRow();
updateDeviceNumberRow();
}
public void setCallState(PrimaryCallState primaryCallState) {
this.primaryCallState = primaryCallState;
updatePrimaryNameAndPhoto();
updateBottomRow();
updateTopRow();
updateDeviceNumberRow();
}
public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
dispatchPopulateAccessibilityEvent(event, statusTextView);
dispatchPopulateAccessibilityEvent(event, contactNameTextView);
BottomRow.Info info = BottomRow.getInfo(context, primaryCallState, primaryInfo);
if (info.shouldPopulateAccessibilityEvent) {
dispatchPopulateAccessibilityEvent(event, bottomTextView);
}
}
public void setAvatarImageView(
@Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) {
this.avatarImageView = avatarImageView;
this.avatarSize = avatarSize;
this.showAnonymousAvatar = showAnonymousAvatar;
updatePrimaryNameAndPhoto();
}
public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
if (this.isInMultiWindowMode == isInMultiWindowMode) {
return;
}
this.isInMultiWindowMode = isInMultiWindowMode;
updateDeviceNumberRow();
}
private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) {
final List<CharSequence> eventText = event.getText();
int size = eventText.size();
view.dispatchPopulateAccessibilityEvent(event);
// If no text added write null to keep relative position.
if (size == eventText.size()) {
eventText.add(null);
}
}
private boolean updateAvatarVisibility() {
if (avatarImageView == null) {
return false;
}
if (!middleRowVisible) {
avatarImageView.setVisibility(View.GONE);
return false;
}
boolean hasPhoto =
(primaryInfo.photo() != null || primaryInfo.photoUri() != null)
&& primaryInfo.photoType() == ContactPhotoType.CONTACT;
if (!hasPhoto && !showAnonymousAvatar) {
avatarImageView.setVisibility(View.GONE);
return false;
}
avatarImageView.setVisibility(View.VISIBLE);
return true;
}
/**
* Updates row 0. For example:
*
* <ul>
* <li>Captain Holt ON HOLD
* <li>Calling...
* <li>[Wi-Fi icon] Calling via Starbucks Wi-Fi
* <li>[Wi-Fi icon] Starbucks Wi-Fi
* <li>Call from
* </ul>
*/
private void updateTopRow() {
TopRow.Info info = TopRow.getInfo(context, primaryCallState, primaryInfo);
if (TextUtils.isEmpty(info.label)) {
// Use INVISIBLE here to prevent the rows below this one from moving up and down.
statusTextView.setVisibility(View.INVISIBLE);
statusTextView.setText(null);
} else {
statusTextView.setText(info.label);
statusTextView.setVisibility(View.VISIBLE);
statusTextView.setSingleLine(info.labelIsSingleLine);
// Required to start the marquee
// This will send a AccessibilityEvent.TYPE_VIEW_SELECTED, but has no observable effect on
// talkback.
statusTextView.setSelected(true);
}
if (info.icon == null) {
connectionIconImageView.setVisibility(View.GONE);
topRowSpace.setVisibility(View.GONE);
} else {
connectionIconImageView.setVisibility(View.VISIBLE);
connectionIconImageView.setImageDrawable(info.icon);
if (statusTextView.getVisibility() == View.VISIBLE
&& !TextUtils.isEmpty(statusTextView.getText())) {
topRowSpace.setVisibility(View.VISIBLE);
} else {
topRowSpace.setVisibility(View.GONE);
}
}
}
/**
* Updates row 1. For example:
*
* <ul>
* <li>Jake Peralta [Contact photo]
* <li>Walgreens
* <li>+1 (650) 253-0000
* </ul>
*/
private void updatePrimaryNameAndPhoto() {
if (TextUtils.isEmpty(primaryInfo.name())) {
contactNameTextView.setText(null);
} else {
contactNameTextView.setText(
primaryInfo.nameIsNumber()
? PhoneNumberUtils.createTtsSpannable(primaryInfo.name())
: primaryInfo.name());
// Set direction of the name field
int nameDirection = View.TEXT_DIRECTION_INHERIT;
if (primaryInfo.nameIsNumber()) {
nameDirection = View.TEXT_DIRECTION_LTR;
}
contactNameTextView.setTextDirection(nameDirection);
}
if (avatarImageView != null) {
if (hideAvatar) {
avatarImageView.setVisibility(View.GONE);
} else if (avatarSize > 0 && updateAvatarVisibility()) {
loadPhotoWithGlide();
}
}
}
private void loadPhotoWithGlide() {
PhotoInfo.Builder photoInfoBuilder =
PhotoInfo.newBuilder()
.setIsBusiness(primaryInfo.photoType() == ContactPhotoType.BUSINESS)
.setIsVoicemail(primaryCallState.isVoiceMailNumber())
.setIsSpam(primaryInfo.isSpam())
.setIsConference(primaryCallState.isConference());
// Contact has a name, that is a number.
if (primaryInfo.nameIsNumber() && primaryInfo.number() != null) {
photoInfoBuilder.setName(primaryInfo.number());
} else if (primaryInfo.name() != null) {
photoInfoBuilder.setName(primaryInfo.name());
}
if (primaryInfo.number() != null) {
photoInfoBuilder.setFormattedNumber(primaryInfo.number());
}
if (primaryInfo.photoUri() != null) {
photoInfoBuilder.setPhotoUri(primaryInfo.photoUri().toString());
}
if (primaryInfo.contactInfoLookupKey() != null) {
photoInfoBuilder.setLookupUri(primaryInfo.contactInfoLookupKey());
}
GlidePhotoManagerComponent.get(context)
.glidePhotoManager()
.loadContactPhoto(avatarImageView, photoInfoBuilder.build());
}
/**
* Updates row 2. For example:
*
* <ul>
* <li>Mobile +1 (650) 253-0000
* <li>[HD attempting icon]/[HD icon] 00:15
* <li>Call ended
* <li>Hanging up
* </ul>
*/
private void updateBottomRow() {
BottomRow.Info info = BottomRow.getInfo(context, primaryCallState, primaryInfo);
bottomTextView.setText(info.label);
bottomTextView.setAllCaps(info.isSpamIconVisible);
workIconImageView.setVisibility(info.isWorkIconVisible ? View.VISIBLE : View.GONE);
if (hdIconImageView.getVisibility() == View.GONE) {
if (info.isHdAttemptingIconVisible) {
hdIconImageView.setImageResource(R.drawable.asd_hd_icon);
hdIconImageView.setVisibility(View.VISIBLE);
hdIconImageView.setActivated(false);
Drawable drawableCurrent = hdIconImageView.getDrawable().getCurrent();
if (drawableCurrent instanceof Animatable && !((Animatable) drawableCurrent).isRunning()) {
((Animatable) drawableCurrent).start();
}
} else if (info.isHdIconVisible) {
hdIconImageView.setImageResource(R.drawable.asd_hd_icon);
hdIconImageView.setVisibility(View.VISIBLE);
hdIconImageView.setActivated(true);
}
} else if (info.isHdIconVisible) {
hdIconImageView.setActivated(true);
} else if (!info.isHdAttemptingIconVisible) {
hdIconImageView.setVisibility(View.GONE);
}
spamIconImageView.setVisibility(info.isSpamIconVisible ? View.VISIBLE : View.GONE);
if (info.isForwardIconVisible) {
forwardIconImageView.setVisibility(View.VISIBLE);
forwardedNumberView.setVisibility(View.VISIBLE);
if (info.isTimerVisible) {
bottomTextSwitcher.setVisibility(View.VISIBLE);
if (ViewCompat.getLayoutDirection(contactGridLayout) == ViewCompat.LAYOUT_DIRECTION_LTR) {
forwardedNumberView.setText(TextUtils.concat(info.label, " • "));
} else {
forwardedNumberView.setText(TextUtils.concat(" • ", info.label));
}
} else {
bottomTextSwitcher.setVisibility(View.GONE);
forwardedNumberView.setText(info.label);
}
} else {
forwardIconImageView.setVisibility(View.GONE);
forwardedNumberView.setVisibility(View.GONE);
bottomTextSwitcher.setVisibility(View.VISIBLE);
}
if (info.isTimerVisible) {
bottomTextSwitcher.setDisplayedChild(1);
bottomTimerView.setBase(
primaryCallState.connectTimeMillis()
- System.currentTimeMillis()
+ SystemClock.elapsedRealtime());
if (!isTimerStarted) {
LogUtil.i(
"ContactGridManager.updateBottomRow",
"starting timer with base: %d",
bottomTimerView.getBase());
bottomTimerView.start();
isTimerStarted = true;
}
} else {
bottomTextSwitcher.setDisplayedChild(0);
bottomTimerView.stop();
isTimerStarted = false;
}
}
private void updateDeviceNumberRow() {
// It might not be available, e.g. in video call.
if (deviceNumberTextView == null) {
return;
}
if (isInMultiWindowMode || TextUtils.isEmpty(primaryCallState.callbackNumber())) {
deviceNumberTextView.setVisibility(View.GONE);
deviceNumberDivider.setVisibility(View.GONE);
return;
}
// This is used for carriers like Project Fi to show the callback number for emergency calls.
deviceNumberTextView.setText(
context.getString(
R.string.contact_grid_callback_number,
BidiFormatter.getInstance()
.unicodeWrap(primaryCallState.callbackNumber(), TextDirectionHeuristics.LTR)));
deviceNumberTextView.setVisibility(View.VISIBLE);
}
}