From 42148360e014c8a2cd4679ad5ec9ff71695d9d62 Mon Sep 17 00:00:00 2001 From: "MINIPC\\frivaong" Date: Sun, 27 Sep 2026 02:41:59 +0900 Subject: [PATCH] Messaging: Add home categories and shared modal/input chrome Co-authored-by: Cursor --- .../messaging/ui/BugleActionBarActivity.java | 10 + .../ui/conversation/ComposeMessageView.java | 58 ++-- .../ui/conversation/ConversationFragment.java | 10 +- .../ConversationListAdapter.java | 56 +++- .../ConversationListFragment.java | 255 +++++++++++++++- .../ConversationListItemView.java | 53 ++-- .../messaging/ui/home/HomeBottomSheet.java | 100 +++++++ .../ui/home/HomeCategoryAddModal.java | 80 ++++++ .../ui/home/HomeCategoryAssignSheet.java | 134 +++++++++ .../ui/home/HomeCategoryClassifier.java | 149 ++++++++++ .../ui/home/HomeCategoryScroller.java | 152 ++++++++++ .../messaging/ui/home/HomeCategoryStore.java | 256 +++++++++++++++++ .../messaging/ui/home/HomeCategoryStrip.java | 112 ++++++++ .../ui/home/HomeComposeActivity.java | 10 + .../ui/home/HomeDeleteAlertDialog.java | 1 - .../messaging/ui/home/HomeInputFocus.java | 88 ++++++ .../android/messaging/ui/home/HomeModal.java | 271 ++++++++++++++++++ .../messaging/ui/home/HomeUiMotion.java | 146 +++++++++- app/src/main/res/anim/home_modal_enter.xml | 17 ++ app/src/main/res/anim/home_modal_exit.xml | 17 ++ app/src/main/res/color/home_add_icon_tint.xml | 7 + .../main/res/color/home_category_chip_bg.xml | 5 + .../res/color/home_category_chip_stroke.xml | 5 + .../res/color/home_category_chip_text.xml | 5 + .../main/res/drawable/home_bg_add_circle.xml | 12 + .../drawable/home_bg_add_circle_focused.xml | 5 + .../res/drawable/home_bg_add_circle_idle.xml | 5 + .../drawable/home_bg_category_fade_end.xml | 7 + .../drawable/home_bg_category_fade_start.xml | 7 + .../res/drawable/home_bg_close_circle.xml | 9 + .../res/drawable/home_bg_compose_pill.xml | 11 +- .../drawable/home_bg_gallery_date_pill.xml | 2 +- .../drawable/home_bg_gallery_fast_scroll.xml | 5 +- app/src/main/res/drawable/home_bg_modal.xml | 6 + .../res/drawable/home_bg_modal_cancel.xml | 10 + .../main/res/drawable/home_bg_modal_icon.xml | 6 + .../main/res/drawable/home_bg_modal_input.xml | 7 + .../drawable/home_bg_modal_input_focused.xml | 9 + .../res/drawable/home_bg_modal_input_idle.xml | 9 + .../res/drawable/home_bg_modal_primary.xml | 10 + .../res/drawable/home_bg_search_field_box.xml | 7 + .../drawable/home_bg_search_field_focused.xml | 9 + .../drawable/home_bg_search_field_idle.xml | 9 + app/src/main/res/drawable/home_cursor.xml | 6 + .../main/res/drawable/ic_home_category.xml | 16 ++ .../res/drawable/ic_home_scroll_handle.xml | 4 +- app/src/main/res/drawable/ic_home_send_up.xml | 21 ++ .../main/res/layout/compose_message_view.xml | 54 ++-- .../res/layout/conversation_list_fragment.xml | 66 +++++ .../layout/conversation_list_item_view.xml | 3 +- app/src/main/res/layout/home_add_button.xml | 8 + app/src/main/res/layout/home_bottom_sheet.xml | 47 +++ .../res/layout/home_category_add_dialog.xml | 36 +++ .../res/layout/home_category_add_sheet.xml | 111 +++++++ .../main/res/layout/home_category_add_tab.xml | 10 + .../res/layout/home_category_assign_sheet.xml | 24 ++ .../res/layout/home_category_name_dialog.xml | 15 + .../res/layout/home_category_suggest_item.xml | 26 ++ app/src/main/res/layout/home_category_tab.xml | 43 +++ app/src/main/res/layout/home_close_button.xml | 8 + .../main/res/layout/home_compose_activity.xml | 9 +- .../home_compose_attach_compose_bar.xml | 16 +- .../home_compose_attach_fast_scroll.xml | 15 +- .../home_conversation_search_activity.xml | 5 +- app/src/main/res/layout/home_modal.xml | 111 +++++++ .../main/res/layout/home_popover_album.xml | 2 +- .../res/layout/home_popover_conversation.xml | 25 +- .../home_popover_conversation_overflow.xml | 2 +- app/src/main/res/layout/home_popover_edit.xml | 26 +- .../main/res/layout/home_popover_filter.xml | 2 +- .../main/res/layout/home_search_activity.xml | 7 +- app/src/main/res/layout/home_sub_header.xml | 16 +- app/src/main/res/values/home_colors.xml | 1 + app/src/main/res/values/home_styles.xml | 88 +++++- app/src/main/res/values/looply_dimens.xml | 5 + app/src/main/res/values/looply_strings.xml | 32 ++- app/src/main/res/values/styles.xml | 3 + 77 files changed, 2823 insertions(+), 182 deletions(-) create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeBottomSheet.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeCategoryAddModal.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeCategoryAssignSheet.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeCategoryClassifier.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeCategoryScroller.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeCategoryStore.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeCategoryStrip.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeInputFocus.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeModal.java create mode 100644 app/src/main/res/anim/home_modal_enter.xml create mode 100644 app/src/main/res/anim/home_modal_exit.xml create mode 100644 app/src/main/res/color/home_add_icon_tint.xml create mode 100644 app/src/main/res/color/home_category_chip_bg.xml create mode 100644 app/src/main/res/color/home_category_chip_stroke.xml create mode 100644 app/src/main/res/color/home_category_chip_text.xml create mode 100644 app/src/main/res/drawable/home_bg_add_circle.xml create mode 100644 app/src/main/res/drawable/home_bg_add_circle_focused.xml create mode 100644 app/src/main/res/drawable/home_bg_add_circle_idle.xml create mode 100644 app/src/main/res/drawable/home_bg_category_fade_end.xml create mode 100644 app/src/main/res/drawable/home_bg_category_fade_start.xml create mode 100644 app/src/main/res/drawable/home_bg_close_circle.xml create mode 100644 app/src/main/res/drawable/home_bg_modal.xml create mode 100644 app/src/main/res/drawable/home_bg_modal_cancel.xml create mode 100644 app/src/main/res/drawable/home_bg_modal_icon.xml create mode 100644 app/src/main/res/drawable/home_bg_modal_input.xml create mode 100644 app/src/main/res/drawable/home_bg_modal_input_focused.xml create mode 100644 app/src/main/res/drawable/home_bg_modal_input_idle.xml create mode 100644 app/src/main/res/drawable/home_bg_modal_primary.xml create mode 100644 app/src/main/res/drawable/home_bg_search_field_box.xml create mode 100644 app/src/main/res/drawable/home_bg_search_field_focused.xml create mode 100644 app/src/main/res/drawable/home_bg_search_field_idle.xml create mode 100644 app/src/main/res/drawable/home_cursor.xml create mode 100644 app/src/main/res/drawable/ic_home_category.xml create mode 100644 app/src/main/res/drawable/ic_home_send_up.xml create mode 100644 app/src/main/res/layout/home_add_button.xml create mode 100644 app/src/main/res/layout/home_bottom_sheet.xml create mode 100644 app/src/main/res/layout/home_category_add_dialog.xml create mode 100644 app/src/main/res/layout/home_category_add_sheet.xml create mode 100644 app/src/main/res/layout/home_category_add_tab.xml create mode 100644 app/src/main/res/layout/home_category_assign_sheet.xml create mode 100644 app/src/main/res/layout/home_category_name_dialog.xml create mode 100644 app/src/main/res/layout/home_category_suggest_item.xml create mode 100644 app/src/main/res/layout/home_category_tab.xml create mode 100644 app/src/main/res/layout/home_close_button.xml create mode 100644 app/src/main/res/layout/home_modal.xml diff --git a/app/src/main/java/com/android/messaging/ui/BugleActionBarActivity.java b/app/src/main/java/com/android/messaging/ui/BugleActionBarActivity.java index f959340..1afe232 100644 --- a/app/src/main/java/com/android/messaging/ui/BugleActionBarActivity.java +++ b/app/src/main/java/com/android/messaging/ui/BugleActionBarActivity.java @@ -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(); diff --git a/app/src/main/java/com/android/messaging/ui/conversation/ComposeMessageView.java b/app/src/main/java/com/android/messaging/ui/conversation/ComposeMessageView.java index 4e9cfcc..3227b07 100644 --- a/app/src/main/java/com/android/messaging/ui/conversation/ComposeMessageView.java +++ b/app/src/main/java/com/android/messaging/ui/conversation/ComposeMessageView.java @@ -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 diff --git a/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java b/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java index 7ed73af..75d446c 100644 --- a/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java +++ b/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java @@ -319,7 +319,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi mScrollToDismissHandled = true; } } - } + } }; private final ActivityResultLauncher 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); diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java index c8d4a93..f09591e 100644 --- a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java +++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java @@ -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 snapshotConversations() { + final Cursor cursor = getCursor(); + if (cursor == null || cursor.getCount() == 0) { + return Collections.emptyList(); + } + final List 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 positions, final Cursor cursor) { diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListFragment.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListFragment.java index ed921a8..632c839 100644 --- a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListFragment.java @@ -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 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 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 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 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, diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java index 80cff01..d7bc8df 100644 --- a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java +++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java @@ -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 diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeBottomSheet.java b/app/src/main/java/com/android/messaging/ui/home/HomeBottomSheet.java new file mode 100644 index 0000000..0c09fd1 --- /dev/null +++ b/app/src/main/java/com/android/messaging/ui/home/HomeBottomSheet.java @@ -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; + } +} diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeCategoryAddModal.java b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryAddModal.java new file mode 100644 index 0000000..014367a --- /dev/null +++ b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryAddModal.java @@ -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 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; + } +} diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeCategoryAssignSheet.java b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryAssignSheet.java new file mode 100644 index 0000000..bd01bc3 --- /dev/null +++ b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryAssignSheet.java @@ -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 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 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 selected) { + final Set 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); + } +} diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeCategoryClassifier.java b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryClassifier.java new file mode 100644 index 0000000..af90913 --- /dev/null +++ b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryClassifier.java @@ -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 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 suggestions(final Context context, + final List items) { + final List 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; + } +} diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeCategoryScroller.java b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryScroller.java new file mode 100644 index 0000000..e8a6a7b --- /dev/null +++ b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryScroller.java @@ -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); + } + } +} diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeCategoryStore.java b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryStore.java new file mode 100644 index 0000000..6349125 --- /dev/null +++ b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryStore.java @@ -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 listTabs(final Context context) { + final List 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 listCustom(final Context context) { + final String raw = prefs(context).getString(KEY_CUSTOM, ""); + if (TextUtils.isEmpty(raw)) { + return Collections.emptyList(); + } + final List 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 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 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 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 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 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 rows = assignmentRows(context); + if (!rows.remove(needle)) { + return; + } + prefs(context).edit().putString(KEY_ASSIGN, TextUtils.join(ROW, rows)).apply(); + } + + public static Set assignedIds(final Context context, + final String conversationId) { + final LinkedHashSet 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 assignmentRows(final Context context) { + final String raw = prefs(context).getString(KEY_ASSIGN, ""); + if (TextUtils.isEmpty(raw)) { + return new ArrayList<>(); + } + final List 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 items) { + final List 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); + } +} diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeCategoryStrip.java b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryStrip.java new file mode 100644 index 0000000..f057513 --- /dev/null +++ b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryStrip.java @@ -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; + } +} diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeComposeActivity.java b/app/src/main/java/com/android/messaging/ui/home/HomeComposeActivity.java index 259d557..caba722 100644 --- a/app/src/main/java/com/android/messaging/ui/home/HomeComposeActivity.java +++ b/app/src/main/java/com/android/messaging/ui/home/HomeComposeActivity.java @@ -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, diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeDeleteAlertDialog.java b/app/src/main/java/com/android/messaging/ui/home/HomeDeleteAlertDialog.java index 5dbe478..8cef22c 100644 --- a/app/src/main/java/com/android/messaging/ui/home/HomeDeleteAlertDialog.java +++ b/app/src/main/java/com/android/messaging/ui/home/HomeDeleteAlertDialog.java @@ -89,6 +89,5 @@ public final class HomeDeleteAlertDialog { WindowManager.LayoutParams.WRAP_CONTENT); window.getDecorView().setPadding(margin, 0, margin, 0); } - HomeUiMotion.dialogEnter(content); } } diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeInputFocus.java b/app/src/main/java/com/android/messaging/ui/home/HomeInputFocus.java new file mode 100644 index 0000000..5021605 --- /dev/null +++ b/app/src/main/java/com/android/messaging/ui/home/HomeInputFocus.java @@ -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); + } + } +} diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeModal.java b/app/src/main/java/com/android/messaging/ui/home/HomeModal.java new file mode 100644 index 0000000..e0a4424 --- /dev/null +++ b/app/src/main/java/com/android/messaging/ui/home/HomeModal.java @@ -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(); + } + }); + } +} diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeUiMotion.java b/app/src/main/java/com/android/messaging/ui/home/HomeUiMotion.java index 3df1dd1..c396d93 100644 --- a/app/src/main/java/com/android/messaging/ui/home/HomeUiMotion.java +++ b/app/src/main/java/com/android/messaging/ui/home/HomeUiMotion.java @@ -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(); + } } diff --git a/app/src/main/res/anim/home_modal_enter.xml b/app/src/main/res/anim/home_modal_enter.xml new file mode 100644 index 0000000..af7c933 --- /dev/null +++ b/app/src/main/res/anim/home_modal_enter.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/anim/home_modal_exit.xml b/app/src/main/res/anim/home_modal_exit.xml new file mode 100644 index 0000000..d5ad23c --- /dev/null +++ b/app/src/main/res/anim/home_modal_exit.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/color/home_add_icon_tint.xml b/app/src/main/res/color/home_add_icon_tint.xml new file mode 100644 index 0000000..f6c2709 --- /dev/null +++ b/app/src/main/res/color/home_add_icon_tint.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/color/home_category_chip_bg.xml b/app/src/main/res/color/home_category_chip_bg.xml new file mode 100644 index 0000000..8d302c0 --- /dev/null +++ b/app/src/main/res/color/home_category_chip_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/home_category_chip_stroke.xml b/app/src/main/res/color/home_category_chip_stroke.xml new file mode 100644 index 0000000..1ffa6dc --- /dev/null +++ b/app/src/main/res/color/home_category_chip_stroke.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/home_category_chip_text.xml b/app/src/main/res/color/home_category_chip_text.xml new file mode 100644 index 0000000..4d963b9 --- /dev/null +++ b/app/src/main/res/color/home_category_chip_text.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/home_bg_add_circle.xml b/app/src/main/res/drawable/home_bg_add_circle.xml new file mode 100644 index 0000000..362c374 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_add_circle.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/home_bg_add_circle_focused.xml b/app/src/main/res/drawable/home_bg_add_circle_focused.xml new file mode 100644 index 0000000..a7b2532 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_add_circle_focused.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/home_bg_add_circle_idle.xml b/app/src/main/res/drawable/home_bg_add_circle_idle.xml new file mode 100644 index 0000000..3ae6505 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_add_circle_idle.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/home_bg_category_fade_end.xml b/app/src/main/res/drawable/home_bg_category_fade_end.xml new file mode 100644 index 0000000..f826527 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_category_fade_end.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/home_bg_category_fade_start.xml b/app/src/main/res/drawable/home_bg_category_fade_start.xml new file mode 100644 index 0000000..8d40e63 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_category_fade_start.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/home_bg_close_circle.xml b/app/src/main/res/drawable/home_bg_close_circle.xml new file mode 100644 index 0000000..1b03472 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_close_circle.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/home_bg_compose_pill.xml b/app/src/main/res/drawable/home_bg_compose_pill.xml index 0d86e84..76e8d40 100644 --- a/app/src/main/res/drawable/home_bg_compose_pill.xml +++ b/app/src/main/res/drawable/home_bg_compose_pill.xml @@ -1,6 +1,7 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/home_bg_gallery_date_pill.xml b/app/src/main/res/drawable/home_bg_gallery_date_pill.xml index dd37225..556797a 100644 --- a/app/src/main/res/drawable/home_bg_gallery_date_pill.xml +++ b/app/src/main/res/drawable/home_bg_gallery_date_pill.xml @@ -2,7 +2,7 @@ - + diff --git a/app/src/main/res/drawable/home_bg_gallery_fast_scroll.xml b/app/src/main/res/drawable/home_bg_gallery_fast_scroll.xml index c52e18a..a7b2532 100644 --- a/app/src/main/res/drawable/home_bg_gallery_fast_scroll.xml +++ b/app/src/main/res/drawable/home_bg_gallery_fast_scroll.xml @@ -1,8 +1,5 @@ - - + diff --git a/app/src/main/res/drawable/home_bg_modal.xml b/app/src/main/res/drawable/home_bg_modal.xml new file mode 100644 index 0000000..6920f19 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_modal.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/home_bg_modal_cancel.xml b/app/src/main/res/drawable/home_bg_modal_cancel.xml new file mode 100644 index 0000000..5b6dd6b --- /dev/null +++ b/app/src/main/res/drawable/home_bg_modal_cancel.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/home_bg_modal_icon.xml b/app/src/main/res/drawable/home_bg_modal_icon.xml new file mode 100644 index 0000000..3d31ceb --- /dev/null +++ b/app/src/main/res/drawable/home_bg_modal_icon.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/home_bg_modal_input.xml b/app/src/main/res/drawable/home_bg_modal_input.xml new file mode 100644 index 0000000..2283798 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_modal_input.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/home_bg_modal_input_focused.xml b/app/src/main/res/drawable/home_bg_modal_input_focused.xml new file mode 100644 index 0000000..5886c00 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_modal_input_focused.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/home_bg_modal_input_idle.xml b/app/src/main/res/drawable/home_bg_modal_input_idle.xml new file mode 100644 index 0000000..7bb59db --- /dev/null +++ b/app/src/main/res/drawable/home_bg_modal_input_idle.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/home_bg_modal_primary.xml b/app/src/main/res/drawable/home_bg_modal_primary.xml new file mode 100644 index 0000000..b8e4e46 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_modal_primary.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/home_bg_search_field_box.xml b/app/src/main/res/drawable/home_bg_search_field_box.xml new file mode 100644 index 0000000..76e8d40 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_search_field_box.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/home_bg_search_field_focused.xml b/app/src/main/res/drawable/home_bg_search_field_focused.xml new file mode 100644 index 0000000..d10c5c8 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_search_field_focused.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/home_bg_search_field_idle.xml b/app/src/main/res/drawable/home_bg_search_field_idle.xml new file mode 100644 index 0000000..2fe2ca0 --- /dev/null +++ b/app/src/main/res/drawable/home_bg_search_field_idle.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/home_cursor.xml b/app/src/main/res/drawable/home_cursor.xml new file mode 100644 index 0000000..c877734 --- /dev/null +++ b/app/src/main/res/drawable/home_cursor.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_home_category.xml b/app/src/main/res/drawable/ic_home_category.xml new file mode 100644 index 0000000..b9f10ad --- /dev/null +++ b/app/src/main/res/drawable/ic_home_category.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_home_scroll_handle.xml b/app/src/main/res/drawable/ic_home_scroll_handle.xml index 6c540ab..a0520ce 100644 --- a/app/src/main/res/drawable/ic_home_scroll_handle.xml +++ b/app/src/main/res/drawable/ic_home_scroll_handle.xml @@ -5,9 +5,9 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_home_send_up.xml b/app/src/main/res/drawable/ic_home_send_up.xml new file mode 100644 index 0000000..2dc673f --- /dev/null +++ b/app/src/main/res/drawable/ic_home_send_up.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/layout/compose_message_view.xml b/app/src/main/res/layout/compose_message_view.xml index 604a6d3..2f4c8de 100644 --- a/app/src/main/res/layout/compose_message_view.xml +++ b/app/src/main/res/layout/compose_message_view.xml @@ -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 @@ - + android:layout_marginStart="@dimen/looply_space_xs" + android:contentDescription="@string/attachMediaButtonContentDescription" + android:focusable="false" - - - + 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" /> diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index 81ffb9a..54a1359 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -197,6 +197,72 @@ app:srcCompat="@drawable/ic_checkmark_circle_primary" /> + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/home_add_button.xml b/app/src/main/res/layout/home_add_button.xml new file mode 100644 index 0000000..3aa681e --- /dev/null +++ b/app/src/main/res/layout/home_add_button.xml @@ -0,0 +1,8 @@ + + + diff --git a/app/src/main/res/layout/home_bottom_sheet.xml b/app/src/main/res/layout/home_bottom_sheet.xml new file mode 100644 index 0000000..0ce94a3 --- /dev/null +++ b/app/src/main/res/layout/home_bottom_sheet.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/home_category_add_dialog.xml b/app/src/main/res/layout/home_category_add_dialog.xml new file mode 100644 index 0000000..843ab44 --- /dev/null +++ b/app/src/main/res/layout/home_category_add_dialog.xml @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/home_category_add_sheet.xml b/app/src/main/res/layout/home_category_add_sheet.xml new file mode 100644 index 0000000..e1e18f6 --- /dev/null +++ b/app/src/main/res/layout/home_category_add_sheet.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/home_category_add_tab.xml b/app/src/main/res/layout/home_category_add_tab.xml new file mode 100644 index 0000000..c999ed6 --- /dev/null +++ b/app/src/main/res/layout/home_category_add_tab.xml @@ -0,0 +1,10 @@ + + diff --git a/app/src/main/res/layout/home_category_assign_sheet.xml b/app/src/main/res/layout/home_category_assign_sheet.xml new file mode 100644 index 0000000..78c2d8b --- /dev/null +++ b/app/src/main/res/layout/home_category_assign_sheet.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/home_category_name_dialog.xml b/app/src/main/res/layout/home_category_name_dialog.xml new file mode 100644 index 0000000..85b1421 --- /dev/null +++ b/app/src/main/res/layout/home_category_name_dialog.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/layout/home_category_suggest_item.xml b/app/src/main/res/layout/home_category_suggest_item.xml new file mode 100644 index 0000000..cfe8434 --- /dev/null +++ b/app/src/main/res/layout/home_category_suggest_item.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/app/src/main/res/layout/home_category_tab.xml b/app/src/main/res/layout/home_category_tab.xml new file mode 100644 index 0000000..00a2476 --- /dev/null +++ b/app/src/main/res/layout/home_category_tab.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/home_close_button.xml b/app/src/main/res/layout/home_close_button.xml new file mode 100644 index 0000000..2115f70 --- /dev/null +++ b/app/src/main/res/layout/home_close_button.xml @@ -0,0 +1,8 @@ + + + diff --git a/app/src/main/res/layout/home_compose_activity.xml b/app/src/main/res/layout/home_compose_activity.xml index a4d37db..16de628 100644 --- a/app/src/main/res/layout/home_compose_activity.xml +++ b/app/src/main/res/layout/home_compose_activity.xml @@ -19,12 +19,14 @@ @@ -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" /> diff --git a/app/src/main/res/layout/home_compose_attach_fast_scroll.xml b/app/src/main/res/layout/home_compose_attach_fast_scroll.xml index 90c6722..de1530f 100644 --- a/app/src/main/res/layout/home_compose_attach_fast_scroll.xml +++ b/app/src/main/res/layout/home_compose_attach_fast_scroll.xml @@ -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"> + android:padding="6dp" + android:src="@drawable/ic_home_scroll_handle" + android:tint="@color/home_accent_soft" /> diff --git a/app/src/main/res/layout/home_conversation_search_activity.xml b/app/src/main/res/layout/home_conversation_search_activity.xml index cb51d69..de29320 100644 --- a/app/src/main/res/layout/home_conversation_search_activity.xml +++ b/app/src/main/res/layout/home_conversation_search_activity.xml @@ -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" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/home_popover_album.xml b/app/src/main/res/layout/home_popover_album.xml index 16c16da..1d7e6f1 100644 --- a/app/src/main/res/layout/home_popover_album.xml +++ b/app/src/main/res/layout/home_popover_album.xml @@ -1,6 +1,6 @@ + + + + + + + - + - - - - - - - diff --git a/app/src/main/res/layout/home_popover_filter.xml b/app/src/main/res/layout/home_popover_filter.xml index fdef67e..1cd5c36 100644 --- a/app/src/main/res/layout/home_popover_filter.xml +++ b/app/src/main/res/layout/home_popover_filter.xml @@ -2,7 +2,7 @@ - + layout="@layout/home_close_button" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_marginStart="4dp" + android:visibility="gone" /> #F2F2F7 #E5E5EA #E8FFFFFF + #CCFFFFFF #F2FFFFFF #14000000 diff --git a/app/src/main/res/values/home_styles.xml b/app/src/main/res/values/home_styles.xml index 9934cda..a404509 100644 --- a/app/src/main/res/values/home_styles.xml +++ b/app/src/main/res/values/home_styles.xml @@ -10,6 +10,36 @@ centerInside + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/looply_dimens.xml b/app/src/main/res/values/looply_dimens.xml index 8ae5132..df079b7 100644 --- a/app/src/main/res/values/looply_dimens.xml +++ b/app/src/main/res/values/looply_dimens.xml @@ -16,6 +16,8 @@ 20dp 24dp 20dp + 260dp + 32dp 104dp @@ -106,6 +108,9 @@ 8dp 14dp 28dp + 56dp + 24dp + 48dp 88dp 22dp 48dp diff --git a/app/src/main/res/values/looply_strings.xml b/app/src/main/res/values/looply_strings.xml index 15bd0d2..0f33e4c 100644 --- a/app/src/main/res/values/looply_strings.xml +++ b/app/src/main/res/values/looply_strings.xml @@ -143,7 +143,6 @@ Settings Select Messages Edit Pinned - Name and Photo Settings Coming soon Pinned Conversations @@ -300,4 +299,35 @@ Set Messages as the default SMS app To send, set Messages as the default SMS app Change + + + All + Personal + Promotion + Add category + + Add new + Add category + Suggested categories based on your conversations + Add manually + Category name + Add + Verification + %1$s (%2$d) + No conversations + Long-press a chat and choose Add to category. + Category added + That category already exists + You can add up to 12 categories + Delete category? + Conversations stay in your inbox. Only this category is removed. + Add to category + Select one or more categories. + Enter a category name. + Confirm + Categories updated + Added to %1$s + Already in %1$s + Add a category first + Remove from %1$s + Removed from %1$s diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 133760b..590c75b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -58,6 +58,9 @@ @color/home_bg true false + @style/Home.EditText + @style/Home.EditText + @color/home_accent