blob: 282c1f51daceb1a090f36a7b16d67daf14566d8d [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source 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.car.messenger.common;
import static com.android.car.apps.common.util.SafeLog.logw;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.android.car.apps.common.LetterTileDrawable;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg.AvatarIconSync;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ConversationNotification;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyle;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyleMessage;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg.Person;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/** Utils methods for the car-messenger-common lib. **/
public class Utils {
private static final String TAG = "CMC.Utils";
/**
* Represents the maximum length of a message substring to be used when constructing the
* message's unique handle/key.
*/
private static final int MAX_SUB_MESSAGE_LENGTH = 5;
/** The Regex format of a telephone number in a BluetoothMapClient contact URI. **/
private static final String MAP_CLIENT_URI_REGEX = "tel:(.+)";
/** The starting substring index for a string formatted with the MAP_CLIENT_URI_REGEX above. **/
private static final int MAP_CLIENT_URI_PHONE_NUMBER_SUBSTRING_INDEX = 4;
// TODO (150711637): Reference BluetoothMapClient Extras once BluetoothMapClient is SystemApi.
protected static final String BMC_EXTRA_MESSAGE_HANDLE =
"android.bluetooth.mapmce.profile.extra.MESSAGE_HANDLE";
protected static final String BMC_EXTRA_SENDER_CONTACT_URI =
"android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_URI";
protected static final String BMC_EXTRA_SENDER_CONTACT_NAME =
"android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_NAME";
protected static final String BMC_EXTRA_MESSAGE_TIMESTAMP =
"android.bluetooth.mapmce.profile.extra.MESSAGE_TIMESTAMP";
protected static final String BMC_EXTRA_MESSAGE_READ_STATUS =
"android.bluetooth.mapmce.profile.extra.MESSAGE_READ_STATUS";
/** Gets the latest message for a {@link NotificationMsg} Conversation. **/
public static MessagingStyleMessage getLatestMessage(
ConversationNotification notification) {
MessagingStyle messagingStyle = notification.getMessagingStyle();
long latestTime = 0;
MessagingStyleMessage latestMessage = null;
for (MessagingStyleMessage message : messagingStyle.getMessagingStyleMsgList()) {
if (message.getTimestamp() > latestTime) {
latestTime = message.getTimestamp();
latestMessage = message;
}
}
return latestMessage;
}
/**
* Helper method to create a unique handle/key for this message. This is used as this Message's
* {@link MessageKey#getSubKey()}.
*/
public static String createMessageHandle(MessagingStyleMessage message) {
String textMessage = message.getTextMessage();
String subMessage = textMessage.substring(
Math.min(MAX_SUB_MESSAGE_LENGTH, textMessage.length()));
return message.getTimestamp() + "/" + message.getSender().getName() + "/" + subMessage;
}
/**
* Ensure the {@link ConversationNotification} object has all the required fields.
*
* @param isShallowCheck should be {@code true} if the caller only wants to verify the
* notification and its {@link MessagingStyle} is valid, without checking
* all of the notification's {@link MessagingStyleMessage}s.
**/
public static boolean isValidConversationNotification(ConversationNotification notification,
boolean isShallowCheck) {
if (notification == null) {
logw(TAG, "ConversationNotification is null");
return false;
} else if (!notification.hasMessagingStyle()) {
logw(TAG, "ConversationNotification is missing required field: messagingStyle");
return false;
} else if (notification.getMessagingAppDisplayName() == null) {
logw(TAG, "ConversationNotification is missing required field: appDisplayName");
return false;
} else if (notification.getMessagingAppPackageName() == null) {
logw(TAG, "ConversationNotification is missing required field: appPackageName");
return false;
}
return isValidMessagingStyle(notification.getMessagingStyle(), isShallowCheck);
}
/**
* Ensure the {@link MessagingStyle} object has all the required fields.
**/
private static boolean isValidMessagingStyle(MessagingStyle messagingStyle,
boolean isShallowCheck) {
if (messagingStyle == null) {
logw(TAG, "MessagingStyle is null");
return false;
} else if (messagingStyle.getConvoTitle() == null) {
logw(TAG, "MessagingStyle is missing required field: convoTitle");
return false;
} else if (messagingStyle.getUserDisplayName() == null) {
logw(TAG, "MessagingStyle is missing required field: userDisplayName");
return false;
} else if (messagingStyle.getMessagingStyleMsgCount() == 0) {
logw(TAG, "MessagingStyle is missing required field: messagingStyleMsg");
return false;
}
if (!isShallowCheck) {
for (MessagingStyleMessage message : messagingStyle.getMessagingStyleMsgList()) {
if (!isValidMessagingStyleMessage(message)) {
return false;
}
}
}
return true;
}
/**
* Ensure the {@link MessagingStyleMessage} object has all the required fields.
**/
public static boolean isValidMessagingStyleMessage(MessagingStyleMessage message) {
if (message == null) {
logw(TAG, "MessagingStyleMessage is null");
return false;
} else if (message.getTextMessage() == null) {
logw(TAG, "MessagingStyleMessage is missing required field: textMessage");
return false;
} else if (!message.hasSender()) {
logw(TAG, "MessagingStyleMessage is missing required field: sender");
return false;
}
return isValidSender(message.getSender());
}
/**
* Ensure the {@link Person} object has all the required fields.
**/
public static boolean isValidSender(Person person) {
if (person.getName() == null) {
logw(TAG, "Person is missing required field: name");
return false;
}
return true;
}
/**
* Ensure the {@link AvatarIconSync} object has all the required fields.
**/
public static boolean isValidAvatarIconSync(AvatarIconSync iconSync) {
if (iconSync == null) {
logw(TAG, "AvatarIconSync is null");
return false;
} else if (iconSync.getMessagingAppPackageName() == null) {
logw(TAG, "AvatarIconSync is missing required field: appPackageName");
return false;
} else if (iconSync.getPerson().getName() == null) {
logw(TAG, "AvatarIconSync is missing required field: Person's name");
return false;
} else if (iconSync.getPerson().getAvatar() == null) {
logw(TAG, "AvatarIconSync is missing required field: Person's avatar");
return false;
}
return true;
}
/**
* Ensure the BluetoothMapClient intent has all the required fields.
**/
public static boolean isValidMapClientIntent(Intent intent) {
if (intent == null) {
logw(TAG, "BluetoothMapClient intent is null");
return false;
} else if (intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) == null) {
logw(TAG, "BluetoothMapClient intent is missing required field: device");
return false;
} else if (intent.getStringExtra(BMC_EXTRA_MESSAGE_HANDLE) == null) {
logw(TAG, "BluetoothMapClient intent is missing required field: senderName");
return false;
} else if (intent.getStringExtra(BMC_EXTRA_SENDER_CONTACT_NAME) == null) {
logw(TAG, "BluetoothMapClient intent is missing required field: handle");
return false;
} else if (intent.getStringExtra(android.content.Intent.EXTRA_TEXT) == null) {
logw(TAG, "BluetoothMapClient intent is missing required field: messageText");
return false;
}
return true;
}
/**
* Creates a Letter Tile Icon that will display the given initials. If the initials are null,
* then an avatar anonymous icon will be drawn.
**/
public static Bitmap createLetterTile(Context context, @Nullable String initials,
String identifier, int avatarSize, float cornerRadiusPercent) {
// TODO(b/135446418): use TelecomUtils once car-telephony-common supports bp.
LetterTileDrawable letterTileDrawable = createLetterTileDrawable(context, initials,
identifier);
RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(
context.getResources(), letterTileDrawable.toBitmap(avatarSize));
return createFromRoundedBitmapDrawable(roundedBitmapDrawable, avatarSize,
cornerRadiusPercent);
}
/** Creates an Icon based on the given roundedBitmapDrawable. **/
private static Bitmap createFromRoundedBitmapDrawable(
RoundedBitmapDrawable roundedBitmapDrawable, int avatarSize,
float cornerRadiusPercent) {
// TODO(b/135446418): use TelecomUtils once car-telephony-common supports bp.
float radius = avatarSize * cornerRadiusPercent;
roundedBitmapDrawable.setCornerRadius(radius);
final Bitmap result = Bitmap.createBitmap(avatarSize, avatarSize,
Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(result);
roundedBitmapDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
roundedBitmapDrawable.draw(canvas);
return roundedBitmapDrawable.getBitmap();
}
/**
* Create a {@link LetterTileDrawable} for the given initials.
*
* @param initials is the letters that will be drawn on the canvas. If it is null, then an
* avatar anonymous icon will be drawn
* @param identifier will decide the color for the drawable. If null, a default color will be
* used.
*/
private static LetterTileDrawable createLetterTileDrawable(
Context context,
@Nullable String initials,
@Nullable String identifier) {
// TODO(b/135446418): use TelecomUtils once car-telephony-common supports bp.
int numberOfLetter = context.getResources().getInteger(
R.integer.config_number_of_letters_shown_for_avatar);
String letters = initials != null
? initials.substring(0, Math.min(initials.length(), numberOfLetter)) : null;
LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources(),
letters, identifier);
return letterTileDrawable;
}
/** Returns whether the BluetoothMapClient intent represents a group conversation. **/
public static boolean isGroupConversation(Intent intent) {
return (intent.getStringArrayExtra(Intent.EXTRA_CC) != null
&& intent.getStringArrayExtra(Intent.EXTRA_CC).length > 0);
}
/**
* Returns the initials based on the name and nameAlt.
*
* @param name should be the display name of a contact.
* @param nameAlt should be alternative display name of a contact.
*/
public static String getInitials(String name, String nameAlt) {
// TODO(b/135446418): use TelecomUtils once car-telephony-common supports bp.
StringBuilder initials = new StringBuilder();
if (!TextUtils.isEmpty(name) && Character.isLetter(name.charAt(0))) {
initials.append(Character.toUpperCase(name.charAt(0)));
}
if (!TextUtils.isEmpty(nameAlt)
&& !TextUtils.equals(name, nameAlt)
&& Character.isLetter(nameAlt.charAt(0))) {
initials.append(Character.toUpperCase(nameAlt.charAt(0)));
}
return initials.toString();
}
/** Returns the list of sender uri for a BluetoothMapClient intent. **/
public static String getSenderUri(Intent intent) {
return intent.getStringExtra(BMC_EXTRA_SENDER_CONTACT_URI);
}
/** Returns the sender name for a BluetoothMapClient intent. **/
public static String getSenderName(Intent intent) {
return intent.getStringExtra(BMC_EXTRA_SENDER_CONTACT_NAME);
}
/** Returns the list of recipient uris for a BluetoothMapClient intent. **/
public static List<String> getInclusiveRecipientsUrisList(Intent intent) {
List<String> ccUris = new ArrayList<>();
ccUris.add(getSenderUri(intent));
if (isGroupConversation(intent)) {
ccUris.addAll(Arrays.asList(intent.getStringArrayExtra(Intent.EXTRA_CC)));
Collections.sort(ccUris);
}
return ccUris;
}
/**
* Extracts the phone number from the BluetoothMapClient contact Uri.
**/
@Nullable
public static String getPhoneNumberFromMapClient(@Nullable String senderContactUri) {
if (senderContactUri == null || !senderContactUri.matches(MAP_CLIENT_URI_REGEX)) {
return null;
}
return senderContactUri.substring(MAP_CLIENT_URI_PHONE_NUMBER_SUBSTRING_INDEX);
}
/** Comparator that sorts names alphabetically first, then phone numbers numerically. **/
public static final Comparator<String> ALPHA_THEN_NUMERIC_COMPARATOR =
new Comparator<String>() {
private boolean isPhoneNumber(String input) {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
try {
Phonenumber.PhoneNumber phoneNumber = util.parse(input, /* defaultRegion */
null);
return util.isValidNumber(phoneNumber);
} catch (NumberParseException e) {
return false;
}
}
private boolean isOfSameType(String o1, String o2) {
boolean isO1PhoneNumber = isPhoneNumber(o1);
boolean isO2PhoneNumber = isPhoneNumber(o2);
return isO1PhoneNumber == isO2PhoneNumber;
}
@Override
public int compare(String o1, String o2) {
// if both are names, sort based on names.
// if both are number, sort numerically.
// if one is phone number and the other is a name, give name precedence.
if (!isOfSameType(o1, o2)) {
return isPhoneNumber(o1) ? 1 : -1;
} else {
return o1.compareTo(o2);
}
}
};
}