Messaging: Add home categories and shared modal/input chrome

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-27 02:41:59 +09:00
co-authored by Cursor
parent a32250fe34
commit 42148360e0
77 changed files with 2823 additions and 182 deletions
@@ -23,13 +23,16 @@ import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import com.android.messaging.R;
import com.android.messaging.ui.home.HomeInputFocus;
import com.android.messaging.util.BugleActivityUtil;
import com.android.messaging.util.ImeUtil;
import com.android.messaging.util.LogUtil;
@@ -91,6 +94,13 @@ public class BugleActionBarActivity extends AppCompatActivity implements ImeUtil
BugleActivityUtil.onActivityResume(this, BugleActionBarActivity.this);
}
@Override
public boolean dispatchTouchEvent(final MotionEvent ev) {
final Window window = getWindow();
HomeInputFocus.clearIfOutside(window == null ? null : window.getDecorView(), ev);
return super.dispatchTouchEvent(ev);
}
@Override
protected void onPause() {
super.onPause();
@@ -221,6 +221,9 @@ public class ComposeMessageView extends LinearLayout
mComposeEditText.setOnEditorActionListener(this);
mComposeEditText.addTextChangedListener(this);
mComposeEditText.setOnFocusChangeListener((v, hasFocus) -> {
if (mAttachMediaButton != null) {
mAttachMediaButton.setSelected(hasFocus);
}
if (v == mComposeEditText && hasFocus) {
if (mHost != null) {
mHost.onComposeEditTextFocused();
@@ -320,6 +323,7 @@ public class ComposeMessageView extends LinearLayout
}
mAttachHost = createComposeAttachHost();
mAttachMediaButton.setOnClickListener(clickView -> toggleComposeAttachmentSheet());
mAttachMediaButton.setSelected(mComposeEditText.hasFocus());
final ImageButton emojiButton = findViewById(R.id.home_compose_emoji_button);
if (emojiButton != null) {
@@ -784,36 +788,22 @@ public class ComposeMessageView extends LinearLayout
int sendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR;
if (selfSendButtonUri != null) {
if (hasWorkingDraft && isDataLoadedForMessageSend()) {
UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null);
if (isOverriddenAvatarAGroup()) {
// If the host has overriden the avatar to show a group avatar where the
// send button sits, we have to hide the group avatar because it can be larger
// than the send button and pieces of the avatar will stick out from behind
// the send button.
UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, GONE, null);
}
hideSelfSendIcon();
mMmsIndicator.setVisibility(draftMessageData.getIsMms() ? VISIBLE : INVISIBLE);
sendWidgetMode = SEND_WIDGET_MODE_SEND_BUTTON;
} else {
mSelfSendIcon.setImageResourceUri(selfSendButtonUri);
if (isOverriddenAvatarAGroup()) {
UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, VISIBLE, null);
}
UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, null);
hideSelfSendIcon();
mMmsIndicator.setVisibility(INVISIBLE);
if (shouldShowSimSelector(mConversationDataModel.getData())) {
sendWidgetMode = SEND_WIDGET_MODE_SIM_SELECTOR;
}
}
} else {
mSelfSendIcon.setImageResourceUri(null);
hideSelfSendIcon();
if (hasWorkingDraft && isDataLoadedForMessageSend()) {
UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null);
UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, GONE, null);
mMmsIndicator.setVisibility(draftMessageData.getIsMms() ? VISIBLE : INVISIBLE);
sendWidgetMode = SEND_WIDGET_MODE_SEND_BUTTON;
} else {
UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, null);
mMmsIndicator.setVisibility(INVISIBLE);
}
}
@@ -898,10 +888,24 @@ public class ComposeMessageView extends LinearLayout
if (mVoiceButton == null || mSendColumn == null) {
return;
}
final boolean showSendColumn = sendWidgetMode == SEND_WIDGET_MODE_SEND_BUTTON
|| sendWidgetMode == SEND_WIDGET_MODE_SIM_SELECTOR;
mSendColumn.setVisibility(showSendColumn ? View.VISIBLE : View.GONE);
mVoiceButton.setVisibility(showSendColumn ? View.GONE : View.VISIBLE);
final boolean showSend = sendWidgetMode == SEND_WIDGET_MODE_SEND_BUTTON;
if (showSend) {
mSendColumn.setVisibility(VISIBLE);
UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null);
UiUtils.revealOrHideViewWithAnimation(mVoiceButton, GONE, null);
return;
}
UiUtils.revealOrHideViewWithAnimation(mVoiceButton, VISIBLE, null);
if (mSendColumn.getVisibility() == VISIBLE) {
UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, () -> {
if (mSendWidgetMode != SEND_WIDGET_MODE_SEND_BUTTON && mSendColumn != null) {
mSendColumn.setVisibility(GONE);
}
});
} else {
mSendButton.setVisibility(GONE);
mSendColumn.setVisibility(GONE);
}
}
private void setSendButtonAccessibility(final int sendWidgetMode) {
@@ -911,7 +915,6 @@ public class ComposeMessageView extends LinearLayout
// important for accessibility.
mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
mSelfSendIcon.setContentDescription(null);
mSendButton.setVisibility(View.GONE);
setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SELF_AVATAR);
break;
@@ -1078,12 +1081,13 @@ public class ComposeMessageView extends LinearLayout
mHost.notifyOfAttachmentLoadFailed();
}
private boolean isOverriddenAvatarAGroup() {
final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri();
if (overridenSelfUri == null) {
return false;
private void hideSelfSendIcon() {
if (mSelfSendIcon == null) {
return;
}
return AvatarUriUtil.TYPE_GROUP_URI.equals(AvatarUriUtil.getAvatarType(overridenSelfUri));
mSelfSendIcon.animate().cancel();
mSelfSendIcon.setVisibility(GONE);
mSelfSendIcon.setImageResourceUri(null);
}
@Override
@@ -319,7 +319,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
mScrollToDismissHandled = true;
}
}
}
}
};
private final ActivityResultLauncher<Intent> mLauncher = registerForActivityResult(
@@ -968,7 +968,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
mRecyclerView.post(mAdapter::notifyDataSetChanged);
return;
}
mAdapter.notifyDataSetChanged();
mAdapter.notifyDataSetChanged();
});
}
@@ -1219,8 +1219,8 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
mListState = mRecyclerView.getLayoutManager().onSaveInstanceState();
try {
LocalBroadcastManager.getInstance(getActivity())
.unregisterReceiver(mConversationSelfIdChangeReceiver);
LocalBroadcastManager.getInstance(getActivity())
.unregisterReceiver(mConversationSelfIdChangeReceiver);
} catch (final IllegalArgumentException ignored) {
// onResume may have aborted before the receiver was registered (camera return).
}
@@ -2421,7 +2421,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
AccessibilityUtil.getVocalizedPhoneNumber(getResources(), conversationName);
titleView.setContentDescription(vocalizedConversationName);
if (getActivity() != null) {
getActivity().setTitle(conversationName);
getActivity().setTitle(conversationName);
}
} else if (getActivity() != null) {
final String appName = getString(R.string.app_name);
@@ -32,6 +32,8 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.ui.CursorRecyclerAdapter;
import com.android.messaging.ui.home.HomeCategoryClassifier;
import com.android.messaging.ui.home.HomeCategoryStore;
import com.android.messaging.ui.home.PinnedConversationStore;
import java.util.ArrayList;
@@ -58,6 +60,7 @@ public class ConversationListAdapter
private final ConversationListItemView.HostInterface mClivHostInterface;
private final ConversationListItemData mFilterProbe = new ConversationListItemData();
private int mFilterMode = FILTER_ALL;
private String mCategoryId = HomeCategoryStore.ID_ALL;
private int[] mDisplayPositions = new int[0];
private View mPinnedStripRoot;
private boolean mPinnedHeaderVisible;
@@ -113,6 +116,46 @@ public class ConversationListAdapter
return mFilterMode;
}
public void setCategoryId(final String categoryId) {
final String next = categoryId == null ? HomeCategoryStore.ID_ALL : categoryId;
if (next.equals(mCategoryId)) {
return;
}
mCategoryId = next;
rebuildFilterIndex();
notifyDataSetChanged();
}
public String getCategoryId() {
return mCategoryId;
}
public void refreshCategoryFilter() {
rebuildFilterIndex();
notifyDataSetChanged();
}
public List<ConversationListItemData> snapshotConversations() {
final Cursor cursor = getCursor();
if (cursor == null || cursor.getCount() == 0) {
return Collections.emptyList();
}
final List<ConversationListItemData> items = new ArrayList<>(cursor.getCount());
final int saved = cursor.getPosition();
for (int i = 0; i < cursor.getCount(); i++) {
if (!cursor.moveToPosition(i)) {
continue;
}
final ConversationListItemData item = new ConversationListItemData();
item.bind(cursor);
items.add(item);
}
if (saved >= 0 && saved < cursor.getCount()) {
cursor.moveToPosition(saved);
}
return items;
}
public void notifySelectionChromeChanged() {
final int count = getItemCount();
if (count > 0) {
@@ -245,16 +288,13 @@ public class ConversationListAdapter
}
private boolean passesFilter() {
if (mFilterMode == FILTER_ALL) {
return true;
if (mFilterMode == FILTER_UNREAD && mFilterProbe.getIsRead()) {
return false;
}
if (mFilterMode == FILTER_UNREAD) {
return !mFilterProbe.getIsRead();
if (mFilterMode == FILTER_GROUPS && mFilterProbe.getParticipantCount() <= 1) {
return false;
}
if (mFilterMode == FILTER_GROUPS) {
return mFilterProbe.getParticipantCount() > 1;
}
return true;
return HomeCategoryClassifier.matches(mContext, mCategoryId, mFilterProbe);
}
private void sortPinnedFirst(final ArrayList<Integer> positions, final Cursor cursor) {
@@ -71,6 +71,11 @@ import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.ui.ContactIconView;
import com.android.messaging.ui.home.HomeCategoryAddModal;
import com.android.messaging.ui.home.HomeCategoryAssignSheet;
import com.android.messaging.ui.home.HomeCategoryClassifier;
import com.android.messaging.ui.home.HomeCategoryStore;
import com.android.messaging.ui.home.HomeModal;
import com.android.messaging.ui.home.HomeEmptyState;
import com.android.messaging.ui.SnackBarInteraction;
import com.android.messaging.ui.UIIntents;
@@ -91,6 +96,7 @@ import com.android.messaging.util.ThreadUtil;
import com.android.messaging.util.UiUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -130,6 +136,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private TextView mHomeReadAllButton;
private int mHomeSelectionCount;
private View mHomeMessagesTop;
private View mHomeCategoryStrip;
private ViewGroup mHomeCategoryAll;
private View mHomeCategoryScroll;
private LinearLayout mHomeCategoryRow;
private TextView mHomeTitle;
private View mHomeAutomationPane;
private View mHomeAutomationCreateButton;
@@ -388,6 +398,17 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
setupHomeChrome(rootView);
mHomeMessagesTop = rootView.findViewById(R.id.home_messages_top);
mHomeCategoryStrip = rootView.findViewById(R.id.home_category_strip);
mHomeCategoryAll = rootView.findViewById(R.id.home_category_all);
mHomeCategoryScroll = rootView.findViewById(R.id.home_category_scroll);
mHomeCategoryRow = rootView.findViewById(R.id.home_category_row);
final View categoryAdd = rootView.findViewById(R.id.home_category_add);
if (categoryAdd != null) {
categoryAdd.setOnClickListener(v -> HomeCategoryAddModal.show(requireContext(),
created -> selectHomeCategory(created.id)));
}
mAdapter.setCategoryId(HomeCategoryStore.getSelectedId(requireContext()));
bindHomeCategoryStrip();
mHomeTitle = rootView.findViewById(R.id.home_title);
mHomeAutomationPane = rootView.findViewById(R.id.home_automation_pane);
mHomeNavDock = rootView.findViewById(R.id.home_nav_dock);
@@ -1108,6 +1129,11 @@ public class ConversationListFragment extends Fragment implements ConversationLi
if (mHomeMessagesTop != null) {
mHomeMessagesTop.setVisibility(View.VISIBLE);
}
if (mHomeCategoryStrip != null) {
mHomeCategoryStrip.setVisibility(showMessages ? View.VISIBLE : View.GONE);
} else if (mHomeCategoryScroll != null) {
mHomeCategoryScroll.setVisibility(showMessages ? View.VISIBLE : View.GONE);
}
final CharSequence title = getString(showMessages
? R.string.home_title : R.string.home_automation_title);
if (mHomeTitle != null) {
@@ -1470,6 +1496,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
dismissHomePopover();
toggleConversationMute(data);
});
content.findViewById(R.id.popover_conversation_category).setOnClickListener(v -> {
dismissHomePopover();
showAssignHomeCategory(data);
});
content.findViewById(R.id.popover_conversation_delete).setOnClickListener(v -> {
dismissHomePopover();
confirmDeleteConversation(data);
@@ -1832,21 +1862,224 @@ public class ConversationListFragment extends Fragment implements ConversationLi
content.findViewById(R.id.popover_edit_pinned).setOnClickListener(v -> {
dismissHomePopoverAndThen(() -> setPinnedEditMode(true));
});
content.findViewById(R.id.popover_name_photo).setOnClickListener(v -> {
dismissHomePopover();
launchHomeSubScreen(() -> {
if (getActivity() instanceof ConversationListActivity) {
((ConversationListActivity) getActivity()).onActionBarSettings();
}
});
});
}
private void selectHomeChip(final int filterMode) {
mAdapter.setFilterMode(filterMode);
bindHomeCategoryStrip();
updateEmptyListUi(mAdapter.getConversationItemCount() == 0);
}
private void bindHomeCategoryStrip() {
if (mHomeCategoryRow == null || mArchiveMode || mForwardMessageMode) {
return;
}
final Context context = getContext();
if (context == null) {
return;
}
final String selectedId = HomeCategoryStore.getSelectedId(context);
final List<ConversationListItemData> items = mAdapter.snapshotConversations();
final LayoutInflater inflater = LayoutInflater.from(context);
if (mHomeCategoryAll != null) {
mHomeCategoryAll.removeAllViews();
}
mHomeCategoryRow.removeAllViews();
for (final HomeCategoryStore.Category category : HomeCategoryStore.listTabs(context)) {
final boolean pinnedAll = HomeCategoryStore.ID_ALL.equals(category.id);
final ViewGroup host = pinnedAll && mHomeCategoryAll != null
? mHomeCategoryAll : mHomeCategoryRow;
final View tab = inflater.inflate(R.layout.home_category_tab, host, false);
bindHomeCategoryTab(tab, category, selectedId, items);
host.addView(tab);
}
}
private void bindHomeCategoryTab(final View tab, final HomeCategoryStore.Category category,
final String selectedId, final List<ConversationListItemData> items) {
final Context context = tab.getContext();
final TextView label = tab.findViewById(R.id.home_category_tab_label);
final View underline = tab.findViewById(R.id.home_category_tab_underline);
final View dot = tab.findViewById(R.id.home_category_tab_dot);
final boolean selected = selectedId.equals(category.id);
if (label != null) {
label.setText(category.name);
label.setTypeface(null, selected ? android.graphics.Typeface.BOLD
: android.graphics.Typeface.NORMAL);
label.setTextColor(ContextCompat.getColor(context, selected
? R.color.home_accent : R.color.home_text_secondary));
}
if (underline != null) {
underline.setVisibility(selected ? View.VISIBLE : View.INVISIBLE);
}
if (dot != null) {
final boolean showDot = !HomeCategoryStore.ID_ALL.equals(category.id)
&& categoryHasUnread(context, category.id, items);
dot.setVisibility(showDot ? View.VISIBLE : View.GONE);
}
tab.setTag(category.id);
tab.setOnClickListener(v -> selectHomeCategory(category.id));
if (!category.builtIn) {
tab.setOnLongClickListener(v -> {
confirmDeleteHomeCategory(category);
return true;
});
}
}
private boolean categoryHasUnread(final Context context, final String categoryId,
final List<ConversationListItemData> items) {
for (final ConversationListItemData item : items) {
if (!item.getIsRead()
&& HomeCategoryClassifier.matches(context, categoryId, item)) {
return true;
}
}
return false;
}
private void selectHomeCategory(final String categoryId) {
final Context context = getContext();
if (context == null || mAdapter == null || TextUtils.isEmpty(categoryId)) {
return;
}
final String previous = HomeCategoryStore.getSelectedId(context);
if (categoryId.equals(previous) && homeCategoryStripHas(categoryId)) {
return;
}
final boolean fromEnd = homeCategoryIndex(context, categoryId)
>= homeCategoryIndex(context, previous);
HomeCategoryStore.setSelectedId(context, categoryId);
mAdapter.setCategoryId(categoryId);
mAdapter.refreshCategoryFilter();
if (homeCategoryStripHas(categoryId)) {
applyHomeCategorySelection(true);
} else {
bindHomeCategoryStrip();
}
updateEmptyListUi(mAdapter.getConversationItemCount() == 0);
if (mRecyclerView != null && mRecyclerView.getVisibility() == View.VISIBLE) {
HomeUiMotion.categoryPageEnter(mRecyclerView, fromEnd);
}
}
private boolean homeCategoryStripHas(final String categoryId) {
if (TextUtils.isEmpty(categoryId)) {
return false;
}
return findHomeCategoryTab(categoryId) != null;
}
private View findHomeCategoryTab(final String categoryId) {
final View pinned = findHomeCategoryTabIn(mHomeCategoryAll, categoryId);
if (pinned != null) {
return pinned;
}
return findHomeCategoryTabIn(mHomeCategoryRow, categoryId);
}
private static View findHomeCategoryTabIn(final ViewGroup host, final String categoryId) {
if (host == null) {
return null;
}
for (int i = 0; i < host.getChildCount(); i++) {
if (categoryId.equals(host.getChildAt(i).getTag())) {
return host.getChildAt(i);
}
}
return null;
}
private int homeCategoryIndex(final Context context, final String categoryId) {
final List<HomeCategoryStore.Category> tabs = HomeCategoryStore.listTabs(context);
for (int i = 0; i < tabs.size(); i++) {
if (tabs.get(i).id.equals(categoryId)) {
return i;
}
}
return 0;
}
private void applyHomeCategorySelection(final boolean animate) {
final Context context = getContext();
if (context == null) {
return;
}
final String selectedId = HomeCategoryStore.getSelectedId(context);
final int selectedColor = ContextCompat.getColor(context, R.color.home_accent);
final int idleColor = ContextCompat.getColor(context, R.color.home_text_secondary);
View selectedTab = null;
selectedTab = applyHomeCategoryHost(mHomeCategoryAll, selectedId, selectedColor, idleColor,
animate, selectedTab);
selectedTab = applyHomeCategoryHost(mHomeCategoryRow, selectedId, selectedColor, idleColor,
animate, selectedTab);
if (selectedTab != null
&& selectedTab.getParent() == mHomeCategoryRow
&& mHomeCategoryScroll instanceof android.widget.HorizontalScrollView) {
final View tab = selectedTab;
final android.widget.HorizontalScrollView scroller =
(android.widget.HorizontalScrollView) mHomeCategoryScroll;
scroller.post(() -> scroller.smoothScrollTo(
Math.max(tab.getLeft() - tab.getWidth(), 0), 0));
}
}
private static View applyHomeCategoryHost(final ViewGroup host, final String selectedId,
final int selectedColor, final int idleColor, final boolean animate,
View selectedTab) {
if (host == null) {
return selectedTab;
}
for (int i = 0; i < host.getChildCount(); i++) {
final View tab = host.getChildAt(i);
final Object id = tab.getTag();
if (!(id instanceof String)) {
continue;
}
final boolean selected = selectedId.equals(id);
if (selected) {
selectedTab = tab;
}
HomeUiMotion.applyCategoryTab(
tab.findViewById(R.id.home_category_tab_label),
tab.findViewById(R.id.home_category_tab_underline),
selectedColor, idleColor, selected, animate);
}
return selectedTab;
}
private void confirmDeleteHomeCategory(final HomeCategoryStore.Category category) {
final Context context = getContext();
if (context == null || category == null || category.builtIn) {
return;
}
HomeModal.builder(context)
.icon(R.drawable.ic_home_category)
.title(R.string.home_category_delete_title)
.message(R.string.home_category_delete_message)
.negative(android.R.string.cancel, null)
.positive(R.string.home_conversation_delete, () -> {
HomeCategoryStore.removeCustom(context, category.id);
selectHomeCategory(HomeCategoryStore.getSelectedId(context));
})
.show();
}
private void showAssignHomeCategory(final ConversationListItemData data) {
final Context context = getContext();
if (context == null || data == null || TextUtils.isEmpty(data.getConversationId())) {
return;
}
HomeCategoryAssignSheet.show(context, data, () -> {
if (!isAdded() || mAdapter == null) {
return;
}
mAdapter.refreshCategoryFilter();
bindHomeCategoryStrip();
updateEmptyListUi(mAdapter.getConversationItemCount() == 0);
});
}
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
@@ -1889,6 +2122,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
final Cursor oldCursor = mAdapter.swapCursor(cursor);
prunePinnedConversations(cursor);
refreshPinnedStrip();
bindHomeCategoryStrip();
updateEmptyListUi(mAdapter.getConversationItemCount() == 0);
if (mListState != null && cursor != null && oldCursor == null) {
mRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);
@@ -2028,6 +2262,11 @@ public class ConversationListFragment extends Fragment implements ConversationLi
mEmptyListMessageView.setContent(R.drawable.ic_home_menu_archive,
R.string.home_empty_archive_title,
R.string.home_empty_archive_body);
} else if (!HomeCategoryStore.ID_ALL.equals(mAdapter.getCategoryId())
|| mAdapter.getFilterMode() != ConversationListAdapter.FILTER_ALL) {
mEmptyListMessageView.setContent(R.drawable.ic_home_nav_messages,
R.string.home_category_empty_title,
R.string.home_category_empty_body);
} else {
mEmptyListMessageView.setContent(R.drawable.ic_home_nav_messages,
R.string.home_empty_messages_title,
@@ -341,20 +341,32 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
private void setSnippet() {
final String snippetText = ellipsizeSnippetForMaxLines(getSnippetText());
final int firstLineMargin = getSnippetLeadingMargin();
final CharSequence toSet;
if (firstLineMargin > 0 && !TextUtils.isEmpty(snippetText)) {
final SpannableString spanned = new SpannableString(snippetText);
spanned.setSpan(new LeadingMarginSpan.Standard(firstLineMargin, 0),
0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
toSet = spanned;
} else {
toSet = snippetText;
}
if (TextUtils.equals(toSet, mSnippetTextView.getText())) {
final int indent = getSnippetChipIndent();
if (indent <= 0) {
mSnippetTextView.setText(snippetText);
return;
}
mSnippetTextView.setText(toSet);
final SpannableString spanned = new SpannableString(snippetText == null ? "" : snippetText);
spanned.setSpan(new LeadingMarginSpan.Standard(indent, 0),
0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mSnippetTextView.setText(spanned);
}
private int getSnippetChipIndent() {
if (mCategoryChipView == null || mCategoryChipView.getVisibility() != VISIBLE) {
return 0;
}
int width = mCategoryChipView.getWidth();
if (width <= 0) {
mCategoryChipView.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
width = mCategoryChipView.getMeasuredWidth();
}
if (width <= 0) {
return 0;
}
return width + getResources().getDimensionPixelSize(R.dimen.looply_space_sm);
}
private String ellipsizeSnippetForMaxLines(final String source) {
@@ -367,11 +379,11 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
if (maxLines <= 0 || width <= 0) {
return source;
}
final int firstLineMargin = getSnippetLeadingMargin();
final int indent = getSnippetChipIndent();
final CharSequence measured;
if (firstLineMargin > 0) {
if (indent > 0) {
final SpannableString spanned = new SpannableString(source);
spanned.setSpan(new LeadingMarginSpan.Standard(firstLineMargin, 0),
spanned.setSpan(new LeadingMarginSpan.Standard(indent, 0),
0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
measured = spanned;
} else {
@@ -408,17 +420,6 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
return source.substring(0, cut) + "...";
}
private int getSnippetLeadingMargin() {
if (mCategoryChipView == null || mCategoryChipView.getVisibility() != VISIBLE) {
return 0;
}
mCategoryChipView.measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
return mCategoryChipView.getMeasuredWidth()
+ Math.round(6f * getResources().getDisplayMetrics().density);
}
private void updateSelectionIndicator(final boolean selectionMode, final boolean isSelected,
final Resources resources) {
if (mSelectionIndicatorView == null || mSelectionRingView == null
@@ -0,0 +1,100 @@
package com.android.messaging.ui.home;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.android.messaging.R;
import com.google.android.material.bottomsheet.BottomSheetDialog;
/** Default home bottom sheet: handle, close X, optional title, custom body, dismiss on outside tap. */
public final class HomeBottomSheet {
public static final class Builder {
private final Context mContext;
private CharSequence mTitle;
private View mCustom;
private boolean mCancelable = true;
public Builder(@NonNull final Context context) {
mContext = context;
}
public Builder title(@StringRes final int titleRes) {
mTitle = mContext.getString(titleRes);
return this;
}
public Builder title(final CharSequence title) {
mTitle = title;
return this;
}
public Builder custom(final View custom) {
mCustom = custom;
return this;
}
public Builder cancelable(final boolean cancelable) {
mCancelable = cancelable;
return this;
}
public BottomSheetDialog show() {
return HomeBottomSheet.show(this);
}
}
private HomeBottomSheet() {
}
public static Builder builder(@NonNull final Context context) {
return new Builder(context);
}
private static BottomSheetDialog show(final Builder options) {
final Context context = options.mContext;
final View root = LayoutInflater.from(context).inflate(R.layout.home_bottom_sheet, null, false);
final TextView title = root.findViewById(R.id.home_sheet_title);
if (title != null) {
title.setText(options.mTitle);
title.setVisibility(TextUtils.isEmpty(options.mTitle) ? View.GONE : View.VISIBLE);
}
final FrameLayout custom = root.findViewById(R.id.home_sheet_custom);
if (custom != null && options.mCustom != null) {
if (options.mCustom.getParent() instanceof ViewGroup) {
((ViewGroup) options.mCustom.getParent()).removeView(options.mCustom);
}
custom.addView(options.mCustom, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
final BottomSheetDialog dialog = new BottomSheetDialog(context,
R.style.Home_BottomSheetDialog);
dialog.setContentView(root);
dialog.setCancelable(options.mCancelable);
dialog.setCanceledOnTouchOutside(options.mCancelable);
final View close = root.findViewById(R.id.home_sheet_close);
if (close != null) {
close.setOnClickListener(v -> dialog.dismiss());
close.setVisibility(options.mCancelable ? View.VISIBLE : View.GONE);
}
final Window window = dialog.getWindow();
if (window != null) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
dialog.show();
HomeInputFocus.install(dialog.getWindow());
return dialog;
}
}
@@ -0,0 +1,80 @@
package com.android.messaging.ui.home;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.messaging.R;
import com.android.messaging.util.ImeUtil;
import java.util.Collections;
import java.util.List;
/** Shared Add category name modal. */
public final class HomeCategoryAddModal {
public interface Listener {
void onCategoryCreated(HomeCategoryStore.Category created);
}
private HomeCategoryAddModal() {
}
public static void show(@NonNull final Context context, @Nullable final Listener listener) {
final View content = LayoutInflater.from(context)
.inflate(R.layout.home_category_name_dialog, null, false);
final EditText field = content.findViewById(R.id.home_category_name_field);
HomeModal.builder(context)
.icon(R.drawable.ic_home_category)
.title(R.string.home_category_add)
.message(R.string.home_category_name_message)
.custom(content)
.negative(android.R.string.cancel, null)
.positive(R.string.home_category_add_new, () -> {
final String name = field == null ? "" : field.getText().toString();
final HomeCategoryStore.Category created = tryCreate(context, name, "",
Collections.emptyList());
if (created != null && listener != null) {
listener.onCategoryCreated(created);
}
})
.show();
if (field != null) {
field.requestFocus();
ImeUtil.get().showImeKeyboard(context, field);
}
}
@Nullable
public static HomeCategoryStore.Category tryCreate(@NonNull final Context context,
final String name, final String kind, @Nullable final List<String> conversationIds) {
final String trimmed = name == null ? "" : name.trim();
if (TextUtils.isEmpty(trimmed)) {
return null;
}
if (HomeCategoryStore.hasName(context, trimmed)) {
HomeToast.show(context, R.string.home_category_exists_toast);
return null;
}
if (HomeCategoryStore.listCustom(context).size() >= HomeCategoryStore.MAX_CUSTOM) {
HomeToast.show(context, R.string.home_category_limit_toast);
return null;
}
final HomeCategoryStore.Category created = HomeCategoryStore.addCustom(context, trimmed,
kind);
if (created == null) {
HomeToast.show(context, R.string.home_category_exists_toast);
return null;
}
if (conversationIds != null && !conversationIds.isEmpty()) {
HomeCategoryStore.assignAll(context, conversationIds, created.id);
}
HomeToast.show(context, R.string.home_category_added_toast);
return created;
}
}
@@ -0,0 +1,134 @@
package com.android.messaging.ui.home;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/** Assigns a conversation to categories using the default home modal. */
public final class HomeCategoryAssignSheet {
public interface Listener {
void onAssignmentsChanged();
}
private HomeCategoryAssignSheet() {
}
public static void show(@NonNull final Context context,
@NonNull final ConversationListItemData data, final Listener listener) {
final String conversationId = data.getConversationId();
if (TextUtils.isEmpty(conversationId)) {
return;
}
final View custom = LayoutInflater.from(context)
.inflate(R.layout.home_category_assign_sheet, null, false);
final ChipGroup chips = custom.findViewById(R.id.home_category_assign_chips);
final Set<String> selected = new LinkedHashSet<>(
HomeCategoryStore.assignedIds(context, conversationId));
if (!selected.contains(HomeCategoryStore.ID_PERSONAL)
&& !selected.contains(HomeCategoryStore.ID_PROMOTION)) {
selected.add(HomeCategoryClassifier.isPromotion(data)
? HomeCategoryStore.ID_PROMOTION : HomeCategoryStore.ID_PERSONAL);
}
final Runnable[] bindChips = new Runnable[1];
bindChips[0] = () -> {
if (chips == null) {
return;
}
chips.removeAllViews();
final List<HomeCategoryStore.Category> categories =
HomeCategoryStore.listTabs(context);
for (final HomeCategoryStore.Category category : categories) {
if (HomeCategoryStore.ID_ALL.equals(category.id)) {
continue;
}
final Chip chip = new Chip(context);
chip.setText(category.name);
chip.setCheckable(true);
chip.setCheckedIconVisible(false);
chip.setChecked(selected.contains(category.id));
styleChip(chip);
chip.setOnCheckedChangeListener((button, isChecked) -> {
if (isChecked) {
selected.add(category.id);
} else {
selected.remove(category.id);
}
styleChip(chip);
});
chips.addView(chip);
}
final Chip addChip = new Chip(context);
addChip.setText(R.string.home_category_add_new);
addChip.setCheckable(false);
addChip.setCheckedIconVisible(false);
styleChip(addChip);
addChip.setOnClickListener(v -> HomeCategoryAddModal.show(context, created -> {
selected.add(created.id);
bindChips[0].run();
}));
chips.addView(addChip);
};
bindChips[0].run();
HomeModal.builder(context)
.icon(R.drawable.ic_home_category)
.title(R.string.home_category_assign)
.message(R.string.home_category_assign_message)
.custom(custom)
.negative(android.R.string.cancel, null)
.positive(R.string.home_category_done, () -> {
applyAssignments(context, conversationId, selected);
if (listener != null) {
listener.onAssignmentsChanged();
}
HomeToast.show(context, R.string.home_category_updated_toast);
})
.show();
}
private static void applyAssignments(final Context context, final String conversationId,
final Set<String> selected) {
final Set<String> current = HomeCategoryStore.assignedIds(context, conversationId);
for (final String id : selected) {
if (!current.contains(id)) {
HomeCategoryStore.assign(context, conversationId, id);
}
}
for (final String id : current) {
if (!selected.contains(id)) {
HomeCategoryStore.unassign(context, conversationId, id);
}
}
}
private static void styleChip(final Chip chip) {
final float density = chip.getResources().getDisplayMetrics().density;
chip.setChipBackgroundColorResource(R.color.home_category_chip_bg);
chip.setChipStrokeColorResource(R.color.home_category_chip_stroke);
chip.setChipStrokeWidth(density);
chip.setChipMinHeight(36f * density);
chip.setChipStartPadding(12f * density);
chip.setChipEndPadding(12f * density);
chip.setTextSize(14f);
chip.setTypeface(chip.getTypeface(), android.graphics.Typeface.BOLD);
chip.setTextColor(ContextCompat.getColorStateList(chip.getContext(),
R.color.home_category_chip_text));
chip.setCheckedIconVisible(false);
chip.setEnsureMinTouchTargetSize(false);
}
}
@@ -0,0 +1,149 @@
package com.android.messaging.ui.home;
import android.content.Context;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ConversationListItemData;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/** Personal / promotion split and suggested categories from conversation text. */
public final class HomeCategoryClassifier {
public static final class Suggestion {
public final String kind;
public final String name;
public final List<String> conversationIds = new ArrayList<>();
Suggestion(final String kind, final String name) {
this.kind = kind;
this.name = name;
}
}
private HomeCategoryClassifier() {
}
public static boolean matches(final Context context, final String categoryId,
final ConversationListItemData item) {
if (item == null || TextUtils.isEmpty(categoryId)
|| HomeCategoryStore.ID_ALL.equals(categoryId)) {
return true;
}
if (HomeCategoryStore.ID_PERSONAL.equals(categoryId)) {
if (HomeCategoryStore.isAssigned(context, item.getConversationId(),
HomeCategoryStore.ID_PERSONAL)) {
return true;
}
if (HomeCategoryStore.isAssigned(context, item.getConversationId(),
HomeCategoryStore.ID_PROMOTION)) {
return false;
}
return isPersonal(item);
}
if (HomeCategoryStore.ID_PROMOTION.equals(categoryId)) {
if (HomeCategoryStore.isAssigned(context, item.getConversationId(),
HomeCategoryStore.ID_PROMOTION)) {
return true;
}
if (HomeCategoryStore.isAssigned(context, item.getConversationId(),
HomeCategoryStore.ID_PERSONAL)) {
return false;
}
return isPromotion(item);
}
return HomeCategoryStore.isAssigned(context, item.getConversationId(), categoryId);
}
public static boolean isPersonal(final ConversationListItemData item) {
return !isPromotion(item);
}
public static boolean isPromotion(final ConversationListItemData item) {
if (item == null) {
return false;
}
if (item.getIsGroup() || item.getParticipantContactId() > 0) {
return false;
}
if (looksLikeMobile(item)) {
return false;
}
return true;
}
public static boolean isVerification(final ConversationListItemData item) {
return containsAny(haystack(item),
"verification", "verify", "otp", "passcode", "auth code",
"인증", "인증번호", "인증코드", "인증 번호");
}
public static List<Suggestion> suggestions(final Context context,
final List<ConversationListItemData> items) {
final List<Suggestion> out = new ArrayList<>();
if (HomeCategoryStore.hasKind(context, HomeCategoryStore.KIND_VERIFICATION)) {
return out;
}
final Suggestion verification = new Suggestion(HomeCategoryStore.KIND_VERIFICATION,
context.getString(R.string.home_category_suggest_verification));
if (items != null) {
for (final ConversationListItemData item : items) {
if (item != null && isVerification(item)
&& !TextUtils.isEmpty(item.getConversationId())) {
verification.conversationIds.add(item.getConversationId());
}
}
}
if (!verification.conversationIds.isEmpty()) {
out.add(verification);
}
return out;
}
private static boolean looksLikeMobile(final ConversationListItemData item) {
final String dest = item.getOtherParticipantNormalizedDestination();
if (looksLikeMobileNumber(dest)) {
return true;
}
return looksLikeMobileNumber(item.getName());
}
private static boolean looksLikeMobileNumber(final String raw) {
if (TextUtils.isEmpty(raw)) {
return false;
}
final String digits = PhoneNumberUtils.normalizeNumber(raw);
if (TextUtils.isEmpty(digits)) {
return false;
}
for (int i = 0; i < digits.length(); i++) {
final char c = digits.charAt(i);
if (c != '+' && !Character.isDigit(c)) {
return false;
}
}
return digits.length() >= 8;
}
private static String haystack(final ConversationListItemData item) {
final String name = item.getName() == null ? "" : item.getName();
final String snippet = item.getSnippetText() == null ? "" : item.getSnippetText();
return (name + " " + snippet).toLowerCase(Locale.ROOT);
}
private static boolean containsAny(final String haystack, final String... needles) {
if (TextUtils.isEmpty(haystack)) {
return false;
}
for (final String needle : needles) {
if (haystack.contains(needle)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,152 @@
/*
* Copyright (C) 2026 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.messaging.ui.home;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewParent;
import android.widget.HorizontalScrollView;
import com.android.messaging.R;
/** Category tab strip that keeps horizontal swipes instead of letting parents steal them. */
public final class HomeCategoryScroller extends HorizontalScrollView {
private final int mTouchSlop;
private float mDownX;
private float mDownY;
private boolean mDragging;
public HomeCategoryScroller(final Context context) {
this(context, null);
}
public HomeCategoryScroller(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public HomeCategoryScroller(final Context context, final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
setHorizontalScrollBarEnabled(false);
setHorizontalFadingEdgeEnabled(false);
setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS);
}
@Override
protected void onLayout(final boolean changed, final int l, final int t, final int r,
final int b) {
super.onLayout(changed, l, t, r, b);
updateEdgeFades();
}
@Override
protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
updateEdgeFades();
}
private void updateEdgeFades() {
final ViewParent parent = getParent();
if (!(parent instanceof View)) {
return;
}
final View host = (View) parent;
final View start = host.findViewById(R.id.home_category_fade_start);
final View end = host.findViewById(R.id.home_category_fade_end);
final boolean overflow = canScrollContent();
final int startVis = overflow && getScrollX() > 0 ? VISIBLE : GONE;
final int endVis = overflow ? VISIBLE : GONE;
if (start != null && start.getVisibility() != startVis) {
start.setVisibility(startVis);
}
if (end != null && end.getVisibility() != endVis) {
end.setVisibility(endVis);
}
}
@Override
public boolean onInterceptTouchEvent(final MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getX();
mDownY = ev.getY();
mDragging = false;
disallowParent(canScrollContent());
break;
case MotionEvent.ACTION_MOVE:
if (isHorizontalDrag(ev)) {
mDragging = true;
disallowParent(true);
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mDragging = false;
disallowParent(false);
break;
default:
break;
}
return super.onInterceptTouchEvent(ev) || mDragging;
}
@Override
public boolean onTouchEvent(final MotionEvent ev) {
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
mDownX = ev.getX();
mDownY = ev.getY();
mDragging = false;
} else if (action == MotionEvent.ACTION_MOVE && isHorizontalDrag(ev)) {
mDragging = true;
}
if (mDragging || canScrollContent()) {
disallowParent(action != MotionEvent.ACTION_UP && action != MotionEvent.ACTION_CANCEL);
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mDragging = false;
disallowParent(false);
}
return super.onTouchEvent(ev);
}
private boolean isHorizontalDrag(final MotionEvent ev) {
final float dx = Math.abs(ev.getX() - mDownX);
final float dy = Math.abs(ev.getY() - mDownY);
return dx > mTouchSlop && dx > dy;
}
private boolean canScrollContent() {
if (getChildCount() == 0) {
return false;
}
final View child = getChildAt(0);
return child.getWidth() > getWidth() - getPaddingLeft() - getPaddingRight();
}
private void disallowParent(final boolean disallow) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(disallow);
}
}
}
@@ -0,0 +1,256 @@
package com.android.messaging.ui.home;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import com.android.messaging.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/** Built-in and user-created conversation categories for the home list. */
public final class HomeCategoryStore {
public static final String ID_ALL = "all";
public static final String ID_PERSONAL = "personal";
public static final String ID_PROMOTION = "promotion";
public static final String KIND_VERIFICATION = "verification";
private static final String PREFS_NAME = "home_categories";
private static final String KEY_CUSTOM = "custom";
private static final String KEY_ASSIGN = "assign";
private static final String KEY_SELECTED = "selected";
private static final String ROW = "\n";
private static final String COL = "\t";
public static final int MAX_CUSTOM = 12;
public static final int MAX_NAME = 24;
public static final class Category {
public final String id;
public final String name;
public final String kind;
public final boolean builtIn;
Category(final String id, final String name, final String kind, final boolean builtIn) {
this.id = id;
this.name = name;
this.kind = kind == null ? "" : kind;
this.builtIn = builtIn;
}
}
private HomeCategoryStore() {
}
public static boolean isBuiltIn(final String id) {
return ID_ALL.equals(id) || ID_PERSONAL.equals(id) || ID_PROMOTION.equals(id);
}
public static List<Category> listTabs(final Context context) {
final List<Category> tabs = new ArrayList<>();
tabs.add(new Category(ID_ALL, context.getString(R.string.home_category_all), "", true));
tabs.add(new Category(ID_PERSONAL, context.getString(R.string.home_category_personal),
"", true));
tabs.add(new Category(ID_PROMOTION, context.getString(R.string.home_category_promotion),
"", true));
tabs.addAll(listCustom(context));
return tabs;
}
public static List<Category> listCustom(final Context context) {
final String raw = prefs(context).getString(KEY_CUSTOM, "");
if (TextUtils.isEmpty(raw)) {
return Collections.emptyList();
}
final List<Category> items = new ArrayList<>();
for (final String line : raw.split(ROW, -1)) {
if (TextUtils.isEmpty(line)) {
continue;
}
final String[] parts = line.split(COL, -1);
if (parts.length < 2 || TextUtils.isEmpty(parts[0]) || TextUtils.isEmpty(parts[1])) {
continue;
}
final String kind = parts.length > 2 ? parts[2] : "";
items.add(new Category(parts[0], parts[1], kind, false));
}
return items;
}
public static boolean hasKind(final Context context, final String kind) {
if (TextUtils.isEmpty(kind)) {
return false;
}
for (final Category category : listCustom(context)) {
if (kind.equals(category.kind)) {
return true;
}
}
return false;
}
public static boolean hasName(final Context context, final String name) {
final String trimmed = name == null ? "" : name.trim();
if (TextUtils.isEmpty(trimmed)) {
return false;
}
for (final Category category : listTabs(context)) {
if (trimmed.equalsIgnoreCase(category.name)) {
return true;
}
}
return false;
}
public static Category addCustom(final Context context, final String name, final String kind) {
String trimmed = name == null ? "" : name.trim();
if (TextUtils.isEmpty(trimmed)) {
return null;
}
if (trimmed.length() > MAX_NAME) {
trimmed = trimmed.substring(0, MAX_NAME);
}
if (hasName(context, trimmed)) {
return null;
}
final List<Category> items = new ArrayList<>(listCustom(context));
if (items.size() >= MAX_CUSTOM) {
return null;
}
final Category created = new Category("c_" + UUID.randomUUID().toString(), trimmed,
kind == null ? "" : kind, false);
items.add(created);
saveCustom(context, items);
return created;
}
public static void removeCustom(final Context context, final String categoryId) {
if (isBuiltIn(categoryId)) {
return;
}
final List<Category> items = new ArrayList<>(listCustom(context));
boolean removed = false;
for (int i = items.size() - 1; i >= 0; i--) {
if (items.get(i).id.equals(categoryId)) {
items.remove(i);
removed = true;
}
}
if (!removed) {
return;
}
saveCustom(context, items);
final List<String> kept = new ArrayList<>();
for (final String row : assignmentRows(context)) {
final String[] parts = row.split(COL, -1);
if (parts.length >= 2 && !categoryId.equals(parts[1])) {
kept.add(row);
}
}
prefs(context).edit().putString(KEY_ASSIGN, TextUtils.join(ROW, kept)).apply();
if (categoryId.equals(getSelectedId(context))) {
setSelectedId(context, ID_ALL);
}
}
public static void assign(final Context context, final String conversationId,
final String categoryId) {
if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(categoryId)
|| ID_ALL.equals(categoryId)) {
return;
}
final LinkedHashSet<String> rows = new LinkedHashSet<>(assignmentRows(context));
rows.add(conversationId + COL + categoryId);
prefs(context).edit().putString(KEY_ASSIGN, TextUtils.join(ROW, rows)).apply();
}
public static void assignAll(final Context context, final List<String> conversationIds,
final String categoryId) {
if (conversationIds == null) {
return;
}
for (final String id : conversationIds) {
assign(context, id, categoryId);
}
}
public static void unassign(final Context context, final String conversationId,
final String categoryId) {
if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(categoryId)) {
return;
}
final String needle = conversationId + COL + categoryId;
final List<String> rows = assignmentRows(context);
if (!rows.remove(needle)) {
return;
}
prefs(context).edit().putString(KEY_ASSIGN, TextUtils.join(ROW, rows)).apply();
}
public static Set<String> assignedIds(final Context context,
final String conversationId) {
final LinkedHashSet<String> ids = new LinkedHashSet<>();
if (TextUtils.isEmpty(conversationId)) {
return ids;
}
for (final String row : assignmentRows(context)) {
final String[] parts = row.split(COL, -1);
if (parts.length >= 2 && conversationId.equals(parts[0])) {
ids.add(parts[1]);
}
}
return ids;
}
public static boolean isAssigned(final Context context, final String conversationId,
final String categoryId) {
if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(categoryId)) {
return false;
}
final String needle = conversationId + COL + categoryId;
return assignmentRows(context).contains(needle);
}
public static String getSelectedId(final Context context) {
final String selected = prefs(context).getString(KEY_SELECTED, ID_ALL);
return TextUtils.isEmpty(selected) ? ID_ALL : selected;
}
public static void setSelectedId(final Context context, final String categoryId) {
prefs(context).edit().putString(KEY_SELECTED,
TextUtils.isEmpty(categoryId) ? ID_ALL : categoryId).apply();
}
private static List<String> assignmentRows(final Context context) {
final String raw = prefs(context).getString(KEY_ASSIGN, "");
if (TextUtils.isEmpty(raw)) {
return new ArrayList<>();
}
final List<String> rows = new ArrayList<>();
for (final String line : raw.split(ROW, -1)) {
if (!TextUtils.isEmpty(line)) {
rows.add(line);
}
}
return rows;
}
private static void saveCustom(final Context context, final List<Category> items) {
final List<String> rows = new ArrayList<>(items.size());
for (final Category item : items) {
rows.add(item.id + COL + item.name + COL + item.kind);
}
prefs(context).edit().putString(KEY_CUSTOM, TextUtils.join(ROW, rows)).apply();
}
private static SharedPreferences prefs(final Context context) {
return context.getApplicationContext().getSharedPreferences(PREFS_NAME,
Context.MODE_PRIVATE);
}
}
@@ -0,0 +1,112 @@
/*
* Copyright (C) 2026 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.messaging.ui.home;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import com.android.messaging.R;
/**
* Category tabs + add button. Tabs stay left of the add control; the add button
* follows the last tab and never crosses the strip's end padding.
*/
public final class HomeCategoryStrip extends LinearLayout {
public HomeCategoryStrip(final Context context) {
this(context, null);
}
public HomeCategoryStrip(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public HomeCategoryStrip(final Context context, final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(HORIZONTAL);
setClipToPadding(true);
setClipChildren(true);
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
final View all = findViewById(R.id.home_category_all);
final View add = findViewById(R.id.home_category_add);
final View scroll = findViewById(R.id.home_category_scroll_host);
int pinned = 0;
int contentHeight = 0;
if (all != null && all.getVisibility() != GONE) {
measureChildWithMargins(all, widthMeasureSpec, 0, heightMeasureSpec, 0);
pinned += measuredWidthWithMargins(all);
contentHeight = Math.max(contentHeight, measuredHeightWithMargins(all));
}
if (add != null && add.getVisibility() != GONE) {
measureChildWithMargins(add, widthMeasureSpec, pinned, heightMeasureSpec, 0);
pinned += measuredWidthWithMargins(add);
contentHeight = Math.max(contentHeight, measuredHeightWithMargins(add));
}
int contentWidth = pinned;
if (scroll != null && scroll.getVisibility() != GONE) {
measureChildWithMargins(scroll, widthMeasureSpec, pinned, heightMeasureSpec, 0);
contentWidth += measuredWidthWithMargins(scroll);
contentHeight = Math.max(contentHeight, measuredHeightWithMargins(scroll));
}
setMeasuredDimension(
resolveSize(contentWidth + getPaddingLeft() + getPaddingRight(),
widthMeasureSpec),
resolveSize(contentHeight + getPaddingTop() + getPaddingBottom(),
heightMeasureSpec));
}
@Override
protected void onLayout(final boolean changed, final int left, final int top, final int right,
final int bottom) {
final int innerTop = getPaddingTop();
final int innerBottom = bottom - top - getPaddingBottom();
int x = getPaddingLeft();
x = layoutChild(findViewById(R.id.home_category_all), x, innerTop, innerBottom);
x = layoutChild(findViewById(R.id.home_category_scroll_host), x, innerTop, innerBottom);
layoutChild(findViewById(R.id.home_category_add), x, innerTop, innerBottom);
}
private int layoutChild(final View child, final int x, final int innerTop,
final int innerBottom) {
if (child == null || child.getVisibility() == GONE) {
return x;
}
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int height = child.getMeasuredHeight();
final int childTop = innerTop + lp.topMargin
+ Math.max(0, (innerBottom - innerTop - height - lp.topMargin - lp.bottomMargin) / 2);
final int childLeft = x + lp.leftMargin;
child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(),
childTop + height);
return childLeft + child.getMeasuredWidth() + lp.rightMargin;
}
private static int measuredWidthWithMargins(final View child) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
return child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
}
private static int measuredHeightWithMargins(final View child) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
return child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
}
@@ -89,6 +89,16 @@ public class HomeComposeActivity extends BugleActionBarActivity
mNextButton.setOnClickListener(v -> openGroupNameStep());
mRecipientField = findViewById(R.id.home_compose_recipient);
final View recipientBox = findViewById(R.id.home_compose_recipient_box);
if (recipientBox != null) {
recipientBox.setOnClickListener(v -> {
if (mRecipientField.getVisibility() != View.VISIBLE) {
return;
}
mRecipientField.requestFocus();
ImeUtil.get().showImeKeyboard(this, mRecipientField);
});
}
mRecipientField.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count,
@@ -89,6 +89,5 @@ public final class HomeDeleteAlertDialog {
WindowManager.LayoutParams.WRAP_CONTENT);
window.getDecorView().setPadding(margin, 0, margin, 0);
}
HomeUiMotion.dialogEnter(content);
}
}
@@ -0,0 +1,88 @@
package com.android.messaging.ui.home;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.Window;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.view.WindowCallbackWrapper;
import com.android.messaging.util.ImeUtil;
/** Shared rule: tap outside an input box clears focus and hides the keyboard. */
public final class HomeInputFocus {
private HomeInputFocus() {
}
public static void install(@Nullable final Window window) {
if (window == null) {
return;
}
final Window.Callback current = window.getCallback();
if (current instanceof Installed) {
return;
}
window.setCallback(new Installed(current, window));
}
public static boolean clearIfOutside(@Nullable final View root, @Nullable final MotionEvent ev) {
if (root == null || ev == null || ev.getAction() != MotionEvent.ACTION_DOWN) {
return false;
}
final View focused = root.findFocus();
if (!(focused instanceof EditText)) {
return false;
}
final View box = findBox(focused);
if (isInside(box, ev)) {
return false;
}
root.setFocusable(true);
root.setFocusableInTouchMode(true);
focused.clearFocus();
root.requestFocus();
ImeUtil.get().hideImeKeyboard(focused.getContext(), focused);
return true;
}
private static View findBox(final View focused) {
ViewParent parent = focused.getParent();
while (parent instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) parent;
if (group.addStatesFromChildren()) {
return group;
}
parent = group.getParent();
}
return focused;
}
private static boolean isInside(final View view, final MotionEvent ev) {
final Rect rect = new Rect();
if (!view.getGlobalVisibleRect(rect)) {
return false;
}
return rect.contains(Math.round(ev.getRawX()), Math.round(ev.getRawY()));
}
private static final class Installed extends WindowCallbackWrapper {
private final Window mWindow;
Installed(final Window.Callback wrapped, final Window window) {
super(wrapped);
mWindow = window;
}
@Override
public boolean dispatchTouchEvent(@NonNull final MotionEvent event) {
clearIfOutside(mWindow.getDecorView(), event);
return super.dispatchTouchEvent(event);
}
}
}
@@ -0,0 +1,271 @@
package com.android.messaging.ui.home;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.android.messaging.R;
/**
* Default home modal: centered icon on a soft well, title, body, optional
* custom content, and stacked pill actions.
*/
public final class HomeModal {
public interface Action {
void onClick();
}
public static final class Handle {
public final AlertDialog dialog;
public final TextView title;
public final TextView message;
public final TextView positive;
public final TextView negative;
public final FrameLayout custom;
Handle(final AlertDialog dialog, final TextView title, final TextView message,
final TextView positive, final TextView negative, final FrameLayout custom) {
this.dialog = dialog;
this.title = title;
this.message = message;
this.positive = positive;
this.negative = negative;
this.custom = custom;
}
public void setTitle(final CharSequence text) {
if (title != null) {
title.setText(text);
title.setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE);
}
}
public void setMessage(final CharSequence text) {
if (message != null) {
message.setText(text);
message.setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE);
}
}
public void setPositive(final CharSequence label, @Nullable final Action action,
final boolean dismiss) {
bindButton(positive, label, action, dismiss);
}
public void setNegative(final CharSequence label, @Nullable final Action action,
final boolean dismiss) {
bindButton(negative, label, action, dismiss);
}
private void bindButton(final TextView button, final CharSequence label,
final Action action, final boolean dismiss) {
if (button == null) {
return;
}
button.setText(label);
button.setVisibility(TextUtils.isEmpty(label) ? View.GONE : View.VISIBLE);
button.setOnClickListener(v -> {
if (dismiss) {
dialog.dismiss();
}
if (action != null) {
action.onClick();
}
});
}
}
public static final class Builder {
private final Context mContext;
@DrawableRes
private int mIconRes;
private CharSequence mTitle;
private CharSequence mMessage;
private CharSequence mPositive;
private CharSequence mNegative;
private Action mOnPositive;
private Action mOnNegative;
private View mCustom;
private boolean mCancelable = true;
public Builder(@NonNull final Context context) {
mContext = context;
}
public Builder icon(@DrawableRes final int iconRes) {
mIconRes = iconRes;
return this;
}
public Builder title(@StringRes final int titleRes) {
mTitle = mContext.getString(titleRes);
return this;
}
public Builder title(final CharSequence title) {
mTitle = title;
return this;
}
public Builder message(@StringRes final int messageRes) {
mMessage = mContext.getString(messageRes);
return this;
}
public Builder message(final CharSequence message) {
mMessage = message;
return this;
}
public Builder custom(final View custom) {
mCustom = custom;
return this;
}
public Builder positive(@StringRes final int labelRes, @Nullable final Action action) {
mPositive = mContext.getString(labelRes);
mOnPositive = action;
return this;
}
public Builder positive(final CharSequence label, @Nullable final Action action) {
mPositive = label;
mOnPositive = action;
return this;
}
public Builder negative(@StringRes final int labelRes, @Nullable final Action action) {
mNegative = mContext.getString(labelRes);
mOnNegative = action;
return this;
}
public Builder negative(final CharSequence label, @Nullable final Action action) {
mNegative = label;
mOnNegative = action;
return this;
}
public Builder cancelable(final boolean cancelable) {
mCancelable = cancelable;
return this;
}
public Handle show() {
return HomeModal.show(this);
}
}
private HomeModal() {
}
public static Builder builder(@NonNull final Context context) {
return new Builder(context);
}
private static Handle show(final Builder options) {
final Context context = options.mContext;
final View content = LayoutInflater.from(context).inflate(R.layout.home_modal, null, false);
bind(content, options);
final AlertDialog dialog = new AlertDialog.Builder(context, R.style.Home_AlertDialog)
.setView(content)
.setCancelable(options.mCancelable)
.create();
dialog.setCanceledOnTouchOutside(options.mCancelable);
final View close = content.findViewById(R.id.home_modal_close);
if (close != null) {
close.setVisibility(options.mCancelable ? View.VISIBLE : View.GONE);
close.setOnClickListener(v -> dialog.dismiss());
}
final TextView positive = content.findViewById(R.id.home_modal_positive);
final TextView negative = content.findViewById(R.id.home_modal_negative);
bindButton(negative, options.mNegative, options.mOnNegative, dialog, true);
bindButton(positive, options.mPositive, options.mOnPositive, dialog, true);
dialog.show();
final Window window = dialog.getWindow();
if (window != null) {
window.setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
final int margin = context.getResources().getDimensionPixelSize(
R.dimen.home_alert_dialog_horizontal_margin);
window.setLayout(WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT);
window.getDecorView().setPadding(margin, 0, margin, 0);
HomeInputFocus.install(window);
}
return new Handle(dialog, content.findViewById(R.id.home_modal_title),
content.findViewById(R.id.home_modal_message), positive, negative,
content.findViewById(R.id.home_modal_custom));
}
private static void bind(final View content, final Builder options) {
final View iconWell = content.findViewById(R.id.home_modal_icon_well);
final ImageView icon = content.findViewById(R.id.home_modal_icon);
if (options.mIconRes != 0 && icon != null) {
icon.setImageResource(options.mIconRes);
if (iconWell != null) {
iconWell.setVisibility(View.VISIBLE);
} else {
icon.setVisibility(View.VISIBLE);
}
}
final TextView title = content.findViewById(R.id.home_modal_title);
if (title != null) {
title.setText(options.mTitle);
title.setVisibility(TextUtils.isEmpty(options.mTitle) ? View.GONE : View.VISIBLE);
}
final TextView message = content.findViewById(R.id.home_modal_message);
if (message != null) {
message.setText(options.mMessage);
message.setVisibility(TextUtils.isEmpty(options.mMessage) ? View.GONE : View.VISIBLE);
}
final FrameLayout custom = content.findViewById(R.id.home_modal_custom);
if (custom != null && options.mCustom != null) {
if (options.mCustom.getParent() instanceof ViewGroup) {
((ViewGroup) options.mCustom.getParent()).removeView(options.mCustom);
}
custom.addView(options.mCustom, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
custom.setVisibility(View.VISIBLE);
}
}
private static void bindButton(final TextView button, final CharSequence label,
final Action action, final AlertDialog dialog, final boolean dismiss) {
if (button == null || TextUtils.isEmpty(label)) {
return;
}
button.setText(label);
button.setVisibility(View.VISIBLE);
button.setOnClickListener(v -> {
if (dismiss) {
dialog.dismiss();
}
if (action != null) {
action.onClick();
}
});
}
}
@@ -16,12 +16,14 @@
package com.android.messaging.ui.home;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.MeasureSpec;
import android.view.animation.PathInterpolator;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
@@ -679,19 +681,52 @@ public final class HomeUiMotion {
return;
}
content.setAlpha(0f);
content.setScaleX(0.94f);
content.setScaleY(0.94f);
content.setScaleX(0.92f);
content.setScaleY(0.92f);
beginLayer(content);
content.animate()
.alpha(1f)
.scaleX(1f)
.scaleY(1f)
.setDuration(300)
.setDuration(280)
.setInterpolator(EASE)
.withEndAction(() -> endLayer(content))
.start();
}
public static void dialogExit(final View content, final Runnable endAction) {
if (content == null) {
if (endAction != null) {
endAction.run();
}
return;
}
if (!enabled()) {
if (endAction != null) {
endAction.run();
}
return;
}
beginLayer(content);
content.animate().cancel();
content.animate()
.alpha(0f)
.scaleX(0.94f)
.scaleY(0.94f)
.setDuration(180)
.setInterpolator(EASE)
.withEndAction(() -> {
endLayer(content);
content.setAlpha(1f);
content.setScaleX(1f);
content.setScaleY(1f);
if (endAction != null) {
endAction.run();
}
})
.start();
}
private static void applyBottomDockStateInstant(final View mainNav, final View selectionBar,
final boolean selectionActive) {
if (mainNav != null) {
@@ -907,7 +942,7 @@ public final class HomeUiMotion {
if (attachButton == null) {
return;
}
attachButton.setImageResource(R.drawable.ic_home_compose_plus);
attachButton.setImageResource(R.drawable.ic_home_automation_plus);
attachButton.setRotation(0f);
}
@@ -1070,4 +1105,107 @@ public final class HomeUiMotion {
})
.start();
}
/** Selects a category tab: accent color plus a scale-in underline. */
public static void applyCategoryTab(final TextView label, final View underline,
final int selectedColor, final int idleColor, final boolean selected,
final boolean animate) {
if (label != null) {
label.setTypeface(null, selected ? android.graphics.Typeface.BOLD
: android.graphics.Typeface.NORMAL);
final Object running = label.getTag(R.id.home_category_tab_label);
if (running instanceof ValueAnimator) {
((ValueAnimator) running).cancel();
label.setTag(R.id.home_category_tab_label, null);
}
if (!enabled() || !animate) {
label.setTextColor(selected ? selectedColor : idleColor);
} else {
final int from = label.getCurrentTextColor();
final int to = selected ? selectedColor : idleColor;
if (from != to) {
final ValueAnimator color = ValueAnimator.ofObject(new ArgbEvaluator(), from, to);
color.setDuration(HOME_TAB_MS);
color.setInterpolator(EASE);
color.addUpdateListener(animation ->
label.setTextColor((Integer) animation.getAnimatedValue()));
color.addListener(new android.animation.AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final android.animation.Animator animation) {
if (label.getTag(R.id.home_category_tab_label) == color) {
label.setTag(R.id.home_category_tab_label, null);
}
}
});
label.setTag(R.id.home_category_tab_label, color);
color.start();
}
}
}
if (underline == null) {
return;
}
underline.animate().cancel();
underline.setPivotX(Math.max(underline.getWidth(), 1) / 2f);
underline.setPivotY(underline.getHeight());
if (selected) {
underline.setVisibility(View.VISIBLE);
if (!enabled() || !animate) {
underline.setScaleX(1f);
underline.setAlpha(1f);
return;
}
underline.setScaleX(0.35f);
underline.setAlpha(0f);
underline.animate()
.scaleX(1f)
.alpha(1f)
.setDuration(HOME_TAB_MS)
.setInterpolator(EASE)
.start();
return;
}
if (!enabled() || !animate || underline.getVisibility() != View.VISIBLE) {
underline.setScaleX(1f);
underline.setAlpha(1f);
underline.setVisibility(View.INVISIBLE);
return;
}
underline.animate()
.scaleX(0.35f)
.alpha(0f)
.setDuration(HOME_TAB_MS)
.setInterpolator(EASE)
.withEndAction(() -> {
underline.setVisibility(View.INVISIBLE);
underline.setScaleX(1f);
underline.setAlpha(1f);
})
.start();
}
/** Short fade + horizontal slide after a category list swap. */
public static void categoryPageEnter(final View list, final boolean fromEnd) {
if (list == null) {
return;
}
list.animate().cancel();
if (!enabled()) {
list.setAlpha(1f);
list.setTranslationX(0f);
return;
}
final int slide = Math.max(list.getWidth() / 12,
list.getResources().getDimensionPixelSize(R.dimen.looply_space_xl));
list.setAlpha(0.2f);
list.setTranslationX(fromEnd ? slide : -slide);
beginLayer(list);
list.animate()
.alpha(1f)
.translationX(0f)
.setDuration(HOME_TAB_MS)
.setInterpolator(EASE)
.withEndAction(() -> endLayer(list))
.start();
}
}
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:shareInterpolator="true">
<alpha
android:duration="220"
android:fromAlpha="0"
android:toAlpha="1" />
<scale
android:duration="280"
android:fromXScale="0.92"
android:fromYScale="0.92"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1"
android:toYScale="1" />
</set>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:shareInterpolator="true">
<alpha
android:duration="180"
android:fromAlpha="1"
android:toAlpha="0" />
<scale
android:duration="180"
android:fromXScale="1"
android:fromYScale="1"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="0.94"
android:toYScale="0.94" />
</set>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:color="@color/home_accent_soft"
android:state_selected="true" />
<item android:color="@color/home_accent" />
</selector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/home_accent" android:state_checked="true" />
<item android:color="@color/home_row_bg" />
</selector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/home_accent" android:state_checked="true" />
<item android:color="@color/home_button_stroke" />
</selector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/white" android:state_checked="true" />
<item android:color="@color/home_text_primary" />
</selector>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_accent_soft">
<item>
<selector>
<item
android:drawable="@drawable/home_bg_add_circle_focused"
android:state_selected="true" />
<item android:drawable="@drawable/home_bg_add_circle_idle" />
</selector>
</item>
</ripple>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/home_accent" />
</shape>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/home_accent_soft" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="0"
android:endColor="@color/home_bg"
android:startColor="#00F4F1FB" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="0"
android:endColor="#00F4F1FB"
android:startColor="@color/home_bg" />
</shape>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_accent_soft">
<item>
<shape android:shape="oval">
<solid android:color="@color/home_accent_soft" />
</shape>
</item>
</ripple>
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/looply_radius_pill" />
<solid android:color="@color/home_compose_pill_bg" />
</shape>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/home_bg_search_field_focused"
android:state_focused="true" />
<item android:drawable="@drawable/home_bg_search_field_idle" />
</selector>
@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="22dp" />
<solid android:color="@color/home_bg" />
<solid android:color="@color/home_date_pill_blur" />
<stroke
android:width="0.5dp"
android:color="@color/home_button_stroke" />
@@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/home_bg" />
<stroke
android:width="1dp"
android:color="@color/home_divider" />
<solid android:color="@color/home_accent" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/home_row_bg" />
<corners android:radius="@dimen/looply_radius_xl" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_surface_elevated">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/home_surface" />
<corners android:radius="@dimen/looply_radius_pill" />
</shape>
</item>
</ripple>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/home_accent_soft" />
<corners android:radius="@dimen/looply_radius_md" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/home_bg_modal_input_focused"
android:state_focused="true" />
<item android:drawable="@drawable/home_bg_modal_input_idle" />
</selector>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/home_accent_soft" />
<corners android:radius="@dimen/looply_radius_lg" />
<stroke
android:width="1.5dp"
android:color="@color/home_accent" />
</shape>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/home_surface" />
<corners android:radius="@dimen/looply_radius_lg" />
<stroke
android:width="1.5dp"
android:color="@color/home_divider" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_accent_soft">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/home_accent" />
<corners android:radius="@dimen/looply_radius_pill" />
</shape>
</item>
</ripple>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/home_bg_search_field_focused"
android:state_focused="true" />
<item android:drawable="@drawable/home_bg_search_field_idle" />
</selector>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/home_accent_soft" />
<corners android:radius="@dimen/looply_radius_pill" />
<stroke
android:width="1.5dp"
android:color="@color/home_accent" />
</shape>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/home_search_field_bg" />
<corners android:radius="@dimen/looply_radius_pill" />
<stroke
android:width="1.5dp"
android:color="@color/home_divider" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="2dp" />
<solid android:color="@color/home_accent" />
</shape>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:pathData="M4.5,6.5h8.2l6.3,5.5 -6.3,5.5H4.5V6.5z"
android:strokeWidth="1.8"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:fillColor="#000000"
android:pathData="M9.35,12 A1.15,1.15 0 1 1 6.9,12 A1.15,1.15 0 1 1 9.35,12 Z" />
</vector>
@@ -5,9 +5,9 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#8E8E93"
android:fillColor="#FFFFFF"
android:pathData="M12,5l5,5H7z" />
<path
android:fillColor="#8E8E93"
android:fillColor="#FFFFFF"
android:pathData="M12,19l-5,-5h10z" />
</vector>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:pathData="M23,12A11,11 0,1 1,1 12A11,11 0,1 1,23 12"
android:strokeWidth="1.8"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M12,17.6L12,6.4M12,6.4L15.2,9.6M12,6.4L8.8,9.6"
android:strokeWidth="1.8"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -242,6 +242,8 @@
android:layout_height="@dimen/home_conversation_compose_pill_height"
android:addStatesFromChildren="true"
android:background="@drawable/home_bg_compose_pill"
android:gravity="center_vertical"
@@ -254,37 +256,21 @@
<FrameLayout
<ImageButton
android:layout_width="@dimen/home_compose_attach_size"
android:id="@+id/attach_media_button"
android:layout_height="@dimen/home_compose_attach_size"
style="@style/Home.AddButton"
android:layout_marginStart="@dimen/looply_space_xs">
android:layout_marginStart="@dimen/looply_space_xs"
android:contentDescription="@string/attachMediaButtonContentDescription"
android:focusable="false"
<ImageButton
app:srcCompat="@drawable/ic_home_automation_plus"
android:id="@+id/attach_media_button"
android:layout_width="@dimen/home_compose_attach_size"
android:layout_height="@dimen/home_compose_attach_size"
android:background="@drawable/home_bg_compose_attach_circle"
android:contentDescription="@string/attachMediaButtonContentDescription"
android:padding="@dimen/home_compose_attach_icon_padding"
android:scaleType="centerInside"
app:srcCompat="@drawable/ic_home_compose_plus"
app:tint="@color/home_accent" />
</FrameLayout>
app:tint="@color/home_add_icon_tint" />
@@ -326,6 +312,8 @@
android:textColorHint="@color/home_text_secondary"
android:textCursorDrawable="@drawable/home_cursor"
android:textSize="16sp" />
@@ -342,6 +330,8 @@
android:contentDescription="@string/home_compose_emoji_content_description"
android:focusable="false"
android:padding="6dp"
android:scaleType="centerInside"
@@ -366,6 +356,8 @@
android:contentDescription="@string/home_compose_gallery_content_description"
android:focusable="false"
android:padding="6dp"
android:scaleType="centerInside"
@@ -452,9 +444,9 @@
android:id="@+id/home_conversation_send_button_container"
android:layout_width="@dimen/home_conversation_compose_send_size"
android:layout_width="@dimen/home_conversation_compose_voice_size"
android:layout_height="@dimen/home_conversation_compose_send_size"
android:layout_height="@dimen/home_conversation_compose_voice_size"
android:layout_gravity="center">
@@ -472,6 +464,8 @@
android:importantForAccessibility="no"
android:visibility="gone"
app:contactIconSize="small"
app:reveal="true" />
@@ -490,15 +484,15 @@
android:contentDescription="@string/sendButtonContentDescription"
android:padding="10dp"
android:padding="5dp"
android:scaleType="centerInside"
android:scaleType="fitCenter"
android:src="@drawable/ic_send_light"
android:src="@drawable/ic_home_send_up"
android:visibility="gone"
app:tint="@android:color/white" />
app:tint="@color/home_accent_soft" />
</FrameLayout>
@@ -197,6 +197,72 @@
app:srcCompat="@drawable/ic_checkmark_circle_primary" />
</FrameLayout>
<com.android.messaging.ui.home.HomeCategoryStrip
android:id="@+id/home_category_strip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingTop="@dimen/looply_space_md"
android:paddingEnd="12dp"
android:paddingBottom="@dimen/looply_space_md">
<FrameLayout
android:id="@+id/home_category_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<FrameLayout
android:id="@+id/home_category_scroll_host"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="true"
android:clipToPadding="true">
<com.android.messaging.ui.home.HomeCategoryScroller
android:id="@+id/home_category_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="true"
android:fillViewport="false"
android:overScrollMode="ifContentScrolls"
android:scrollbars="none">
<LinearLayout
android:id="@+id/home_category_row"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal" />
</com.android.messaging.ui.home.HomeCategoryScroller>
<View
android:id="@+id/home_category_fade_start"
android:layout_width="@dimen/home_category_fade_width"
android:layout_height="36dp"
android:layout_gravity="start|center_vertical"
android:background="@drawable/home_bg_category_fade_start"
android:clickable="false"
android:focusable="false"
android:visibility="gone" />
<View
android:id="@+id/home_category_fade_end"
android:layout_width="@dimen/home_category_fade_width"
android:layout_height="36dp"
android:layout_gravity="end|center_vertical"
android:background="@drawable/home_bg_category_fade_end"
android:clickable="false"
android:focusable="false"
android:visibility="gone" />
</FrameLayout>
<include
android:id="@+id/home_category_add"
layout="@layout/home_category_add_tab"
android:layout_width="32dp"
android:layout_height="32dp" />
</com.android.messaging.ui.home.HomeCategoryStrip>
</LinearLayout>
<FrameLayout
@@ -274,8 +274,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:text="@string/home_chip_personal"
android:textSize="11sp"
android:layout_marginTop="2dp"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Circled plus used by home chrome, matching Home.CloseButton. -->
<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Home.AddButton"
android:contentDescription="@string/home_category_add_content_description"
app:srcCompat="@drawable/ic_home_automation_plus"
app:tint="@color/home_add_icon_tint" />
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_bottom_sheet"
android:orientation="vertical"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingTop="@dimen/looply_space_sm"
android:paddingEnd="@dimen/looply_screen_padding"
android:paddingBottom="@dimen/looply_space_xl">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="36dp">
<View
android:layout_width="@dimen/home_gallery_handle_width"
android:layout_height="@dimen/home_gallery_handle_height"
android:layout_gravity="center"
android:background="@drawable/home_bg_gallery_handle" />
<include
android:id="@+id/home_sheet_close"
layout="@layout/home_close_button"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="end|center_vertical" />
</FrameLayout>
<TextView
android:id="@+id/home_sheet_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_sm"
android:includeFontPadding="false"
android:textColor="@color/home_text_primary"
android:textSize="18sp"
android:textStyle="bold"
android:visibility="gone" />
<FrameLayout
android:id="@+id/home_sheet_custom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_md" />
</LinearLayout>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/looply_space_lg"
android:paddingTop="@dimen/looply_space_sm"
android:paddingEnd="@dimen/looply_space_lg">
<TextView
android:id="@+id/home_category_add_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_category_suggested_subtitle"
android:textColor="@color/home_text_secondary"
android:textSize="14sp" />
<LinearLayout
android:id="@+id/home_category_suggest_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_md"
android:orientation="vertical" />
<TextView
android:id="@+id/home_category_add_manually"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:paddingTop="@dimen/looply_space_lg"
android:paddingBottom="@dimen/looply_space_sm"
android:text="@string/home_category_add_manually"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
</LinearLayout>
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/looply_space_xl"
android:paddingTop="@dimen/looply_space_sm"
android:paddingEnd="@dimen/looply_space_xl"
android:paddingBottom="@dimen/looply_space_lg">
<View
android:layout_width="@dimen/home_gallery_handle_width"
android:layout_height="@dimen/home_gallery_handle_height"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="@dimen/looply_space_md"
android:background="@drawable/home_bg_gallery_handle" />
<TextView
android:id="@+id/home_category_sheet_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:text="@string/home_category_add"
android:textColor="@color/home_text_primary"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/home_category_sheet_suggest_page"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/home_category_add_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_sm"
android:text="@string/home_category_suggested_subtitle"
android:textColor="@color/home_text_secondary"
android:textSize="14sp" />
<LinearLayout
android:id="@+id/home_category_suggest_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_md"
android:orientation="vertical" />
<TextView
android:id="@+id/home_category_add_manually"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:paddingTop="@dimen/looply_space_lg"
android:paddingBottom="@dimen/looply_space_sm"
android:text="@string/home_category_add_manually"
android:textColor="@color/home_accent"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/home_category_sheet_name_page"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<EditText
android:id="@+id/home_category_name_field"
style="@style/Home.ModalInput"
android:layout_marginTop="@dimen/looply_space_lg"
android:hint="@string/home_category_name_hint"
android:imeOptions="actionDone"
android:inputType="textCapWords"
android:maxLength="24"
android:maxLines="1" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_md"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/home_category_sheet_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/looply_space_sm"
android:text="@android:string/cancel"
android:textAllCaps="false"
android:textColor="@color/home_text_secondary"
android:textSize="16sp" />
<TextView
android:id="@+id/home_category_sheet_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_md"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/looply_space_sm"
android:text="@string/home_category_add_action"
android:textColor="@color/home_accent"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Home.AddButton"
android:layout_marginTop="6dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="8dp"
android:contentDescription="@string/home_category_add_content_description"
app:srcCompat="@drawable/ic_home_automation_plus"
app:tint="@color/home_add_icon_tint" />
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/home_category_assign_page"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.chip.ChipGroup
android:id="@+id/home_category_assign_chips"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
app:chipSpacingHorizontal="@dimen/looply_space_sm"
app:chipSpacingVertical="@dimen/looply_space_sm"
app:singleSelection="false" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/home_category_name_field"
style="@style/Home.ModalInput"
android:hint="@string/home_category_name_hint"
android:imeOptions="actionDone"
android:inputType="textCapWords"
android:maxLength="24"
android:maxLines="1" />
</LinearLayout>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<RadioButton
android:id="@+id/home_category_suggest_radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:buttonTint="@color/home_accent"
android:clickable="false"
android:focusable="false" />
<TextView
android:id="@+id/home_category_suggest_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_sm"
android:layout_weight="1"
android:includeFontPadding="false"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
</LinearLayout>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="center_horizontal"
android:minHeight="36dp"
android:orientation="vertical"
android:paddingStart="2dp"
android:paddingEnd="14dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/home_category_tab_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
<View
android:id="@+id/home_category_tab_dot"
android:layout_width="6dp"
android:layout_height="6dp"
android:layout_marginStart="5dp"
android:background="@drawable/home_bg_unread_dot"
android:visibility="gone" />
</LinearLayout>
<View
android:id="@+id/home_category_tab_underline"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/home_accent"
android:visibility="invisible" />
</LinearLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Circled X used by home modal, bottom sheet, and sheet-style headers. -->
<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Home.CloseButton"
android:contentDescription="@string/home_sub_header_close"
app:srcCompat="@drawable/ic_home_close"
app:tint="@color/home_accent" />
@@ -19,12 +19,14 @@
<include layout="@layout/home_sub_header" />
<LinearLayout
android:id="@+id/home_compose_recipient_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_md"
android:layout_marginTop="@dimen/looply_space_sm"
android:layout_marginEnd="@dimen/looply_space_md"
android:background="@drawable/home_bg_input_field"
android:addStatesFromChildren="true"
android:background="@drawable/home_bg_search_field_box"
android:gravity="center_vertical"
android:minHeight="@dimen/home_compose_field_height"
android:orientation="horizontal"
@@ -34,6 +36,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:includeFontPadding="false"
android:text="@string/home_compose_recipient_label"
android:textColor="@color/home_text_secondary"
@@ -41,6 +44,7 @@
<EditText
android:id="@+id/home_compose_recipient"
style="@style/Home.EditText"
android:layout_width="0dp"
android:layout_height="@dimen/home_compose_field_height"
android:layout_marginStart="@dimen/looply_space_sm"
@@ -61,6 +65,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_sm"
android:layout_weight="1"
android:focusable="false"
android:focusableInTouchMode="false"
android:paddingTop="@dimen/looply_space_xs"
android:paddingBottom="@dimen/looply_space_xs"
android:scrollbars="none"
@@ -79,6 +85,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_sm"
android:focusable="false"
android:includeFontPadding="false"
android:text="@string/home_compose_add_people"
android:textColor="@color/home_search_hint"
@@ -4,19 +4,21 @@
android:id="@+id/home_compose_attach_compose"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/home_bg"
android:background="@android:color/transparent"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layout_marginBottom="@dimen/looply_space_sm"
android:paddingStart="@dimen/looply_space_md"
android:paddingTop="@dimen/looply_space_sm"
android:paddingEnd="@dimen/looply_space_md"
android:paddingBottom="@dimen/looply_space_md"
android:paddingBottom="@dimen/looply_space_xl"
android:visibility="gone">
<LinearLayout
android:layout_width="0dp"
android:layout_height="@dimen/home_conversation_compose_pill_height"
android:layout_weight="1"
android:addStatesFromChildren="true"
android:background="@drawable/home_bg_compose_pill"
android:gravity="center_vertical"
android:orientation="horizontal"
@@ -29,6 +31,7 @@
android:layout_height="36dp"
android:background="@android:color/transparent"
android:contentDescription="@string/home_compose_emoji_content_description"
android:focusable="false"
android:padding="6dp"
android:scaleType="centerInside"
app:srcCompat="@drawable/ic_home_emoji"
@@ -51,6 +54,7 @@
android:paddingEnd="@dimen/looply_space_xs"
android:textColor="@color/home_text_primary"
android:textColorHint="@color/home_text_secondary"
android:textCursorDrawable="@drawable/home_cursor"
android:textSize="16sp" />
</LinearLayout>
@@ -61,8 +65,8 @@
android:layout_marginStart="@dimen/looply_space_sm"
android:background="@drawable/home_bg_compose_send_circle"
android:contentDescription="@string/sendButtonContentDescription"
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_send_light"
app:tint="@android:color/white" />
android:padding="4dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_home_send_up"
app:tint="@color/home_accent_soft" />
</LinearLayout>
@@ -8,14 +8,16 @@
android:alpha="0"
android:background="@drawable/home_bg_gallery_date_pill"
android:clickable="true"
android:clipToOutline="true"
android:elevation="8dp"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:outlineAmbientShadowColor="@color/home_pill_shadow_ambient"
android:outlineProvider="background"
android:outlineSpotShadowColor="@color/home_pill_shadow_spot"
android:paddingStart="14dp"
android:paddingEnd="6dp"
android:paddingEnd="2dp"
android:visibility="gone">
<TextView
@@ -28,11 +30,12 @@
android:textStyle="bold" />
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="6dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="8dp"
android:background="@drawable/home_bg_gallery_fast_scroll"
android:contentDescription="@null"
android:padding="4dp"
android:src="@drawable/ic_home_scroll_handle" />
android:padding="6dp"
android:src="@drawable/ic_home_scroll_handle"
android:tint="@color/home_accent_soft" />
</LinearLayout>
@@ -15,7 +15,8 @@
android:layout_marginStart="@dimen/looply_space_md"
android:layout_marginTop="@dimen/looply_space_sm"
android:layout_marginEnd="@dimen/looply_space_md"
android:background="@drawable/home_bg_search_field_inner"
android:addStatesFromChildren="true"
android:background="@drawable/home_bg_search_field_box"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_md"
@@ -44,6 +45,7 @@
android:maxLines="1"
android:textColor="@color/home_text_primary"
android:textColorHint="@color/home_search_hint"
android:textCursorDrawable="@drawable/home_cursor"
android:textSize="16sp" />
<ImageButton
@@ -52,6 +54,7 @@
android:layout_height="32dp"
android:background="@android:color/transparent"
android:contentDescription="@string/home_search_clear"
android:focusable="false"
android:padding="6dp"
android:scaleType="centerInside"
android:visibility="gone"
+111
View File
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_modal"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingEnd="12dp">
<include
android:id="@+id/home_modal_close"
layout="@layout/home_close_button"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="end" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingStart="@dimen/looply_space_xl"
android:paddingTop="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_xl"
android:paddingBottom="@dimen/looply_space_xl">
<FrameLayout
android:id="@+id/home_modal_icon_well"
android:layout_width="@dimen/home_modal_icon_well"
android:layout_height="@dimen/home_modal_icon_well"
android:layout_marginBottom="@dimen/looply_space_lg"
android:background="@drawable/home_bg_modal_icon"
android:visibility="gone">
<ImageView
android:id="@+id/home_modal_icon"
android:layout_width="@dimen/home_modal_icon_size"
android:layout_height="@dimen/home_modal_icon_size"
android:layout_gravity="center"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:tint="@color/home_accent" />
</FrameLayout>
<TextView
android:id="@+id/home_modal_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:textColor="@color/home_text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/home_modal_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_sm"
android:gravity="center"
android:includeFontPadding="false"
android:lineSpacingExtra="3dp"
android:textColor="@color/home_text_secondary"
android:textSize="14sp"
android:visibility="gone" />
<FrameLayout
android:id="@+id/home_modal_custom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_lg"
android:visibility="gone" />
<LinearLayout
android:id="@+id/home_modal_actions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_xl"
android:orientation="vertical">
<TextView
android:id="@+id/home_modal_positive"
android:layout_width="match_parent"
android:layout_height="@dimen/home_modal_button_height"
android:background="@drawable/home_bg_modal_primary"
android:gravity="center"
android:includeFontPadding="false"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold"
android:visibility="gone" />
<TextView
android:id="@+id/home_modal_negative"
android:layout_width="match_parent"
android:layout_height="@dimen/home_modal_button_height"
android:layout_marginTop="@dimen/looply_space_sm"
android:background="@drawable/home_bg_modal_cancel"
android:gravity="center"
android:includeFontPadding="false"
android:textColor="@color/home_text_primary"
android:textSize="15sp"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="260dp"
android:layout_width="@dimen/home_popover_width"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_popover"
android:elevation="12dp"
@@ -34,7 +34,7 @@
</LinearLayout>
<LinearLayout
android:layout_width="248dp"
android:layout_width="@dimen/home_popover_width"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/home_bg_popover"
@@ -113,6 +113,29 @@
android:text="@string/home_conversation_hide_alerts" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_conversation_category"
style="@style/Home.PopoverItem"
android:layout_width="match_parent"
android:layout_height="44dp">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_category"
android:tint="@color/home_text_primary" />
<TextView
android:id="@+id/popover_conversation_category_label"
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_category_assign" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_conversation_delete"
style="@style/Home.PopoverItem"
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="220dp"
android:layout_width="@dimen/home_popover_width"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_popover"
android:elevation="12dp"
+2 -24
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Edit popover (Select Messages / Edit Pinned / Name and Photo) -->
<!-- Edit popover (Select Messages / Edit Pinned) -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="260dp"
android:layout_width="@dimen/home_popover_width"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_popover"
android:elevation="12dp"
@@ -53,26 +53,4 @@
android:layout_weight="1"
android:text="@string/home_popover_edit_pinned" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_name_photo"
style="@style/Home.PopoverItem"
android:layout_width="match_parent"
android:layout_height="44dp">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_menu_person"
android:tint="@color/home_text_primary" />
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_popover_name_photo" />
</LinearLayout>
</LinearLayout>
@@ -2,7 +2,7 @@
<!-- Filter popover (Messages / Spam / Archive + Filter By) -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="260dp"
android:layout_width="@dimen/home_popover_width"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_popover"
android:elevation="12dp"
@@ -29,8 +29,8 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/home_search_field_height"
android:background="@drawable/home_bg_search_field_inner"
android:elevation="2dp"
android:addStatesFromChildren="true"
android:background="@drawable/home_bg_search_field_box"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_md"
@@ -59,6 +59,7 @@
android:maxLines="1"
android:textColor="@color/home_text_primary"
android:textColorHint="@color/home_search_hint"
android:textCursorDrawable="@drawable/home_cursor"
android:textSize="15sp" />
<ImageButton
@@ -67,6 +68,7 @@
android:layout_height="36dp"
android:background="@android:color/transparent"
android:contentDescription="@string/home_search_clear"
android:focusable="false"
android:padding="8dp"
android:scaleType="centerInside"
android:src="@drawable/ic_home_close"
@@ -79,6 +81,7 @@
android:layout_height="36dp"
android:background="@android:color/transparent"
android:contentDescription="@string/home_search_voice"
android:focusable="false"
android:padding="6dp"
android:scaleType="centerInside"
android:src="@drawable/ic_home_mic"
+6 -10
View File
@@ -18,17 +18,13 @@
style="@style/Home.SubHeaderTitle" />
<!-- Shown instead of the back arrow when the screen is presented as a sheet. -->
<ImageButton
<include
android:id="@+id/home_sub_header_close"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@android:color/transparent"
android:contentDescription="@string/home_sub_header_close"
android:padding="9dp"
android:scaleType="centerInside"
android:visibility="gone"
app:srcCompat="@drawable/ic_home_close"
app:tint="@color/home_text_primary" />
layout="@layout/home_close_button"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="4dp"
android:visibility="gone" />
<ImageButton
android:id="@+id/home_sub_header_overflow"
+1
View File
@@ -9,6 +9,7 @@
<color name="home_surface">#F2F2F7</color>
<color name="home_surface_elevated">#E5E5EA</color>
<color name="home_surface_translucent">#E8FFFFFF</color>
<color name="home_date_pill_blur">#CCFFFFFF</color>
<color name="home_popover_bg">#F2FFFFFF</color>
<color name="home_button_stroke">#14000000</color>
<!-- Elevation + drawable spread (keep alphas low for a soft float) -->
+84 -4
View File
@@ -10,6 +10,36 @@
<item name="android:scaleType">centerInside</item>
</style>
<style name="Home.EditText" parent="Widget.AppCompat.EditText">
<item name="android:textCursorDrawable">@drawable/home_cursor</item>
</style>
<style name="Home.ModalInput" parent="Home.EditText">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">@dimen/looply_input_height</item>
<item name="android:background">@drawable/home_bg_modal_input</item>
<item name="android:gravity">center_vertical</item>
<item name="android:includeFontPadding">false</item>
<item name="android:paddingStart">@dimen/looply_space_lg</item>
<item name="android:paddingEnd">@dimen/looply_space_lg</item>
<item name="android:textColor">@color/home_text_primary</item>
<item name="android:textColorHint">@color/home_text_secondary</item>
<item name="android:textSize">16sp</item>
</style>
<style name="Home.CloseButton">
<item name="android:layout_width">32dp</item>
<item name="android:layout_height">32dp</item>
<item name="android:background">@drawable/home_bg_close_circle</item>
<item name="android:padding">8dp</item>
<item name="android:scaleType">centerInside</item>
</style>
<style name="Home.AddButton" parent="Home.CloseButton">
<item name="android:background">@drawable/home_bg_add_circle</item>
<item name="android:tint">@color/home_add_icon_tint</item>
</style>
<style name="Home.SubHeaderTitle">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
@@ -76,8 +106,19 @@
<style name="Home.AlertDialog" parent="@style/ThemeOverlay.MaterialComponents.Dialog.Alert">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:backgroundDimAmount">0.35</item>
<item name="android:windowCloseOnTouchOutside">true</item>
<item name="android:windowAnimationStyle">@style/Home.AlertDialog.Animation</item>
<item name="editTextStyle">@style/Home.EditText</item>
<item name="android:editTextStyle">@style/Home.EditText</item>
</style>
<style name="Home.AlertDialog.Animation">
<item name="android:windowEnterAnimation">@anim/home_modal_enter</item>
<item name="android:windowExitAnimation">@anim/home_modal_exit</item>
</style>
<style name="Home.SubScreen" parent="@style/BugleTheme">
<item name="editTextStyle">@style/Home.EditText</item>
<item name="android:editTextStyle">@style/Home.EditText</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowBackground">@color/home_bg</item>
@@ -141,12 +182,51 @@
<item name="dialogCornerRadius">28dp</item>
</style>
<style name="Home.BottomSheetDialog" parent="@style/ThemeOverlay.MaterialComponents.BottomSheetDialog">
<item name="colorPrimary">@color/home_accent</item>
<item name="colorAccent">@color/home_accent</item>
<item name="android:textColorPrimary">@color/home_text_primary</item>
<item name="android:textColorSecondary">@color/home_text_secondary</item>
<item name="bottomSheetStyle">@style/Home.ModalBottomSheet</item>
<item name="android:windowCloseOnTouchOutside">true</item>
</style>
<style name="Home.ModalBottomSheet" parent="Widget.MaterialComponents.BottomSheet.Modal">
<item name="backgroundTint">@color/home_bg</item>
<item name="shapeAppearanceOverlay">@style/Home.SheetCorners</item>
</style>
<style name="Home.SheetCorners">
<item name="cornerFamily">rounded</item>
<item name="cornerSizeTopLeft">@dimen/looply_radius_xl</item>
<item name="cornerSizeTopRight">@dimen/looply_radius_xl</item>
<item name="cornerSizeBottomLeft">0dp</item>
<item name="cornerSizeBottomRight">0dp</item>
</style>
<style name="Home.CategoryChip" parent="Widget.MaterialComponents.Chip.Filter">
<item name="chipBackgroundColor">@color/home_category_chip_bg</item>
<item name="chipStrokeColor">@color/home_category_chip_stroke</item>
<item name="chipStrokeWidth">1dp</item>
<item name="android:textColor">@color/home_category_chip_text</item>
<item name="checkedIconVisible">false</item>
<item name="chipMinHeight">36dp</item>
<item name="chipStartPadding">12dp</item>
<item name="chipEndPadding">12dp</item>
<item name="android:textSize">14sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="Home.SimBadge" parent="Home.Chip">
<item name="android:background">@drawable/home_bg_sim_badge</item>
<item name="android:textColor">@color/home_sim_badge_text</item>
<item name="android:minHeight">18dp</item>
<item name="android:paddingTop">3dp</item>
<item name="android:paddingBottom">3dp</item>
<item name="android:includeFontPadding">true</item>
<item name="android:textSize">9sp</item>
<item name="android:minHeight">14dp</item>
<item name="android:paddingStart">4dp</item>
<item name="android:paddingEnd">4dp</item>
<item name="android:paddingTop">0dp</item>
<item name="android:paddingBottom">0dp</item>
<item name="android:includeFontPadding">false</item>
<item name="android:gravity">center</item>
</style>
</resources>
@@ -16,6 +16,8 @@
<dimen name="looply_space_xl">20dp</dimen>
<dimen name="looply_space_xxl">24dp</dimen>
<dimen name="looply_screen_padding">20dp</dimen>
<dimen name="home_popover_width">260dp</dimen>
<dimen name="home_category_fade_width">32dp</dimen>
<dimen name="home_empty_dock_padding">104dp</dimen>
<!-- Components -->
@@ -106,6 +108,9 @@
<dimen name="home_nav_item_padding">8dp</dimen>
<dimen name="home_nav_dock_padding_bottom">14dp</dimen>
<dimen name="home_alert_dialog_horizontal_margin">28dp</dimen>
<dimen name="home_modal_icon_well">56dp</dimen>
<dimen name="home_modal_icon_size">24dp</dimen>
<dimen name="home_modal_button_height">48dp</dimen>
<dimen name="home_search_filter_height">88dp</dimen>
<dimen name="home_search_filter_icon_size">22dp</dimen>
<dimen name="home_search_field_height">48dp</dimen>
+31 -1
View File
@@ -143,7 +143,6 @@
<string name="home_popover_settings" translatable="false">Settings</string>
<string name="home_popover_select_messages" translatable="false">Select Messages</string>
<string name="home_popover_edit_pinned" translatable="false">Edit Pinned</string>
<string name="home_popover_name_photo" translatable="false">Name and Photo Settings</string>
<string name="home_popover_coming_soon" translatable="false">Coming soon</string>
<string name="home_pinned_dialog_title" translatable="false">Pinned Conversations</string>
@@ -300,4 +299,35 @@
<string name="home_requires_default_sms_app" translatable="false">Set Messages as the default SMS app</string>
<string name="home_requires_default_sms_app_to_send" translatable="false">To send, set Messages as the default SMS app</string>
<string name="home_requires_default_sms_change" translatable="false">Change</string>
<!-- Home conversation categories -->
<string name="home_category_all" translatable="false">All</string>
<string name="home_category_personal" translatable="false">Personal</string>
<string name="home_category_promotion" translatable="false">Promotion</string>
<string name="home_category_add" translatable="false">Add category</string>
<string name="home_category_add_new" translatable="false">+ Add new</string>
<string name="home_category_add_content_description" translatable="false">Add category</string>
<string name="home_category_suggested_subtitle" translatable="false">Suggested categories based on your conversations</string>
<string name="home_category_add_manually" translatable="false">Add manually</string>
<string name="home_category_name_hint" translatable="false">Category name</string>
<string name="home_category_add_action" translatable="false">Add</string>
<string name="home_category_suggest_verification" translatable="false">Verification</string>
<string name="home_category_suggest_count" translatable="false">%1$s (%2$d)</string>
<string name="home_category_empty_title" translatable="false">No conversations</string>
<string name="home_category_empty_body" translatable="false">Long-press a chat and choose Add to category.</string>
<string name="home_category_added_toast" translatable="false">Category added</string>
<string name="home_category_exists_toast" translatable="false">That category already exists</string>
<string name="home_category_limit_toast" translatable="false">You can add up to 12 categories</string>
<string name="home_category_delete_title" translatable="false">Delete category?</string>
<string name="home_category_delete_message" translatable="false">Conversations stay in your inbox. Only this category is removed.</string>
<string name="home_category_assign" translatable="false">Add to category</string>
<string name="home_category_assign_message" translatable="false">Select one or more categories.</string>
<string name="home_category_name_message" translatable="false">Enter a category name.</string>
<string name="home_category_done" translatable="false">Confirm</string>
<string name="home_category_updated_toast" translatable="false">Categories updated</string>
<string name="home_category_assigned_toast" translatable="false">Added to %1$s</string>
<string name="home_category_already_assigned_toast" translatable="false">Already in %1$s</string>
<string name="home_category_none_toast" translatable="false">Add a category first</string>
<string name="home_category_remove_from" translatable="false">Remove from %1$s</string>
<string name="home_category_removed_toast" translatable="false">Removed from %1$s</string>
</resources>
+3
View File
@@ -58,6 +58,9 @@
<item name="android:navigationBarColor">@color/home_bg</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="android:forceDarkAllowed">false</item>
<item name="editTextStyle">@style/Home.EditText</item>
<item name="android:editTextStyle">@style/Home.EditText</item>
<item name="colorAccent">@color/home_accent</item>
</style>
<style name="BugleTheme.ConversationActivityBase" parent="BugleTheme">