diff --git a/Android.bp b/Android.bp
index 3250281..225d1cb 100644
--- a/Android.bp
+++ b/Android.bp
@@ -88,12 +88,12 @@ android_app {
"--version-code",
"10001040",
"--rename-manifest-package",
- "com.filabcorp.msgq",
+ "com.bnkode.notie",
],
required: [
"libframesequence",
"libgiftranscode",
- "com.filabcorp.msgq.allowlist",
+ "com.bnkode.notie.allowlist",
],
optimize: {
obfuscate: true,
@@ -107,9 +107,9 @@ android_app {
}
prebuilt_etc {
- name: "com.filabcorp.msgq.allowlist",
+ name: "com.bnkode.notie.allowlist",
product_specific: true,
sub_dir: "sysconfig",
- src: "com.filabcorp.msgq.allowlist.xml",
+ src: "com.bnkode.notie.allowlist.xml",
filename_from_src: true,
}
diff --git a/app/build.gradle b/app/build.gradle
index 68ae539..27d88ed 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -13,7 +13,7 @@ android {
compileSdk 35
defaultConfig {
- applicationId 'com.filabcorp.msgq'
+ applicationId 'com.bnkode.notie'
minSdk 33
targetSdk 34
versionCode 10001040
@@ -106,5 +106,5 @@ tasks.register('launchMessaging', Exec) {
description = "Launch Messaging on ${messagingAvdSerial}"
dependsOn 'installMessaging'
commandLine adbExecutable.absolutePath, '-s', messagingAvdSerial, 'shell', 'am', 'start',
- '-n', 'com.filabcorp.msgq/com.android.messaging.ui.conversationlist.ConversationListActivity'
+ '-n', 'com.bnkode.notie/com.android.messaging.ui.conversationlist.ConversationListActivity'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4dac443..097e956 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -398,20 +398,20 @@
@@ -467,18 +467,18 @@
android:name=".receiver.SendStatusReceiver"
android:exported="false">
-
+
-
+
-
+
-
+
@@ -521,7 +521,7 @@
android:name=".receiver.ScheduledMessageReceiver"
android:exported="false">
-
+
@@ -536,7 +536,7 @@
android:name=".datamodel.action.ActionServiceImpl$PendingActionReceiver"
android:exported="false">
-
+
diff --git a/app/src/main/java/com/android/messaging/datamodel/BugleDatabaseOperations.java b/app/src/main/java/com/android/messaging/datamodel/BugleDatabaseOperations.java
index 2ac102b..f4a6bc9 100644
--- a/app/src/main/java/com/android/messaging/datamodel/BugleDatabaseOperations.java
+++ b/app/src/main/java/com/android/messaging/datamodel/BugleDatabaseOperations.java
@@ -700,7 +700,16 @@ public class BugleDatabaseOperations {
sortTimestamp = cursor.getLong(1);
}
}
-
+ if (sortTimestamp == 0) {
+ try (Cursor existing = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
+ new String[]{ ConversationColumns.SORT_TIMESTAMP },
+ ConversationColumns._ID + "=?",
+ new String[]{ conversationId }, null, null, null)) {
+ if (existing.moveToFirst()) {
+ sortTimestamp = existing.getLong(0);
+ }
+ }
+ }
final ContentValues values = new ContentValues();
if (draftMessage == null || !draftMessage.hasContent()) {
diff --git a/app/src/main/java/com/android/messaging/datamodel/MediaScratchFileProvider.java b/app/src/main/java/com/android/messaging/datamodel/MediaScratchFileProvider.java
index 4c663cc..9c096d2 100644
--- a/app/src/main/java/com/android/messaging/datamodel/MediaScratchFileProvider.java
+++ b/app/src/main/java/com/android/messaging/datamodel/MediaScratchFileProvider.java
@@ -46,7 +46,7 @@ public class MediaScratchFileProvider extends FileProvider {
private static final SimpleArrayMap sUriToDisplayNameMap = new SimpleArrayMap<>();
public static final String AUTHORITY =
- "com.filabcorp.msgq.datamodel.MediaScratchFileProvider";
+ "com.bnkode.notie.datamodel.MediaScratchFileProvider";
private static final String MEDIA_SCRATCH_SPACE_DIR = "mediascratchspace";
public static boolean isMediaScratchSpaceUri(final Uri uri) {
diff --git a/app/src/main/java/com/android/messaging/datamodel/MessagingContentProvider.java b/app/src/main/java/com/android/messaging/datamodel/MessagingContentProvider.java
index 12b430e..3db0f67 100644
--- a/app/src/main/java/com/android/messaging/datamodel/MessagingContentProvider.java
+++ b/app/src/main/java/com/android/messaging/datamodel/MessagingContentProvider.java
@@ -53,7 +53,7 @@ public class MessagingContentProvider extends ContentProvider {
private static final String TAG = LogUtil.BUGLE_TAG;
public static final String AUTHORITY =
- "com.filabcorp.msgq.datamodel.MessagingContentProvider";
+ "com.bnkode.notie.datamodel.MessagingContentProvider";
private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/';
// Conversations query
diff --git a/app/src/main/java/com/android/messaging/datamodel/MmsFileProvider.java b/app/src/main/java/com/android/messaging/datamodel/MmsFileProvider.java
index ae77472..69f0307 100644
--- a/app/src/main/java/com/android/messaging/datamodel/MmsFileProvider.java
+++ b/app/src/main/java/com/android/messaging/datamodel/MmsFileProvider.java
@@ -32,7 +32,7 @@ import java.io.IOException;
public class MmsFileProvider extends FileProvider {
private static final String TAG = LogUtil.BUGLE_TAG;
- static final String AUTHORITY = "com.filabcorp.msgq.datamodel.MmsFileProvider";
+ static final String AUTHORITY = "com.bnkode.notie.datamodel.MmsFileProvider";
private static final String RAW_MMS_DIR = "rawmms";
/**
diff --git a/app/src/main/java/com/android/messaging/datamodel/action/ActionServiceImpl.java b/app/src/main/java/com/android/messaging/datamodel/action/ActionServiceImpl.java
index 1522e54..58cffe5 100644
--- a/app/src/main/java/com/android/messaging/datamodel/action/ActionServiceImpl.java
+++ b/app/src/main/java/com/android/messaging/datamodel/action/ActionServiceImpl.java
@@ -137,7 +137,7 @@ public class ActionServiceImpl extends JobIntentService {
* Broadcast receiver for alarms scheduled through ActionService.
*/
public static class PendingActionReceiver extends BroadcastReceiver {
- static final String ACTION = "com.filabcorp.msgq.datamodel.PENDING_ACTION";
+ static final String ACTION = "com.bnkode.notie.datamodel.PENDING_ACTION";
/**
* Allocate an intent with a specific opcode and alarm action.
diff --git a/app/src/main/java/com/android/messaging/receiver/ScheduledMessageReceiver.java b/app/src/main/java/com/android/messaging/receiver/ScheduledMessageReceiver.java
index 1306ef3..f3d9f34 100644
--- a/app/src/main/java/com/android/messaging/receiver/ScheduledMessageReceiver.java
+++ b/app/src/main/java/com/android/messaging/receiver/ScheduledMessageReceiver.java
@@ -11,7 +11,7 @@ import java.util.concurrent.Executors;
/** Fires a previously scheduled compose message when its AlarmManager time is reached. */
public class ScheduledMessageReceiver extends BroadcastReceiver {
- public static final String ACTION_SEND = "com.filabcorp.msgq.SEND_SCHEDULED_MESSAGE";
+ public static final String ACTION_SEND = "com.bnkode.notie.SEND_SCHEDULED_MESSAGE";
public static final String EXTRA_ID = "scheduled_id";
@Override
diff --git a/app/src/main/java/com/android/messaging/receiver/SendStatusReceiver.java b/app/src/main/java/com/android/messaging/receiver/SendStatusReceiver.java
index 03228d6..a3c4a35 100644
--- a/app/src/main/java/com/android/messaging/receiver/SendStatusReceiver.java
+++ b/app/src/main/java/com/android/messaging/receiver/SendStatusReceiver.java
@@ -38,13 +38,13 @@ import com.android.messaging.util.LogUtil;
*/
public class SendStatusReceiver extends BroadcastReceiver {
public static final String MESSAGE_SENT_ACTION =
- "com.filabcorp.msgq.receiver.SendStatusReceiver.MESSAGE_SENT";
+ "com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_SENT";
public static final String MESSAGE_DELIVERED_ACTION =
- "com.filabcorp.msgq.receiver.SendStatusReceiver.MESSAGE_DELIVERED";
+ "com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_DELIVERED";
public static final String MMS_SENT_ACTION =
- "com.filabcorp.msgq.receiver.SendStatusReceiver.MMS_SENT";
+ "com.bnkode.notie.receiver.SendStatusReceiver.MMS_SENT";
public static final String MMS_DOWNLOADED_ACTION =
- "com.filabcorp.msgq.receiver.SendStatusReceiver.MMS_DOWNLOADED";
+ "com.bnkode.notie.receiver.SendStatusReceiver.MMS_DOWNLOADED";
// Defined by platform, but no constant provided. See docs for SmsManager.sendTextMessage.
public static final String EXTRA_ERROR_CODE = "errorCode";
diff --git a/app/src/main/java/com/android/messaging/ui/UIIntents.java b/app/src/main/java/com/android/messaging/ui/UIIntents.java
index 4c079d4..bec5496 100644
--- a/app/src/main/java/com/android/messaging/ui/UIIntents.java
+++ b/app/src/main/java/com/android/messaging/ui/UIIntents.java
@@ -71,7 +71,7 @@ public abstract class UIIntents {
public static final String UI_INTENT_EXTRA_WITH_CUSTOM_TRANSITION = "with_custom_transition";
public static final String ACTION_RESET_NOTIFICATIONS =
- "com.filabcorp.msgq.reset_notifications";
+ "com.bnkode.notie.reset_notifications";
// Sending VCard uri to VCard detail activity
public static final String UI_INTENT_EXTRA_VCARD_URI = "vcard_uri";
@@ -108,7 +108,7 @@ public abstract class UIIntents {
public static final String UI_INTENT_EXTRA_LOCATION_IMAGE_URI = "location_image_uri";
public static final String ACTION_WIDGET_CONVERSATION =
- "com.filabcorp.msgq.widget_conversation:";
+ "com.bnkode.notie.widget_conversation:";
public static final String UI_INTENT_EXTRA_REQUIRES_MMS = "requires_mms";
diff --git a/app/src/main/java/com/android/messaging/ui/contact/ContactListItemView.java b/app/src/main/java/com/android/messaging/ui/contact/ContactListItemView.java
index b1e89c4..011cd82 100644
--- a/app/src/main/java/com/android/messaging/ui/contact/ContactListItemView.java
+++ b/app/src/main/java/com/android/messaging/ui/contact/ContactListItemView.java
@@ -34,6 +34,7 @@ import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.data.ContactListItemData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.ui.ContactIconView;
+import com.android.messaging.ui.home.HomeAvatarOverlay;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
@@ -44,6 +45,10 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
public interface HostInterface {
void onContactListItemClicked(ContactListItemData item, ContactListItemView view);
boolean isContactSelected(ContactListItemData item);
+
+ default boolean isContactSelectionMode() {
+ return false;
+ }
}
final ContactListItemData mData;
@@ -189,7 +194,8 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
mContactIconView.setImageResourceUri(avatarUri, mData.getContactId(),
mData.getLookupKey(), destinationString);
mContactIconView.setVisibility(VISIBLE);
- mContactCheckmarkView.setVisibility(GONE);
+ applySelectionCheck(isContactChecked());
+ bindAvatarClickTargets();
mContactDetailTypeTextView.setVisibility(GONE);
mContactDetailsTextView.setVisibility(GONE);
mContactNameTextView.setVisibility(VISIBLE);
@@ -202,7 +208,8 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
mContactNameTextView.setVisibility(VISIBLE);
final boolean isSelected = mHostInterface.isContactSelected(mData);
setSelected(isSelected);
- mContactCheckmarkView.setVisibility(isSelected ? VISIBLE : GONE);
+ applySelectionCheck(isSelected);
+ bindAvatarClickTargets();
mContactDetailsTextView.setVisibility(VISIBLE);
mContactDetailTypeTextView.setVisibility(VISIBLE);
} else {
@@ -211,7 +218,8 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
mContactNameTextView.setVisibility(GONE);
final boolean isSelected = mHostInterface.isContactSelected(mData);
setSelected(isSelected);
- mContactCheckmarkView.setVisibility(isSelected ? VISIBLE : GONE);
+ applySelectionCheck(isSelected);
+ bindAvatarClickTargets();
mContactDetailsTextView.setVisibility(VISIBLE);
mContactDetailTypeTextView.setVisibility(VISIBLE);
}
@@ -230,6 +238,34 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
}
}
+ private boolean isContactChecked() {
+ return mHostInterface != null && mHostInterface.isContactSelected(mData);
+ }
+
+ private void applySelectionCheck(final boolean selected) {
+ HomeAvatarOverlay.apply(mContactCheckmarkView, isSelectionMode(), selected);
+ }
+
+ private boolean isSelectionMode() {
+ return mHostInterface != null && mHostInterface.isContactSelectionMode();
+ }
+
+ private void bindAvatarClickTargets() {
+ if (mContactIconView == null) {
+ return;
+ }
+ mContactIconView.setImageClickHandlerDisabled(true);
+ final boolean selectionMode = isSelectionMode();
+ final OnClickListener selectRow = v -> performClick();
+ if (mContactCheckmarkView != null) {
+ mContactCheckmarkView.setOnClickListener(selectionMode ? selectRow : null);
+ mContactCheckmarkView.setClickable(selectionMode);
+ mContactCheckmarkView.setFocusable(false);
+ }
+ mContactIconView.setOnClickListener(selectionMode ? selectRow : null);
+ mContactIconView.setClickable(selectionMode);
+ }
+
/**
* {@inheritDoc} from OnClickListener
*/
diff --git a/app/src/main/java/com/android/messaging/ui/contact/ContactPickerFragment.java b/app/src/main/java/com/android/messaging/ui/contact/ContactPickerFragment.java
index 2ca83e0..e2dc261 100644
--- a/app/src/main/java/com/android/messaging/ui/contact/ContactPickerFragment.java
+++ b/app/src/main/java/com/android/messaging/ui/contact/ContactPickerFragment.java
@@ -291,6 +291,13 @@ public class ContactPickerFragment extends Fragment implements ContactPickerData
item.getRecipientEntry().getDestination()));
}
+ @Override
+ public boolean isContactSelectionMode() {
+ return mContactPickingMode == MODE_PICK_INITIAL_CONTACT
+ || mContactPickingMode == MODE_PICK_MORE_CONTACTS
+ || mContactPickingMode == MODE_PICK_MAX_PARTICIPANTS;
+ }
+
/**
* Call this immediately after attaching the fragment, or when there's a ui state change that
* changes our host (i.e. restore from saved instance state).
diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java
index 2abdcbe..d9102b4 100644
--- a/app/src/main/java/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java
@@ -18,15 +18,29 @@ package com.android.messaging.ui.conversationlist;
import android.os.Bundle;
import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
import androidx.appcompat.app.ActionBar;
+import androidx.core.content.ContextCompat;
import com.android.messaging.R;
+import com.android.messaging.ui.home.HomeAutomationHome;
+import com.android.messaging.ui.home.HomeEmptyState;
import com.android.messaging.ui.home.HomeInboxMenu;
import com.android.messaging.ui.home.HomeSubHeader;
public class ArchivedConversationListActivity extends AbstractConversationListActivity {
+ private static final String STATE_SHOW_RULES = "home_archived_show_rules";
+
+ private TextView mChatsTab;
+ private TextView mRulesTab;
+ private View mListHost;
+ private View mRulesHost;
+ private View mSearchCollapse;
+ private boolean mShowRules;
+
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -36,6 +50,11 @@ public class ArchivedConversationListActivity extends AbstractConversationListAc
}
HomeSubHeader.bind(this, R.string.home_archived_title, v -> onActionBarHome());
HomeInboxMenu.bind(this, HomeInboxMenu.Screen.ARCHIVE);
+ bindTabs();
+ if (savedInstanceState != null) {
+ mShowRules = savedInstanceState.getBoolean(STATE_SHOW_RULES, false);
+ }
+ showRules(mShowRules);
if (savedInstanceState == null) {
mConversationListFragment =
@@ -50,6 +69,79 @@ public class ArchivedConversationListActivity extends AbstractConversationListAc
}
}
+ @Override
+ protected void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(STATE_SHOW_RULES, mShowRules);
+ }
+
+ private void bindTabs() {
+ final View tabs = findViewById(R.id.home_archived_tabs);
+ mChatsTab = findViewById(R.id.home_archived_tab_chats);
+ mRulesTab = findViewById(R.id.home_archived_tab_rules);
+ mListHost = findViewById(R.id.home_archived_list_host);
+ mRulesHost = findViewById(R.id.home_archived_rules_host);
+ mSearchCollapse = findViewById(R.id.home_sub_search_collapse);
+ if (tabs != null) {
+ tabs.setVisibility(View.VISIBLE);
+ }
+ if (mRulesHost != null) {
+ HomeAutomationHome.bindArchive(mRulesHost);
+ final HomeEmptyState empty = findViewById(R.id.home_archived_rules_empty);
+ if (empty != null) {
+ empty.setContent(R.drawable.ic_home_menu_archive,
+ R.string.home_archived_rules_empty_title,
+ R.string.home_archived_rules_empty_body);
+ }
+ }
+ if (mChatsTab != null) {
+ mChatsTab.setOnClickListener(v -> showRules(false));
+ }
+ if (mRulesTab != null) {
+ mRulesTab.setOnClickListener(v -> showRules(true));
+ }
+ }
+
+ private void showRules(final boolean rules) {
+ if (rules && isInConversationListSelectMode()) {
+ exitMultiSelectState();
+ }
+ mShowRules = rules;
+ applyPill(mChatsTab, !rules);
+ applyPill(mRulesTab, rules);
+ if (mListHost != null) {
+ mListHost.setVisibility(rules ? View.GONE : View.VISIBLE);
+ }
+ if (mRulesHost != null) {
+ mRulesHost.setVisibility(rules ? View.VISIBLE : View.GONE);
+ }
+ if (mSearchCollapse != null) {
+ mSearchCollapse.setVisibility(rules ? View.GONE : View.VISIBLE);
+ }
+ final View edit = findViewById(R.id.home_sub_header_edit);
+ if (edit != null) {
+ edit.setVisibility(rules ? View.GONE : View.VISIBLE);
+ }
+ final View selectBar = findViewById(R.id.home_sub_select_bar);
+ if (selectBar != null && rules) {
+ selectBar.setVisibility(View.GONE);
+ }
+ if (rules && mRulesHost != null) {
+ HomeAutomationHome.bindArchive(mRulesHost);
+ }
+ }
+
+ private void applyPill(final TextView pill, final boolean selected) {
+ if (pill == null) {
+ return;
+ }
+ pill.setBackgroundResource(selected
+ ? R.drawable.home_bg_automation_pill_selected
+ : R.drawable.home_bg_automation_pill);
+ pill.setTextColor(ContextCompat.getColor(this,
+ selected ? android.R.color.white : R.color.home_text_primary));
+ }
+
@Override
protected boolean useHomeSelectionChrome() {
return true;
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 985610f..9357736 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
@@ -79,6 +79,7 @@ import com.android.messaging.datamodel.data.ConversationListData.ConversationLis
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.ui.ContactIconView;
import com.android.messaging.ui.home.HomeAutomationHome;
+import com.android.messaging.ui.home.HomeDemoGroupSeed;
import com.android.messaging.ui.home.HomeCategoryAddModal;
import com.android.messaging.ui.home.HomeCategoryAssignSheet;
import com.android.messaging.ui.home.HomeCategoryClassifier;
@@ -306,6 +307,9 @@ public class ConversationListFragment extends Fragment implements ConversationLi
updateHomeMainNavVisibility();
refreshPinnedStrip();
updateUi();
+ if (mHomeAutomationPane != null) {
+ HomeAutomationHome.applyVisibility(mHomeAutomationPane);
+ }
}
public void setScrolledToNewestConversationIfNeeded() {
@@ -547,6 +551,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
selectHomeTab(mHomeTab);
updateHomeEditButtonForPinnedMode();
+ HomeDemoGroupSeed.ensure(requireContext());
}
bindSubListSearch();
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 7fdbcf0..70c5748 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
@@ -64,6 +64,7 @@ import com.android.messaging.ui.AudioAttachmentView;
import com.android.messaging.ui.ContactIconView;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.SnackBarInteraction;
+import com.android.messaging.ui.home.HomeAvatarOverlay;
import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.ui.home.MutedConversationStore;
import com.android.messaging.util.Assert;
@@ -172,6 +173,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
private static final int SELECTION_TOGGLE_ANIM_MS = 180;
private String mSelectionStateConversationId;
+ private Boolean mSelectionStateMode;
private Boolean mSelectionStateChecked;
public ConversationListItemView(final Context context, final AttributeSet attrs) {
@@ -465,82 +467,57 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
private void updateSelectionIndicator(final boolean selectionMode, final boolean isSelected,
final Resources resources) {
- if (mSelectionIndicatorView == null || mSelectionRingView == null
- || mSelectionCheckView == null) {
- return;
- }
- if (!selectionMode) {
+ if (mSelectionIndicatorView != null) {
mSelectionIndicatorView.animate().cancel();
- mSelectionCheckView.animate().cancel();
- mSelectionIndicatorView.setScaleX(1f);
- mSelectionIndicatorView.setScaleY(1f);
- mSelectionCheckView.setAlpha(1f);
- mSelectionCheckView.setScaleX(1f);
- mSelectionCheckView.setScaleY(1f);
mSelectionIndicatorView.setVisibility(GONE);
- mSelectionStateConversationId = null;
- mSelectionStateChecked = null;
+ }
+ if (mContactCheckmarkView == null) {
return;
}
- mSelectionIndicatorView.setVisibility(VISIBLE);
final String conversationId = mData.getConversationId();
final boolean sameConversation = conversationId != null
&& conversationId.equals(mSelectionStateConversationId);
- final boolean animateToggle = sameConversation && mSelectionStateChecked != null
- && mSelectionStateChecked.booleanValue() != isSelected;
+ final boolean wasMode = Boolean.TRUE.equals(mSelectionStateMode);
+ final boolean wasSelected = Boolean.TRUE.equals(mSelectionStateChecked);
+ final boolean animateToggle = sameConversation
+ && (wasMode != selectionMode || wasSelected != isSelected);
mSelectionStateConversationId = conversationId;
+ mSelectionStateMode = selectionMode;
mSelectionStateChecked = isSelected;
- mSelectionIndicatorView.animate().cancel();
- mSelectionCheckView.animate().cancel();
-
- if (isSelected) {
- mSelectionRingView.setBackground(resources.getDrawable(
- R.drawable.home_bg_selection_ring_checked, getContext().getTheme()));
- mSelectionCheckView.setVisibility(VISIBLE);
- if (animateToggle) {
- mSelectionIndicatorView.setScaleX(0.88f);
- mSelectionIndicatorView.setScaleY(0.88f);
- mSelectionIndicatorView.animate()
- .scaleX(1f)
- .scaleY(1f)
+ mContactCheckmarkView.animate().cancel();
+ if (!selectionMode) {
+ if (animateToggle && mContactCheckmarkView.getVisibility() == VISIBLE) {
+ mContactCheckmarkView.animate()
+ .alpha(0f)
+ .scaleX(0.6f)
+ .scaleY(0.6f)
.setDuration(SELECTION_TOGGLE_ANIM_MS)
- .setInterpolator(new OvershootInterpolator(1.6f));
- mSelectionCheckView.setAlpha(0f);
- mSelectionCheckView.setScaleX(0.4f);
- mSelectionCheckView.setScaleY(0.4f);
- mSelectionCheckView.animate()
- .alpha(1f)
- .scaleX(1f)
- .scaleY(1f)
- .setDuration(SELECTION_TOGGLE_ANIM_MS)
- .setInterpolator(UiUtils.DEFAULT_INTERPOLATOR);
+ .setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
+ .withEndAction(() ->
+ HomeAvatarOverlay.apply(mContactCheckmarkView, false, false))
+ .start();
} else {
- mSelectionIndicatorView.setScaleX(1f);
- mSelectionIndicatorView.setScaleY(1f);
- mSelectionCheckView.setAlpha(1f);
- mSelectionCheckView.setScaleX(1f);
- mSelectionCheckView.setScaleY(1f);
+ HomeAvatarOverlay.apply(mContactCheckmarkView, false, false);
}
+ return;
+ }
+ HomeAvatarOverlay.apply(mContactCheckmarkView, true, isSelected);
+ if (animateToggle && isSelected && !wasSelected) {
+ mContactCheckmarkView.setAlpha(0f);
+ mContactCheckmarkView.setScaleX(0.6f);
+ mContactCheckmarkView.setScaleY(0.6f);
+ mContactCheckmarkView.animate()
+ .alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(SELECTION_TOGGLE_ANIM_MS)
+ .setInterpolator(new OvershootInterpolator(1.6f))
+ .start();
} else {
- mSelectionRingView.setBackground(resources.getDrawable(
- R.drawable.home_bg_selection_ring, getContext().getTheme()));
- mSelectionCheckView.setVisibility(GONE);
- mSelectionCheckView.setAlpha(1f);
- mSelectionCheckView.setScaleX(1f);
- mSelectionCheckView.setScaleY(1f);
- if (animateToggle) {
- mSelectionIndicatorView.setScaleX(0.92f);
- mSelectionIndicatorView.setScaleY(0.92f);
- mSelectionIndicatorView.animate()
- .scaleX(1f)
- .scaleY(1f)
- .setDuration(SELECTION_TOGGLE_ANIM_MS)
- .setInterpolator(UiUtils.DEFAULT_INTERPOLATOR);
- } else {
- mSelectionIndicatorView.setScaleX(1f);
- mSelectionIndicatorView.setScaleY(1f);
- }
+ mContactCheckmarkView.setAlpha(1f);
+ mContactCheckmarkView.setScaleX(1f);
+ mContactCheckmarkView.setScaleY(1f);
}
}
@@ -581,8 +558,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
}
final ViewGroup.MarginLayoutParams layoutParams =
(ViewGroup.MarginLayoutParams) mListDividerView.getLayoutParams();
- final int insetDp = selectionMode ? LIST_DIVIDER_INSET_SELECTION_DP
- : LIST_DIVIDER_INSET_DEFAULT_DP;
+ final int insetDp = LIST_DIVIDER_INSET_DEFAULT_DP;
final float density = getResources().getDisplayMetrics().density;
final int insetPx = (int) (insetDp * density + 0.5f);
if (layoutParams.leftMargin != insetPx) {
@@ -788,7 +764,6 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
setSelected(isSelected);
Uri iconUri = null;
int contactIconVisibility = VISIBLE;
- int checkmarkVisiblity = GONE;
int failStatusVisiblity = GONE;
if (!isSelected) {
// Only show the fail icon if it is not a group conversation.
@@ -805,18 +780,8 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
mData.getParticipantLookupKey(), mData.getOtherParticipantNormalizedDestination());
mContactIconView.setVisibility(contactIconVisibility);
mContactIconView.setImageClickHandlerDisabled(true);
- mContactIconView.setOnLongClickListener(this);
- final boolean avatarInteractive = !mHostInterface.isSelectionMode();
- mContactIconView.setClickable(avatarInteractive);
- mContactIconView.setLongClickable(avatarInteractive);
- if (avatarInteractive) {
- mContactIconView.setOnClickListener(v -> mHostInterface.onConversationAvatarClicked(
- mData, mContactIconView));
- } else {
- mContactIconView.setOnClickListener(null);
- }
+ bindAvatarClickTargets(selectionMode);
- mContactCheckmarkView.setVisibility(checkmarkVisiblity);
mFailedStatusIconView.setVisibility(failStatusVisiblity);
final boolean muted = MutedConversationStore.isMuted(getContext(),
mData.getConversationId());
@@ -865,7 +830,9 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */));
mImagePreviewView.setOnLongClickListener(this);
mImagePreviewView.setVisibility(previewImageVisibility);
- mImagePreviewView.setOnClickListener(previewClickListener);
+ mImagePreviewView.setOnClickListener(selectionMode
+ ? v -> processClick(mSwipeableContainer, false)
+ : previewClickListener);
mAudioAttachmentView.setOnLongClickListener(this);
mAudioAttachmentView.setVisibility(audioPreviewVisiblity);
@@ -888,14 +855,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
mData.getConversationId());
setSelected(isSelected);
updateSelectionIndicator(selectionMode, isSelected, resources);
- mContactIconView.setClickable(!selectionMode);
- mContactIconView.setLongClickable(!selectionMode);
- if (!selectionMode) {
- mContactIconView.setOnClickListener(v -> mHostInterface.onConversationAvatarClicked(
- mData, mContactIconView));
- } else {
- mContactIconView.setOnClickListener(null);
- }
+ bindAvatarClickTargets(selectionMode);
updatePinButton(selectionMode, resources);
updateListDividerInset(selectionMode);
}
@@ -1093,8 +1053,28 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
return processClick(v, true);
}
+ private void bindAvatarClickTargets(final boolean selectionMode) {
+ final OnClickListener selectRow = v -> processClick(mSwipeableContainer, false);
+ if (mContactCheckmarkView != null) {
+ mContactCheckmarkView.setOnClickListener(selectionMode ? selectRow : null);
+ mContactCheckmarkView.setClickable(selectionMode);
+ mContactCheckmarkView.setFocusable(false);
+ }
+ mContactIconView.setOnLongClickListener(this);
+ if (selectionMode) {
+ mContactIconView.setOnClickListener(selectRow);
+ mContactIconView.setClickable(true);
+ mContactIconView.setLongClickable(false);
+ } else {
+ mContactIconView.setOnClickListener(v -> mHostInterface.onConversationAvatarClicked(
+ mData, mContactIconView));
+ mContactIconView.setClickable(true);
+ mContactIconView.setLongClickable(true);
+ }
+ }
+
private boolean processClick(final View v, final boolean isLongClick) {
- if (v == mContactIconView) {
+ if (!mHostInterface.isSelectionMode() && v == mContactIconView) {
return false;
}
Assert.isTrue(v == mSwipeableContainer || v == mImagePreviewView);
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeAutomationCreateActivity.java b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationCreateActivity.java
index fa25a75..a150875 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeAutomationCreateActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationCreateActivity.java
@@ -15,16 +15,50 @@
*/
package com.android.messaging.ui.home;
+import android.net.Uri;
import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
import android.view.View;
-import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.StringRes;
+import androidx.core.content.ContextCompat;
import com.android.messaging.R;
import com.android.messaging.ui.BugleActionBarActivity;
+import com.android.messaging.ui.ContactIconView;
-/** Sheet to create a new automation task. */
+import java.util.ArrayList;
+
+/** Sheet to create a new automation rule. */
public class HomeAutomationCreateActivity extends BugleActionBarActivity {
+ private View mTriggerMissed;
+ private View mTriggerThanks;
+ private View mTriggerSchedule;
+ private View mTriggerKeyword;
+ private TextView mRecipientAll;
+ private TextView mRecipientContacts;
+ private TextView mRecipientUnknown;
+ private TextView mSelectContacts;
+ private HomeFadeScrollView mRecipientScroll;
+ private View mTargetChipHost;
+ private View mTargetChipScroll;
+ private LinearLayout mTargetChips;
+
+ private static final String STATE_TRIGGER = "home_automation_trigger";
+ private static final String STATE_RECIPIENT = "home_automation_recipient";
+ private static final String STATE_TARGETS = "home_automation_targets";
+
+ private int mTrigger;
+ private int mRecipient;
+ private final ArrayList mTargets = new ArrayList<>();
+ private boolean mRestorePromptOpen;
+
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -33,21 +67,336 @@ public class HomeAutomationCreateActivity extends BugleActionBarActivity {
getSupportActionBar().hide();
}
- final ImageButton back = findViewById(R.id.home_sub_header_back);
- if (back != null) {
- back.setVisibility(View.GONE);
- }
- final ImageButton close = findViewById(R.id.home_sub_header_close);
+ final View close = findViewById(R.id.home_sub_header_close);
if (close != null) {
- close.setVisibility(View.VISIBLE);
close.setOnClickListener(v -> finish());
}
- HomeSubHeader.bind(this, R.string.home_automation_create_title, v -> finish());
-
final View scrim = findViewById(R.id.home_automation_create_scrim);
if (scrim != null) {
scrim.setOnClickListener(v -> finish());
}
+
+ mTriggerMissed = bindTriggerRow(R.id.home_automation_trigger_missed,
+ R.drawable.ic_home_automation_phone,
+ R.string.home_automation_trigger_missed_title,
+ R.string.home_automation_trigger_missed_body, true, 0);
+ mTriggerThanks = bindTriggerRow(R.id.home_automation_trigger_thanks,
+ R.drawable.ic_home_automation_phone,
+ R.string.home_automation_trigger_thanks_title,
+ R.string.home_automation_trigger_thanks_body, false, 1);
+ mTriggerSchedule = bindTriggerTile(R.id.home_automation_trigger_schedule,
+ R.drawable.ic_home_schedule_line,
+ R.string.home_automation_trigger_schedule_title,
+ R.string.home_automation_trigger_schedule_body, 2);
+ mTriggerKeyword = bindTriggerTile(R.id.home_automation_trigger_keyword,
+ R.drawable.ic_home_automation_chat,
+ R.string.home_automation_trigger_keyword_title,
+ R.string.home_automation_trigger_keyword_body, 3);
+
+ mRecipientScroll = findViewById(R.id.home_automation_recipient_scroll);
+ mRecipientAll = findViewById(R.id.home_automation_recipient_all);
+ mRecipientContacts = findViewById(R.id.home_automation_recipient_contacts);
+ mRecipientUnknown = findViewById(R.id.home_automation_recipient_unknown);
+ bindRecipient(mRecipientAll, 0);
+ bindRecipient(mRecipientContacts, 1);
+ bindRecipient(mRecipientUnknown, 2);
+
+ mSelectContacts = findViewById(R.id.home_automation_select_contacts);
+ mTargetChipHost = findViewById(R.id.home_automation_target_chip_host);
+ mTargetChipScroll = findViewById(R.id.home_automation_target_chip_scroll);
+ mTargetChips = findViewById(R.id.home_automation_target_chips);
+ getSupportFragmentManager().setFragmentResultListener(
+ HomeAutomationTargetPickerSheet.REQUEST_KEY, this, (key, bundle) -> {
+ final ArrayList selected = bundle
+ .getParcelableArrayList(HomeAutomationTargetPickerSheet.EXTRA_TARGETS);
+ mTargets.clear();
+ if (selected != null) {
+ mTargets.addAll(selected);
+ }
+ bindTargets();
+ });
+ if (mSelectContacts != null) {
+ mSelectContacts.setOnClickListener(v -> openTargetPicker());
+ }
+
+ comingSoon(R.id.home_automation_preview);
+ comingSoon(R.id.home_automation_activate);
+
+ if (savedInstanceState != null) {
+ restoreState(savedInstanceState);
+ } else if (HomeAutomationDraftStore.hasDraft(this)) {
+ promptRestoreDraft();
+ }
+
+ applyTrigger();
+ applyRecipient();
+ bindTargets();
+ }
+
+ private View bindTriggerRow(final int id, @DrawableRes final int icon,
+ @StringRes final int title, @StringRes final int body, final boolean recommended,
+ final int triggerIndex) {
+ final View row = findViewById(id);
+ if (row == null) {
+ return null;
+ }
+ final ImageView iconView = row.findViewById(R.id.home_automation_trigger_icon);
+ if (iconView != null) {
+ iconView.setImageResource(icon);
+ }
+ final TextView titleView = row.findViewById(R.id.home_automation_trigger_title);
+ if (titleView != null) {
+ titleView.setText(title);
+ }
+ final TextView bodyView = row.findViewById(R.id.home_automation_trigger_body);
+ if (bodyView != null) {
+ bodyView.setText(body);
+ }
+ final View badge = row.findViewById(R.id.home_automation_trigger_badge);
+ if (badge != null) {
+ badge.setVisibility(recommended ? View.VISIBLE : View.GONE);
+ }
+ if (triggerIndex >= 0) {
+ row.setOnClickListener(v -> selectTrigger(triggerIndex));
+ }
+ return row;
+ }
+
+ private View bindTriggerTile(final int id, @DrawableRes final int icon,
+ @StringRes final int title, @StringRes final int body, final int triggerIndex) {
+ final View tile = findViewById(id);
+ if (tile == null) {
+ return null;
+ }
+ final ImageView iconView = tile.findViewById(R.id.home_automation_trigger_icon);
+ if (iconView != null) {
+ iconView.setImageResource(icon);
+ }
+ final TextView titleView = tile.findViewById(R.id.home_automation_trigger_title);
+ if (titleView != null) {
+ titleView.setText(title);
+ }
+ final TextView bodyView = tile.findViewById(R.id.home_automation_trigger_body);
+ if (bodyView != null) {
+ bodyView.setText(body);
+ }
+ tile.setOnClickListener(v -> selectTrigger(triggerIndex));
+ return tile;
+ }
+
+ private void bindRecipient(final TextView pill, final int index) {
+ if (pill != null) {
+ pill.setOnClickListener(v -> selectRecipient(index));
+ }
+ }
+
+ private void comingSoon(final int id) {
+ final View view = findViewById(id);
+ if (view != null) {
+ view.setOnClickListener(v -> HomeToast.show(this, R.string.home_automate_coming_soon));
+ }
+ }
+
+ private void selectTrigger(final int index) {
+ if (mTrigger == index) {
+ return;
+ }
+ mTrigger = index;
+ applyTrigger();
+ }
+
+ private void selectRecipient(final int index) {
+ if (mRecipient == index) {
+ return;
+ }
+ mRecipient = index;
+ applyRecipient();
+ }
+
+ private void applyTrigger() {
+ applyTriggerRow(mTriggerMissed, mTrigger == 0, true);
+ applyTriggerRow(mTriggerThanks, mTrigger == 1, true);
+ applyTriggerTile(mTriggerSchedule, mTrigger == 2);
+ applyTriggerTile(mTriggerKeyword, mTrigger == 3);
+ }
+
+ private void applyRecipient() {
+ applyPill(mRecipientAll, mRecipient == 0);
+ applyPill(mRecipientContacts, mRecipient == 1);
+ applyPill(mRecipientUnknown, mRecipient == 2);
+ if (mRecipientScroll != null) {
+ mRecipientScroll.centerChild(mRecipient == 0 ? mRecipientAll
+ : mRecipient == 1 ? mRecipientContacts : mRecipientUnknown);
+ }
+ }
+
+ private void applySelectContacts() {
+ final boolean selected = !mTargets.isEmpty();
+ applyPill(mSelectContacts, selected);
+ if (selected && mRecipientScroll != null) {
+ mRecipientScroll.centerChild(mSelectContacts);
+ }
+ }
+
+ private void openTargetPicker() {
+ HomeAutomationTargetPickerSheet.show(getSupportFragmentManager(),
+ new ArrayList<>(mTargets));
+ }
+
+ private void bindTargets() {
+ if (mTargetChips == null) {
+ return;
+ }
+ mTargetChips.removeAllViews();
+ final boolean empty = mTargets.isEmpty();
+ if (mTargetChipHost != null) {
+ mTargetChipHost.setVisibility(empty ? View.GONE : View.VISIBLE);
+ } else if (mTargetChipScroll != null) {
+ mTargetChipScroll.setVisibility(empty ? View.GONE : View.VISIBLE);
+ }
+ applySelectContacts();
+ if (empty) {
+ return;
+ }
+ final LayoutInflater inflater = LayoutInflater.from(this);
+ for (final HomeAutomationTarget target : mTargets) {
+ final View chip = inflater.inflate(R.layout.home_automation_target_chip, mTargetChips,
+ false);
+ final ContactIconView avatar = chip.findViewById(R.id.home_compose_chip_avatar);
+ if (!TextUtils.isEmpty(target.iconUri)) {
+ avatar.setImageResourceUri(Uri.parse(target.iconUri), target.contactId,
+ target.lookupKey, target.destination);
+ }
+ avatar.setImageClickHandlerDisabled(true);
+ final TextView name = chip.findViewById(R.id.home_compose_chip_name);
+ name.setText(target.displayName());
+ final View remove = chip.findViewById(R.id.home_automation_target_chip_remove);
+ if (remove != null) {
+ remove.setOnClickListener(v -> {
+ mTargets.remove(target);
+ bindTargets();
+ });
+ }
+ final LinearLayout.LayoutParams params =
+ (LinearLayout.LayoutParams) chip.getLayoutParams();
+ params.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.looply_space_xs));
+ mTargetChips.addView(chip, params);
+ }
+ }
+
+ private void applyTriggerRow(final View row, final boolean selected, final boolean tintIcon) {
+ if (row == null) {
+ return;
+ }
+ row.setBackgroundResource(selected
+ ? R.drawable.home_bg_automation_option_selected
+ : R.drawable.home_bg_automation_option);
+ final ImageView icon = row.findViewById(R.id.home_automation_trigger_icon);
+ if (icon != null && tintIcon) {
+ if (selected) {
+ icon.setBackgroundResource(R.drawable.home_bg_automation_icon_on);
+ icon.setColorFilter(ContextCompat.getColor(this, android.R.color.white));
+ } else {
+ icon.setBackground(null);
+ icon.setColorFilter(ContextCompat.getColor(this, R.color.home_text_primary));
+ }
+ }
+ applyCheck(row.findViewById(R.id.home_automation_trigger_check), selected);
+ }
+
+ private void applyTriggerTile(final View tile, final boolean selected) {
+ if (tile == null) {
+ return;
+ }
+ tile.setBackgroundResource(selected
+ ? R.drawable.home_bg_automation_option_selected
+ : R.drawable.home_bg_automation_option);
+ }
+
+ private void applyPill(final TextView pill, final boolean selected) {
+ if (pill == null) {
+ return;
+ }
+ pill.setBackgroundResource(selected
+ ? R.drawable.home_bg_automation_pill_selected
+ : R.drawable.home_bg_automation_pill);
+ pill.setTextColor(ContextCompat.getColor(this,
+ selected ? android.R.color.white : R.color.home_text_primary));
+ }
+
+ private void applyCheck(final ImageView check, final boolean selected) {
+ if (check == null) {
+ return;
+ }
+ if (selected) {
+ check.setBackgroundResource(R.drawable.home_bg_selection_ring_checked);
+ check.setImageResource(R.drawable.ic_home_check);
+ check.setColorFilter(ContextCompat.getColor(this, android.R.color.white));
+ check.setPadding(4, 4, 4, 4);
+ } else {
+ check.setBackgroundResource(R.drawable.home_bg_selection_ring);
+ check.setImageDrawable(null);
+ check.setColorFilter(null);
+ check.setPadding(0, 0, 0, 0);
+ }
+ }
+
+ private void promptRestoreDraft() {
+ mRestorePromptOpen = true;
+ final HomeModal.Handle handle = HomeModal.builder(this)
+ .icon(R.drawable.ic_home_nav_automate)
+ .title(R.string.home_automation_draft_title)
+ .message(R.string.home_automation_draft_body)
+ .positive(R.string.home_automation_draft_resume, this::applyStoredDraft)
+ .negative(R.string.home_automation_draft_start_new, () ->
+ HomeAutomationDraftStore.clear(this))
+ .show();
+ handle.dialog.setOnDismissListener(dialog -> mRestorePromptOpen = false);
+ }
+
+ private void applyStoredDraft() {
+ final HomeAutomationDraftStore.Draft draft = HomeAutomationDraftStore.load(this);
+ if (draft == null) {
+ return;
+ }
+ applyDraft(draft.trigger, draft.recipient, draft.targets);
+ }
+
+ private void restoreState(final Bundle savedInstanceState) {
+ final ArrayList targets =
+ savedInstanceState.getParcelableArrayList(STATE_TARGETS);
+ applyDraft(savedInstanceState.getInt(STATE_TRIGGER, 0),
+ savedInstanceState.getInt(STATE_RECIPIENT, 0), targets);
+ }
+
+ private void applyDraft(final int trigger, final int recipient,
+ final ArrayList targets) {
+ mTrigger = trigger;
+ mRecipient = recipient;
+ mTargets.clear();
+ if (targets != null) {
+ mTargets.addAll(targets);
+ }
+ applyTrigger();
+ applyRecipient();
+ bindTargets();
+ }
+
+ @Override
+ protected void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(STATE_TRIGGER, mTrigger);
+ outState.putInt(STATE_RECIPIENT, mRecipient);
+ outState.putParcelableArrayList(STATE_TARGETS, new ArrayList<>(mTargets));
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mRestorePromptOpen) {
+ return;
+ }
+ HomeAutomationDraftStore.save(this, mTrigger, mRecipient, mTargets);
}
@Override
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeAutomationDraftStore.java b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationDraftStore.java
new file mode 100644
index 0000000..ad5f138
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationDraftStore.java
@@ -0,0 +1,130 @@
+/*
+ * 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.content.SharedPreferences;
+import android.text.TextUtils;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+/** Local draft for an automation rule that was left unfinished. */
+public final class HomeAutomationDraftStore {
+
+ private static final String PREFS_NAME = "home_automation_draft";
+ private static final String KEY_JSON = "draft_json";
+
+ public static final class Draft {
+ public final int trigger;
+ public final int recipient;
+ public final ArrayList targets;
+
+ public Draft(final int trigger, final int recipient,
+ final ArrayList targets) {
+ this.trigger = trigger;
+ this.recipient = recipient;
+ this.targets = targets == null ? new ArrayList<>() : targets;
+ }
+
+ public boolean isDirty() {
+ return trigger != 0 || recipient != 0 || !targets.isEmpty();
+ }
+ }
+
+ private HomeAutomationDraftStore() {
+ }
+
+ public static boolean hasDraft(final Context context) {
+ final Draft draft = load(context);
+ return draft != null && draft.isDirty();
+ }
+
+ public static Draft load(final Context context) {
+ final String json = prefs(context).getString(KEY_JSON, null);
+ if (TextUtils.isEmpty(json)) {
+ return null;
+ }
+ try {
+ final JSONObject object = new JSONObject(json);
+ final ArrayList targets = new ArrayList<>();
+ final JSONArray array = object.optJSONArray("targets");
+ if (array != null) {
+ for (int i = 0; i < array.length(); i++) {
+ final JSONObject item = array.optJSONObject(i);
+ if (item == null) {
+ continue;
+ }
+ targets.add(new HomeAutomationTarget(
+ item.optString("name", null),
+ item.optString("destination", null),
+ item.optBoolean("group", false),
+ item.optLong("contactId", -1),
+ emptyToNull(item.optString("lookupKey", null)),
+ emptyToNull(item.optString("iconUri", null))));
+ }
+ }
+ return new Draft(object.optInt("trigger", 0), object.optInt("recipient", 0), targets);
+ } catch (final Exception ignored) {
+ return null;
+ }
+ }
+
+ public static void save(final Context context, final int trigger, final int recipient,
+ final ArrayList targets) {
+ final Draft draft = new Draft(trigger, recipient, targets);
+ if (!draft.isDirty()) {
+ clear(context);
+ return;
+ }
+ try {
+ final JSONObject object = new JSONObject();
+ object.put("trigger", trigger);
+ object.put("recipient", recipient);
+ final JSONArray array = new JSONArray();
+ for (final HomeAutomationTarget target : draft.targets) {
+ if (target == null) {
+ continue;
+ }
+ final JSONObject item = new JSONObject();
+ item.put("name", target.name);
+ item.put("destination", target.destination);
+ item.put("group", target.group);
+ item.put("contactId", target.contactId);
+ item.put("lookupKey", target.lookupKey);
+ item.put("iconUri", target.iconUri);
+ array.put(item);
+ }
+ object.put("targets", array);
+ prefs(context).edit().putString(KEY_JSON, object.toString()).apply();
+ } catch (final Exception ignored) {
+ }
+ }
+
+ public static void clear(final Context context) {
+ prefs(context).edit().remove(KEY_JSON).apply();
+ }
+
+ private static String emptyToNull(final String value) {
+ return TextUtils.isEmpty(value) ? null : value;
+ }
+
+ 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/HomeAutomationHome.java b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationHome.java
index 80412ef..eab1475 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeAutomationHome.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationHome.java
@@ -21,13 +21,25 @@ import android.widget.TextView;
import com.android.messaging.R;
-/** Binds the Automation home preview: modes, sample rules, and local toggles. */
+/** Binds the Automation home preview and archived-rule list. */
public final class HomeAutomationHome {
private HomeAutomationHome() {
}
public static void bind(final View root) {
+ bind(root, false);
+ }
+
+ public static void bindArchive(final View root) {
+ bind(root, true);
+ }
+
+ public static void applyVisibility(final View root) {
+ applyVisibility(root, false);
+ }
+
+ private static void bind(final View root, final boolean archiveList) {
if (root == null) {
return;
}
@@ -44,10 +56,12 @@ public final class HomeAutomationHome {
bindChip(root, R.id.home_automation_rule_weekly_chip2,
R.string.home_automation_rule_weekly_chip2);
- comingSoon(root, R.id.home_automation_modes_more);
- comingSoon(root, R.id.home_automation_mode_call);
- comingSoon(root, R.id.home_automation_mode_cards);
- comingSoon(root, R.id.home_automation_rules_sort);
+ if (!archiveList) {
+ comingSoon(root, R.id.home_automation_modes_more);
+ comingSoon(root, R.id.home_automation_mode_call);
+ comingSoon(root, R.id.home_automation_mode_cards);
+ comingSoon(root, R.id.home_automation_rules_sort);
+ }
comingSoon(root, R.id.home_automation_rule_missed);
comingSoon(root, R.id.home_automation_rule_thanks);
comingSoon(root, R.id.home_automation_rule_weekly);
@@ -55,6 +69,80 @@ public final class HomeAutomationHome {
bindSwitch(root, R.id.home_automation_rule_missed_switch, true);
bindSwitch(root, R.id.home_automation_rule_thanks_switch, true);
bindSwitch(root, R.id.home_automation_rule_weekly_switch, true);
+
+ bindSwipe(root, R.id.home_automation_rule_missed, archiveList);
+ bindSwipe(root, R.id.home_automation_rule_thanks, archiveList);
+ bindSwipe(root, R.id.home_automation_rule_weekly, archiveList);
+ applyVisibility(root, archiveList);
+ }
+
+ private static void bindSwipe(final View root, final int id, final boolean archiveList) {
+ final View card = root.findViewById(id);
+ if (card == null || card.getParent() instanceof HomeSwipeCard) {
+ return;
+ }
+ final String key = HomeAutomationRuleStore.keyForView(id);
+ HomeSwipeCard.install(card, new HomeSwipeCard.Listener() {
+ @Override
+ public void onArchive(final HomeSwipeCard swipe) {
+ if (archiveList) {
+ HomeAutomationRuleStore.unarchive(root.getContext(), key);
+ HomeToast.show(root.getContext(), R.string.home_automation_rule_restored);
+ } else {
+ HomeAutomationRuleStore.archive(root.getContext(), key);
+ HomeToast.show(root.getContext(), R.string.home_automation_rule_archived);
+ }
+ applyVisibility(root, archiveList);
+ }
+
+ @Override
+ public void onDelete(final HomeSwipeCard swipe) {
+ HomeAutomationRuleStore.delete(root.getContext(), key);
+ HomeToast.show(root.getContext(), R.string.home_automation_rule_deleted);
+ applyVisibility(root, archiveList);
+ }
+ });
+ }
+
+ private static void applyVisibility(final View root, final boolean archiveList) {
+ if (root == null) {
+ return;
+ }
+ int visible = 0;
+ for (final int id : HomeAutomationRuleStore.RULE_VIEW_IDS) {
+ visible += applyRuleVisibility(root, id, archiveList) ? 1 : 0;
+ }
+ final TextView count = root.findViewById(R.id.home_automation_rules_count);
+ if (count != null) {
+ count.setText(String.valueOf(visible));
+ count.setVisibility(visible > 0 ? View.VISIBLE : View.GONE);
+ }
+ final View empty = root.findViewById(R.id.home_archived_rules_empty);
+ if (empty != null) {
+ empty.setVisibility(archiveList && visible == 0 ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private static boolean applyRuleVisibility(final View root, final int id,
+ final boolean archiveList) {
+ View card = root.findViewById(id);
+ if (card == null) {
+ return false;
+ }
+ if (card.getParent() instanceof HomeSwipeCard) {
+ card = (View) card.getParent();
+ }
+ final String key = HomeAutomationRuleStore.keyForView(id);
+ final boolean deleted = HomeAutomationRuleStore.isDeleted(root.getContext(), key);
+ final boolean archived = HomeAutomationRuleStore.isArchived(root.getContext(), key);
+ final boolean show = archiveList ? (!deleted && archived) : (!deleted && !archived);
+ if (card instanceof HomeSwipeCard) {
+ if (show) {
+ ((HomeSwipeCard) card).setSwipeTranslationX(0f);
+ }
+ }
+ card.setVisibility(show ? View.VISIBLE : View.GONE);
+ return show;
}
private static void bindChip(final View root, final int chipId, final int labelId) {
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeAutomationRuleStore.java b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationRuleStore.java
new file mode 100644
index 0000000..abff1af
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationRuleStore.java
@@ -0,0 +1,120 @@
+/*
+ * 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.content.SharedPreferences;
+
+import com.android.messaging.R;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Local archive/delete flags for the demo automation rules. */
+public final class HomeAutomationRuleStore {
+ public static final String MISSED = "missed";
+ public static final String THANKS = "thanks";
+ public static final String WEEKLY = "weekly";
+
+ static final int[] RULE_VIEW_IDS = {
+ R.id.home_automation_rule_missed,
+ R.id.home_automation_rule_thanks,
+ R.id.home_automation_rule_weekly
+ };
+
+ private static final String PREFS = "home_automation_rules";
+ private static final String KEY_ARCHIVED = "archived";
+ private static final String KEY_DELETED = "deleted";
+
+ private HomeAutomationRuleStore() {
+ }
+
+ static String keyForView(final int viewId) {
+ if (viewId == R.id.home_automation_rule_missed) {
+ return MISSED;
+ }
+ if (viewId == R.id.home_automation_rule_thanks) {
+ return THANKS;
+ }
+ if (viewId == R.id.home_automation_rule_weekly) {
+ return WEEKLY;
+ }
+ return null;
+ }
+
+ public static boolean isArchived(final Context context, final String key) {
+ return set(context, KEY_ARCHIVED).contains(key);
+ }
+
+ public static boolean isDeleted(final Context context, final String key) {
+ return set(context, KEY_DELETED).contains(key);
+ }
+
+ public static void archive(final Context context, final String key) {
+ add(context, KEY_ARCHIVED, key);
+ remove(context, KEY_DELETED, key);
+ }
+
+ public static void unarchive(final Context context, final String key) {
+ remove(context, KEY_ARCHIVED, key);
+ }
+
+ public static void delete(final Context context, final String key) {
+ add(context, KEY_DELETED, key);
+ remove(context, KEY_ARCHIVED, key);
+ }
+
+ public static boolean hasArchived(final Context context) {
+ if (context == null) {
+ return false;
+ }
+ for (final String key : set(context, KEY_ARCHIVED)) {
+ if (!isDeleted(context, key)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static Set set(final Context context, final String key) {
+ return new HashSet<>(prefs(context).getStringSet(key, Collections.emptySet()));
+ }
+
+ private static void add(final Context context, final String key, final String value) {
+ if (value == null) {
+ return;
+ }
+ final Set next = set(context, key);
+ if (next.add(value)) {
+ prefs(context).edit().putStringSet(key, next).apply();
+ }
+ }
+
+ private static void remove(final Context context, final String key, final String value) {
+ if (value == null) {
+ return;
+ }
+ final Set next = set(context, key);
+ if (next.remove(value)) {
+ prefs(context).edit().putStringSet(key, next).apply();
+ }
+ }
+
+ private static SharedPreferences prefs(final Context context) {
+ return context.getApplicationContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE);
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeAutomationTarget.java b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationTarget.java
new file mode 100644
index 0000000..1705608
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationTarget.java
@@ -0,0 +1,86 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/** A contact or group chosen as an automation include/exclude target. */
+public final class HomeAutomationTarget implements Parcelable {
+
+ public final String name;
+ public final String destination;
+ public final boolean group;
+ public final long contactId;
+ public final String lookupKey;
+ public final String iconUri;
+
+ public HomeAutomationTarget(final String name, final String destination, final boolean group,
+ final long contactId, final String lookupKey, final String iconUri) {
+ this.name = name;
+ this.destination = destination;
+ this.group = group;
+ this.contactId = contactId;
+ this.lookupKey = lookupKey;
+ this.iconUri = iconUri;
+ }
+
+ protected HomeAutomationTarget(final Parcel in) {
+ name = in.readString();
+ destination = in.readString();
+ group = in.readInt() == 1;
+ contactId = in.readLong();
+ lookupKey = in.readString();
+ iconUri = in.readString();
+ }
+
+ public String key() {
+ return (group ? "g:" : "c:") + (destination == null ? "" : destination);
+ }
+
+ public String displayName() {
+ return TextUtils.isEmpty(name) ? destination : name;
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int flags) {
+ dest.writeString(name);
+ dest.writeString(destination);
+ dest.writeInt(group ? 1 : 0);
+ dest.writeLong(contactId);
+ dest.writeString(lookupKey);
+ dest.writeString(iconUri);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator CREATOR =
+ new Creator() {
+ @Override
+ public HomeAutomationTarget createFromParcel(final Parcel in) {
+ return new HomeAutomationTarget(in);
+ }
+
+ @Override
+ public HomeAutomationTarget[] newArray(final int size) {
+ return new HomeAutomationTarget[size];
+ }
+ };
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeAutomationTargetPickerSheet.java b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationTargetPickerSheet.java
new file mode 100644
index 0000000..000f372
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeAutomationTargetPickerSheet.java
@@ -0,0 +1,372 @@
+/*
+ * 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.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.CursorLoader;
+import androidx.loader.content.Loader;
+
+import com.android.ex.chips.RecipientEntry;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.ui.ContactIconView;
+import com.android.messaging.util.AvatarUriUtil;
+import com.android.messaging.util.ContactRecipientEntryUtils;
+import com.android.messaging.util.ContactUtil;
+import com.android.messaging.util.PhoneUtils;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+/** In-window sheet to pick contacts and contact groups as automation targets. */
+public class HomeAutomationTargetPickerSheet extends Fragment
+ implements HomeContactsFragment.GroupSelectionHost, LoaderManager.LoaderCallbacks {
+
+ public static final String REQUEST_KEY = "home_automation_target_picker";
+ public static final String EXTRA_TARGETS = "home_automation_targets";
+
+ private static final String TAG = "home_automation_target_picker";
+ private static final int GROUPS_LOADER = 1;
+
+ private View mDim;
+ private View mPanel;
+ private boolean mClosing;
+ private TextView mContactsTab;
+ private TextView mGroupsTab;
+ private View mContactsHost;
+ private ListView mGroupsList;
+ private HomeEmptyState mGroupsEmpty;
+ private final LinkedHashMap mSelectedGroups =
+ new LinkedHashMap<>();
+ private final ArrayList mGroups = new ArrayList<>();
+ private final GroupAdapter mGroupAdapter = new GroupAdapter();
+ private boolean mShowGroups;
+ private boolean mContactsSeeded;
+
+ public static void show(@NonNull final FragmentManager fragmentManager,
+ @Nullable final ArrayList incoming) {
+ final HomeAutomationTargetPickerSheet sheet = new HomeAutomationTargetPickerSheet();
+ final Bundle args = new Bundle();
+ args.putParcelableArrayList(EXTRA_TARGETS, incoming);
+ if (fragmentManager.findFragmentByTag(TAG) != null) {
+ return;
+ }
+ sheet.setArguments(args);
+ fragmentManager.beginTransaction()
+ .replace(R.id.home_automation_picker_host, sheet, TAG)
+ .commit();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull final LayoutInflater inflater,
+ @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.home_automation_target_picker_sheet, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mDim = view.findViewById(R.id.home_automation_picker_dim);
+ mPanel = view.findViewById(R.id.home_automation_picker_panel);
+ if (mDim != null) {
+ mDim.setOnClickListener(v -> dismissSheet());
+ }
+ view.findViewById(R.id.home_automation_picker_close).setOnClickListener(v -> dismissSheet());
+ view.findViewById(R.id.home_automation_picker_done).setOnClickListener(v -> finishWithResult());
+ final View host = requireActivity().findViewById(R.id.home_automation_picker_host);
+ if (host != null) {
+ host.setVisibility(View.VISIBLE);
+ }
+ requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
+ new OnBackPressedCallback(true) {
+ @Override
+ public void handleOnBackPressed() {
+ dismissSheet();
+ }
+ });
+ if (savedInstanceState == null) {
+ view.post(() -> HomeUiMotion.gallerySheetOpen(mDim, mPanel));
+ }
+
+ mContactsTab = view.findViewById(R.id.home_automation_picker_contacts);
+ mGroupsTab = view.findViewById(R.id.home_automation_picker_groups);
+ mContactsHost = view.findViewById(R.id.home_automation_picker_contacts_host);
+ mGroupsList = view.findViewById(R.id.home_automation_picker_groups_list);
+ mGroupsEmpty = view.findViewById(R.id.home_automation_picker_groups_empty);
+ mGroupsList.setAdapter(mGroupAdapter);
+ if (mGroupsEmpty != null) {
+ mGroupsEmpty.setContent(R.drawable.ic_home_automation_person,
+ R.string.home_automation_no_groups_title,
+ R.string.home_automation_no_groups_body);
+ }
+
+ HomeContactGroups.ensureDemo(requireContext());
+ seedIncoming(getArguments() == null ? null
+ : getArguments().getParcelableArrayList(EXTRA_TARGETS));
+ mContactsTab.setOnClickListener(v -> showGroups(false));
+ mGroupsTab.setOnClickListener(v -> showGroups(true));
+ showGroups(false);
+
+ prepareContactsFragment();
+ if (ContactUtil.hasReadContactsPermission()) {
+ LoaderManager.getInstance(this).initLoader(GROUPS_LOADER, null, this);
+ }
+ }
+
+ private void dismissSheet() {
+ if (mClosing || !isAdded()) {
+ return;
+ }
+ mClosing = true;
+ HomeUiMotion.gallerySheetClose(mDim, mPanel, () -> {
+ if (!isAdded()) {
+ return;
+ }
+ final View host = requireActivity().findViewById(R.id.home_automation_picker_host);
+ getParentFragmentManager().beginTransaction().remove(this).commitAllowingStateLoss();
+ if (host != null) {
+ host.setVisibility(View.GONE);
+ }
+ });
+ }
+
+ private void prepareContactsFragment() {
+ HomeContactsFragment fragment = getContactsFragment();
+ if (fragment == null) {
+ fragment = new HomeContactsFragment();
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.home_automation_picker_contacts_host, fragment)
+ .commitNow();
+ }
+ fragment.setGroupMode(true);
+ if (!mContactsSeeded) {
+ fragment.seedSelection(contactSeeds());
+ mContactsSeeded = true;
+ }
+ }
+
+ private ArrayList contactSeeds() {
+ final ArrayList incoming = getArguments() == null
+ ? null : getArguments().getParcelableArrayList(EXTRA_TARGETS);
+ final ArrayList seeds = new ArrayList<>();
+ if (incoming == null) {
+ return seeds;
+ }
+ for (final HomeAutomationTarget target : incoming) {
+ if (target != null && !target.group && target.destination != null) {
+ seeds.add(target.destination);
+ }
+ }
+ return seeds;
+ }
+
+ private void seedIncoming(final ArrayList incoming) {
+ mSelectedGroups.clear();
+ if (incoming == null) {
+ return;
+ }
+ for (final HomeAutomationTarget target : incoming) {
+ if (target != null && target.group && target.destination != null) {
+ mSelectedGroups.put(target.destination, target);
+ }
+ }
+ mGroupAdapter.notifyDataSetChanged();
+ }
+
+ private HomeContactsFragment getContactsFragment() {
+ return (HomeContactsFragment) getChildFragmentManager()
+ .findFragmentById(R.id.home_automation_picker_contacts_host);
+ }
+
+ private void showGroups(final boolean groups) {
+ mShowGroups = groups;
+ applyPill(mContactsTab, !groups);
+ applyPill(mGroupsTab, groups);
+ if (mContactsHost != null) {
+ mContactsHost.setVisibility(groups ? View.GONE : View.VISIBLE);
+ }
+ updateGroupsVisibility();
+ }
+
+ private void updateGroupsVisibility() {
+ final boolean empty = mGroups.isEmpty();
+ if (mGroupsList != null) {
+ mGroupsList.setVisibility(mShowGroups && !empty ? View.VISIBLE : View.GONE);
+ }
+ if (mGroupsEmpty != null) {
+ mGroupsEmpty.setVisibility(mShowGroups && empty ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private void applyPill(final TextView pill, final boolean selected) {
+ if (pill == null) {
+ return;
+ }
+ pill.setBackgroundResource(selected
+ ? R.drawable.home_bg_automation_pill_selected
+ : R.drawable.home_bg_automation_pill);
+ pill.setTextColor(ContextCompat.getColor(requireContext(),
+ selected ? android.R.color.white : R.color.home_text_primary));
+ }
+
+ @Override
+ public void onGroupSelectionChanged(final List selected) {
+ // Selection is read from the fragment when Done is pressed.
+ }
+
+ @NonNull
+ @Override
+ public Loader onCreateLoader(final int id, @Nullable final Bundle args) {
+ return new CursorLoader(requireContext(), HomeContactGroups.SUMMARY_URI,
+ HomeContactGroups.PROJECTION, HomeContactGroups.SELECTION, null,
+ HomeContactGroups.SORT);
+ }
+
+ @Override
+ public void onLoadFinished(@NonNull final Loader loader, final Cursor data) {
+ mGroups.clear();
+ if (data != null && data.moveToFirst()) {
+ do {
+ final HomeContactGroups.Item item = HomeContactGroups.fromCursor(data);
+ if (!item.title.isEmpty()) {
+ mGroups.add(item);
+ }
+ } while (data.moveToNext());
+ }
+ mGroupAdapter.notifyDataSetChanged();
+ updateGroupsVisibility();
+ }
+
+ @Override
+ public void onLoaderReset(@NonNull final Loader loader) {
+ mGroups.clear();
+ mGroupAdapter.notifyDataSetChanged();
+ updateGroupsVisibility();
+ }
+
+ private void finishWithResult() {
+ final LinkedHashMap targets = new LinkedHashMap<>();
+ final HomeContactsFragment fragment = getContactsFragment();
+ if (fragment != null) {
+ for (final RecipientEntry entry : fragment.getSelectedEntries()) {
+ final HomeAutomationTarget target = fromEntry(entry);
+ targets.put(target.key(), target);
+ }
+ }
+ for (final HomeAutomationTarget group : mSelectedGroups.values()) {
+ targets.put(group.key(), group);
+ }
+ final Bundle result = new Bundle();
+ result.putParcelableArrayList(EXTRA_TARGETS, new ArrayList<>(targets.values()));
+ getParentFragmentManager().setFragmentResult(REQUEST_KEY, result);
+ dismissSheet();
+ }
+
+ private static HomeAutomationTarget fromEntry(final RecipientEntry entry) {
+ final ParticipantData participant = ParticipantData.getFromRecipientEntry(entry);
+ final Uri icon = AvatarUriUtil.createAvatarUri(participant);
+ return new HomeAutomationTarget(
+ ContactRecipientEntryUtils.getDisplayNameForContactList(entry),
+ PhoneUtils.getDefault().getCanonicalBySystemLocale(entry.getDestination()),
+ false,
+ participant.getContactId(),
+ participant.getLookupKey(),
+ icon == null ? null : icon.toString());
+ }
+
+ private static HomeAutomationTarget fromGroup(final HomeContactGroups.Item item) {
+ final Uri icon = item.avatarUri();
+ return new HomeAutomationTarget(item.title, item.destination(), true, 0, null,
+ icon == null ? null : icon.toString());
+ }
+
+ private void applyCheck(final View check, final boolean selected) {
+ if (check instanceof ImageView) {
+ HomeAvatarOverlay.apply((ImageView) check, true, selected);
+ }
+ }
+
+ private final class GroupAdapter extends BaseAdapter {
+ @Override
+ public int getCount() {
+ return mGroups.size();
+ }
+
+ @Override
+ public HomeContactGroups.Item getItem(final int position) {
+ return mGroups.get(position);
+ }
+
+ @Override
+ public long getItemId(final int position) {
+ return getItem(position).id;
+ }
+
+ @Override
+ public View getView(final int position, final View convertView, final ViewGroup parent) {
+ final View row = convertView != null ? convertView
+ : LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.home_automation_target_row, parent, false);
+ final HomeContactGroups.Item item = getItem(position);
+ final ContactIconView avatar = row.findViewById(R.id.home_automation_target_avatar);
+ final TextView name = row.findViewById(R.id.home_automation_target_name);
+ final TextView meta = row.findViewById(R.id.home_automation_target_meta);
+ final View check = row.findViewById(R.id.home_automation_target_check);
+ avatar.setImageResourceUri(item.avatarUri());
+ avatar.setImageClickHandlerDisabled(true);
+ name.setText(item.title);
+ if (meta != null) {
+ if (item.memberCount > 0) {
+ meta.setVisibility(View.VISIBLE);
+ meta.setText(getResources().getQuantityString(
+ R.plurals.home_automation_group_member_count, item.memberCount,
+ item.memberCount));
+ } else {
+ meta.setVisibility(View.GONE);
+ }
+ }
+ final boolean selected = mSelectedGroups.containsKey(item.destination());
+ applyCheck(check, selected);
+ row.setOnClickListener(v -> {
+ final String id = item.destination();
+ if (mSelectedGroups.remove(id) == null) {
+ mSelectedGroups.put(id, fromGroup(item));
+ }
+ notifyDataSetChanged();
+ });
+ return row;
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeAvatarOverlay.java b/app/src/main/java/com/android/messaging/ui/home/HomeAvatarOverlay.java
new file mode 100644
index 0000000..10292d3
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeAvatarOverlay.java
@@ -0,0 +1,73 @@
+/*
+ * 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 and the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.home;
+
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.messaging.R;
+
+/** Avatar wash + check used while a people/message list is in edit/selection mode. */
+public final class HomeAvatarOverlay {
+ private HomeAvatarOverlay() {
+ }
+
+ public static void apply(final ImageView overlay, final boolean selectionMode,
+ final boolean selected) {
+ if (overlay == null) {
+ return;
+ }
+ if (!selectionMode) {
+ overlay.animate().cancel();
+ overlay.setBackground(null);
+ overlay.setImageDrawable(null);
+ overlay.setPadding(0, 0, 0, 0);
+ overlay.setVisibility(View.GONE);
+ overlay.setAlpha(1f);
+ overlay.setScaleX(1f);
+ overlay.setScaleY(1f);
+ return;
+ }
+ overlay.setBackgroundResource(selected
+ ? R.drawable.home_bg_avatar_selected_wash
+ : R.drawable.home_bg_avatar_edit);
+ if (selected) {
+ overlay.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ applyHeaderCheckRatio(overlay);
+ overlay.setImageResource(R.drawable.ic_home_check_stroke);
+ } else {
+ overlay.setPadding(0, 0, 0, 0);
+ overlay.setImageDrawable(null);
+ }
+ overlay.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Matches {@code ic_checkmark_circle_primary}: the tick is 16.3/42 of the circle. The
+ * stroke asset has extra viewport padding, so inset is reduced to keep that same visible size.
+ */
+ private static void applyHeaderCheckRatio(final ImageView overlay) {
+ int side = overlay.getWidth();
+ if (side <= 0 && overlay.getLayoutParams() != null) {
+ side = overlay.getLayoutParams().width;
+ }
+ if (side <= 0) {
+ side = overlay.getResources().getDimensionPixelSize(R.dimen.home_list_avatar_size);
+ }
+ final int inset = Math.round(side * ((42f - 16.3f) / 42f / 2f) * (16f / 24f));
+ overlay.setPadding(inset, inset, inset, inset);
+ }
+}
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
index e8a6a7b..d57ba01 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeCategoryScroller.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeCategoryScroller.java
@@ -69,17 +69,8 @@ public final class HomeCategoryScroller extends HorizontalScrollView {
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);
- }
+ HomeScrollEdgeFades.apply(this, host.findViewById(R.id.home_category_fade_start),
+ host.findViewById(R.id.home_category_fade_end));
}
@Override
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeComposeAttachPanel.java b/app/src/main/java/com/android/messaging/ui/home/HomeComposeAttachPanel.java
index c42ca41..5afeea5 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeComposeAttachPanel.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeComposeAttachPanel.java
@@ -254,6 +254,7 @@ public final class HomeComposeAttachPanel {
int imeBottom;
final int dockBaseBottomPadding;
final int originalStatusBarColor;
+ final int originalNavigationBarColor;
boolean expanded;
boolean closing;
boolean pendingComposeKeyboard;
@@ -300,6 +301,7 @@ public final class HomeComposeAttachPanel {
allItems.addAll(items);
albums.addAll(collectAlbums(items));
originalStatusBarColor = activity.getWindow().getStatusBarColor();
+ originalNavigationBarColor = activity.getWindow().getNavigationBarColor();
}
void open() {
@@ -320,8 +322,10 @@ public final class HomeComposeAttachPanel {
final int screenHeight = activity.getResources().getDisplayMetrics().heightPixels;
if (scrim != null) {
scrim.setAlpha(0f);
- scrim.setOnClickListener(null);
- scrim.setClickable(false);
+ scrim.setOnClickListener(v -> dismiss());
+ }
+ if (activity.getWindow() != null) {
+ activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
sheet.setTranslationY(screenHeight);
grid.setLayoutManager(new GridLayoutManager(activity, SPAN_COUNT));
@@ -424,11 +428,8 @@ public final class HomeComposeAttachPanel {
final int height = sheet.getHeight() > 0
? sheet.getHeight() : screenHeight;
sheet.setTranslationY(height);
- if (scrim != null) {
- scrim.setAlpha(0f);
- }
applyDockInsets();
- HomeUiMotion.gallerySheetOpen(null, sheet, peekY,
+ HomeUiMotion.gallerySheetOpen(scrim, sheet, peekY,
Session.this::notifyChatExtent);
return false;
}
@@ -488,7 +489,7 @@ public final class HomeComposeAttachPanel {
host.onDismiss();
final boolean openKeyboard = pendingComposeKeyboard;
pendingComposeKeyboard = false;
- HomeUiMotion.gallerySheetClose(null, sheet, () -> {
+ HomeUiMotion.gallerySheetClose(scrim, sheet, () -> {
host.onAttachSheetExtentChanged(0);
if (overlay.getParent() instanceof ViewGroup) {
((ViewGroup) overlay.getParent()).removeView(overlay);
@@ -591,6 +592,7 @@ public final class HomeComposeAttachPanel {
void applyWindowInsets(final WindowInsetsCompat insets) {
statusBarTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
navigationBarBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
+ applyBottomMargin(sheet, sheetBottomInset());
applyImeLift(Math.max(
insets.getInsets(WindowInsetsCompat.Type.ime()).bottom,
inferredImeHeight()));
@@ -613,13 +615,18 @@ public final class HomeComposeAttachPanel {
return;
}
imeBottom = ime;
- applyBottomMargin(sheet, imeBottom);
+ applyBottomMargin(sheet, sheetBottomInset());
applyBottomMargin(dock, imeBottom);
applyDockInsets();
applyExpandHeaderInsets(expanded);
}
+ int sheetBottomInset() {
+ return imeBottom > 0 ? imeBottom : navigationBarBottom;
+ }
+
void applyDockInsets() {
+ applyBottomMargin(sheet, sheetBottomInset());
if (dock != null) {
final int bottom = dockBaseBottomPadding
+ (imeBottom > 0 ? 0 : navigationBarBottom);
@@ -703,6 +710,7 @@ public final class HomeComposeAttachPanel {
void restoreStatusBar() {
if (activity.getWindow() != null) {
activity.getWindow().setStatusBarColor(originalStatusBarColor);
+ activity.getWindow().setNavigationBarColor(originalNavigationBarColor);
}
}
@@ -1338,10 +1346,17 @@ public final class HomeComposeAttachPanel {
private static void updateScrimForDrag(final View scrim, final float translation,
final float peekY, final int height) {
- if (scrim == null) {
+ if (scrim == null || height <= 0) {
return;
}
- scrim.setAlpha(0f);
+ final float peek = peekY > 0f ? peekY : height * 0.45f;
+ final float alpha;
+ if (translation <= peek) {
+ alpha = 1f;
+ } else {
+ alpha = 1f - (translation - peek) / Math.max(height - peek, 1f);
+ }
+ scrim.setAlpha(clamp(alpha, 0f, 1f));
}
private static float clamp(final float value, final float min, final float max) {
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeContactGroups.java b/app/src/main/java/com/android/messaging/ui/home/HomeContactGroups.java
new file mode 100644
index 0000000..65833ae
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeContactGroups.java
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.home;
+
+import android.Manifest;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+
+import com.android.messaging.util.AvatarUriUtil;
+import com.android.messaging.util.ContactUtil;
+import com.android.messaging.util.OsUtil;
+
+/** Android Contacts groups (not chat threads) for automation include/exclude. */
+final class HomeContactGroups {
+ static final String[] PROJECTION = {
+ Groups._ID,
+ Groups.TITLE,
+ Groups.SUMMARY_COUNT,
+ Groups.SUMMARY_WITH_PHONES
+ };
+ static final String SELECTION = Groups.DELETED + "=0 AND (" + Groups.AUTO_ADD
+ + "=0 OR " + Groups.AUTO_ADD + " IS NULL)";
+ static final String SORT = Groups.TITLE + " COLLATE LOCALIZED ASC";
+ static final Uri SUMMARY_URI = Groups.CONTENT_SUMMARY_URI;
+
+ private static final String PREFS = "home_demo";
+ private static final String KEY_SEEDED = "contact_groups_seeded_v1";
+ private static final String DEST_PREFIX = "cg:";
+
+ private HomeContactGroups() {
+ }
+
+ static void ensureDemo(final Context context) {
+ if (context == null || !OsUtil.hasPermission(Manifest.permission.WRITE_CONTACTS)
+ || !ContactUtil.hasReadContactsPermission()) {
+ return;
+ }
+ final SharedPreferences prefs =
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
+ if (prefs.getBoolean(KEY_SEEDED, false)) {
+ return;
+ }
+ final ContentResolver resolver = context.getContentResolver();
+ seedGroup(resolver, "Family",
+ "+15551230008", "+15551230003", "+15551230023", "+15551230005");
+ seedGroup(resolver, "Friends",
+ "+15551230006", "+15551230012", "+15551230015", "+15551230018",
+ "+15551230020", "+15551230007");
+ seedGroup(resolver, "Work",
+ "+15551230001", "+15551230002", "+15551230004", "+15551230011",
+ "+15551230016", "+15551230013");
+ seedGroup(resolver, "Clients",
+ "+15551230009", "+15551230010", "+15551230014", "+15551230017",
+ "+15551230021", "+15551230022");
+ prefs.edit().putBoolean(KEY_SEEDED, true).apply();
+ }
+
+ static Item fromCursor(final Cursor cursor) {
+ final long id = cursor.getLong(0);
+ final String title = cursor.getString(1);
+ int count = 0;
+ if (!cursor.isNull(3)) {
+ count = cursor.getInt(3);
+ } else if (!cursor.isNull(2)) {
+ count = cursor.getInt(2);
+ }
+ return new Item(id, title, count);
+ }
+
+ static final class Item {
+ final long id;
+ final String title;
+ final int memberCount;
+
+ Item(final long id, final String title, final int memberCount) {
+ this.id = id;
+ this.title = title == null ? "" : title;
+ this.memberCount = memberCount;
+ }
+
+ String destination() {
+ return DEST_PREFIX + id;
+ }
+
+ Uri avatarUri() {
+ if (TextUtils.isEmpty(title)) {
+ return AvatarUriUtil.createAvatarUri(null, null, destination(), null);
+ }
+ return AvatarUriUtil.createAvatarUri(null, title, destination(), null);
+ }
+ }
+
+ private static void seedGroup(final ContentResolver resolver, final String title,
+ final String... numbers) {
+ if (findGroupId(resolver, title) >= 0) {
+ return;
+ }
+ final ContentValues values = new ContentValues();
+ values.put(Groups.TITLE, title);
+ values.put(Groups.GROUP_VISIBLE, 1);
+ bindAccount(resolver, values);
+ final Uri inserted = resolver.insert(Groups.CONTENT_URI, values);
+ if (inserted == null) {
+ return;
+ }
+ final long groupId = android.content.ContentUris.parseId(inserted);
+ if (groupId <= 0 || numbers == null) {
+ return;
+ }
+ for (final String number : numbers) {
+ final long rawId = findRawContactId(resolver, number);
+ if (rawId > 0) {
+ addMembership(resolver, rawId, groupId);
+ }
+ }
+ }
+
+ private static long findGroupId(final ContentResolver resolver, final String title) {
+ try (Cursor cursor = resolver.query(Groups.CONTENT_URI,
+ new String[] { Groups._ID },
+ Groups.DELETED + "=0 AND " + Groups.TITLE + "=?",
+ new String[] { title }, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getLong(0);
+ }
+ }
+ return -1;
+ }
+
+ private static void bindAccount(final ContentResolver resolver, final ContentValues values) {
+ try (Cursor cursor = resolver.query(RawContacts.CONTENT_URI,
+ new String[] { RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE },
+ RawContacts.DELETED + "=0", null, RawContacts._ID + " ASC")) {
+ if (cursor != null && cursor.moveToFirst()) {
+ final String name = cursor.getString(0);
+ final String type = cursor.getString(1);
+ if (!TextUtils.isEmpty(type)) {
+ values.put(Groups.ACCOUNT_NAME, name);
+ values.put(Groups.ACCOUNT_TYPE, type);
+ }
+ }
+ }
+ }
+
+ private static long findRawContactId(final ContentResolver resolver, final String number) {
+ if (TextUtils.isEmpty(number)) {
+ return -1;
+ }
+ final String digits = digitsOnly(number);
+ try (Cursor cursor = resolver.query(Phone.CONTENT_URI,
+ new String[] { Phone.RAW_CONTACT_ID, Phone.NORMALIZED_NUMBER, Phone.NUMBER },
+ null, null, null)) {
+ if (cursor == null) {
+ return -1;
+ }
+ while (cursor.moveToNext()) {
+ final String normalized = cursor.getString(1);
+ final String raw = cursor.getString(2);
+ if (sameNumber(digits, normalized) || sameNumber(digits, raw)) {
+ return cursor.getLong(0);
+ }
+ }
+ }
+ return -1;
+ }
+
+ private static void addMembership(final ContentResolver resolver, final long rawContactId,
+ final long groupId) {
+ final ContentValues values = new ContentValues();
+ values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
+ values.put(ContactsContract.Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
+ values.put(GroupMembership.GROUP_ROW_ID, groupId);
+ resolver.insert(ContactsContract.Data.CONTENT_URI, values);
+ }
+
+ private static boolean sameNumber(final String expectedDigits, final String candidate) {
+ if (TextUtils.isEmpty(candidate)) {
+ return false;
+ }
+ return TextUtils.equals(expectedDigits, digitsOnly(candidate));
+ }
+
+ private static String digitsOnly(final String value) {
+ final StringBuilder out = new StringBuilder(value.length());
+ for (int i = 0; i < value.length(); i++) {
+ final char c = value.charAt(i);
+ if (c >= '0' && c <= '9') {
+ out.append(c);
+ }
+ }
+ return out.toString();
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeContactListAdapter.java b/app/src/main/java/com/android/messaging/ui/home/HomeContactListAdapter.java
index d468529..8c97e36 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeContactListAdapter.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeContactListAdapter.java
@@ -5,6 +5,7 @@ import android.database.Cursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.AbsListView;
import android.widget.CursorAdapter;
import android.widget.SectionIndexer;
@@ -47,6 +48,7 @@ public class HomeContactListAdapter extends CursorAdapter implements SectionInde
final ContactListItemView itemView = (ContactListItemView) view;
itemView.bind(cursor, mHost, mNeedAlphabetHeader, alphabetHeader);
itemView.setSectionBox(sectionRole(cursor.getPosition()));
+ wrapListRow(itemView);
}
private int sectionRole(final int position) {
@@ -78,8 +80,21 @@ public class HomeContactListAdapter extends CursorAdapter implements SectionInde
@Override
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
- return LayoutInflater.from(context).inflate(R.layout.home_contacts_list_item, parent,
- false);
+ final View view = LayoutInflater.from(context).inflate(R.layout.home_contacts_list_item,
+ parent, false);
+ wrapListRow(view);
+ return view;
+ }
+
+ private static void wrapListRow(final View view) {
+ ViewGroup.LayoutParams params = view.getLayoutParams();
+ if (params instanceof AbsListView.LayoutParams) {
+ params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ view.setLayoutParams(params);
+ return;
+ }
+ view.setLayoutParams(new AbsListView.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
@Override
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeContactsFragment.java b/app/src/main/java/com/android/messaging/ui/home/HomeContactsFragment.java
index ba531a4..48735ea 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeContactsFragment.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeContactsFragment.java
@@ -60,6 +60,7 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
private Cursor mAllContactsCursor;
private String mFilterText;
private boolean mGroupMode;
+ private java.util.Set mPendingSeed;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
@@ -85,6 +86,10 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
mSendToView = view.findViewById(R.id.home_contacts_send_to);
mAdapter = new HomeContactListAdapter(requireContext(), null, this);
listView.setAdapter(mAdapter);
+ if (getParentFragment() instanceof GroupSelectionHost
+ || getActivity() instanceof GroupSelectionHost) {
+ mGroupMode = true;
+ }
updateResultViews();
}
@@ -103,6 +108,16 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
}
}
+ public List getSelectedEntries() {
+ return new ArrayList<>(mSelected.values());
+ }
+
+ public void seedSelection(final List destinations) {
+ mPendingSeed = destinations == null
+ ? null : new java.util.LinkedHashSet<>(destinations);
+ applyPendingSeed();
+ }
+
public ArrayList getSelectedParticipants() {
final ArrayList participants = new ArrayList<>(mSelected.size());
for (final RecipientEntry entry : mSelected.values()) {
@@ -163,6 +178,7 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
mAllContactsCursor = data;
if (TextUtils.isEmpty(mFilterText)) {
mAdapter.changeCursor(data);
+ applyPendingSeed();
updateResultViews();
}
}
@@ -172,6 +188,7 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
mBinding.ensureBound();
// A null cursor means the search was cleared, so fall back to the full list.
mAdapter.changeCursor(data != null ? data : mAllContactsCursor);
+ applyPendingSeed();
updateResultViews();
}
@@ -204,12 +221,40 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
}
}
+ private void applyPendingSeed() {
+ if (mPendingSeed == null || mPendingSeed.isEmpty() || mAdapter == null) {
+ return;
+ }
+ final Cursor cursor = mAdapter.getCursor();
+ if (cursor == null || cursor.getCount() == 0) {
+ return;
+ }
+ final ContactListItemData item = DataModel.get().createContactListItemData();
+ final int position = cursor.getPosition();
+ if (cursor.moveToFirst()) {
+ do {
+ item.bind(cursor, null);
+ final RecipientEntry entry = item.getRecipientEntry();
+ final String key = selectionKey(entry);
+ if (mPendingSeed.remove(key)) {
+ mSelected.put(key, entry);
+ }
+ } while (cursor.moveToNext() && !mPendingSeed.isEmpty());
+ }
+ cursor.moveToPosition(position);
+ notifyGroupSelectionChanged();
+ mAdapter.notifyDataSetChanged();
+ }
+
private static String selectionKey(final RecipientEntry entry) {
return PhoneUtils.getDefault().getCanonicalBySystemLocale(entry.getDestination());
}
private void notifyGroupSelectionChanged() {
- if (getActivity() instanceof GroupSelectionHost) {
+ if (getParentFragment() instanceof GroupSelectionHost) {
+ ((GroupSelectionHost) getParentFragment()).onGroupSelectionChanged(
+ new ArrayList<>(mSelected.values()));
+ } else if (getActivity() instanceof GroupSelectionHost) {
((GroupSelectionHost) getActivity()).onGroupSelectionChanged(
new ArrayList<>(mSelected.values()));
}
@@ -229,6 +274,12 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
return mGroupMode && mSelected.containsKey(selectionKey(item.getRecipientEntry()));
}
+ @Override
+ public boolean isContactSelectionMode() {
+ return mGroupMode || getParentFragment() instanceof GroupSelectionHost
+ || getActivity() instanceof HomeComposeActivity;
+ }
+
@Override
@RunsOnMainThread
public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor, final Object data,
@@ -270,7 +321,20 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
mSendToView.bind(entry, getString(R.string.home_compose_send_to,
ContactRecipientEntryUtils.formatDestination(entry)), null, this, true, false);
// The RecipientEntry bind path is meant for dropdowns, which handle clicks themselves.
- mSendToView.setOnClickListener(v -> startConversation(entry));
+ mSendToView.setOnClickListener(v -> {
+ if (mGroupMode) {
+ final String key = selectionKey(entry);
+ if (mSelected.remove(key) == null) {
+ mSelected.put(key, entry);
+ }
+ notifyGroupSelectionChanged();
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+ } else {
+ startConversation(entry);
+ }
+ });
mSendToView.setVisibility(View.VISIBLE);
} else {
mSendToView.setVisibility(View.GONE);
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeDemoGroupSeed.java b/app/src/main/java/com/android/messaging/ui/home/HomeDemoGroupSeed.java
new file mode 100644
index 0000000..01a0eca
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeDemoGroupSeed.java
@@ -0,0 +1,173 @@
+/*
+ * 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 and the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.home;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.Telephony;
+
+import androidx.annotation.NonNull;
+
+import com.android.messaging.Factory;
+import com.android.messaging.datamodel.BugleDatabaseOperations;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
+import com.android.messaging.datamodel.DatabaseWrapper;
+import com.android.messaging.datamodel.MessagingContentProvider;
+import com.android.messaging.datamodel.action.Action;
+import com.android.messaging.datamodel.data.ConversationListItemData;
+import com.android.messaging.datamodel.data.MessageData;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.sms.MmsUtils;
+
+import java.util.ArrayList;
+
+/** One-shot demo groups for the emulator preview (Weekend hike, Family, Looply launch, Game night). */
+public final class HomeDemoGroupSeed extends Action {
+ private static final String PREFS = "home_demo";
+ private static final String KEY_SEEDED = "groups_seeded_v2";
+
+ public static void ensure(final Context context) {
+ if (context == null) {
+ return;
+ }
+ if (context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(KEY_SEEDED,
+ false)) {
+ return;
+ }
+ new HomeDemoGroupSeed().start();
+ }
+
+ private HomeDemoGroupSeed() {
+ }
+
+ @Override
+ protected Object executeAction() {
+ final Context context = Factory.get().getApplicationContext();
+ final SharedPreferences prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
+ if (prefs.getBoolean(KEY_SEEDED, false)) {
+ return null;
+ }
+
+ final long now = System.currentTimeMillis();
+ seed(context, "Weekend hike", "Bring extra water.", now,
+ "+15551230006", "+15551230012", "+15551230007");
+ seed(context, "Family", "Dinner this Sunday?", now - 60_000L,
+ "+15551230008", "+15551230005", "+15551230003");
+ seed(context, "Looply launch", "Ship checklist looks good.", now - 120_000L,
+ "+15551230002", "+15551230016", "+15551230011");
+ seed(context, "Game night", "Friday at 8?", now - 180_000L,
+ "+15551230015", "+15551230018", "+15551230020");
+
+ prefs.edit().putBoolean(KEY_SEEDED, true).apply();
+ MessagingContentProvider.notifyConversationListChanged();
+ return null;
+ }
+
+ private static void seed(final Context context, final String name, final String snippet,
+ final long timestamp, final String... numbers) {
+ final ArrayList participants = new ArrayList<>();
+ for (final String number : numbers) {
+ participants.add(ParticipantData.getFromRawPhoneBySystemLocale(number));
+ }
+ BugleDatabaseOperations.sanitizeConversationParticipants(participants);
+ final ArrayList recipients =
+ BugleDatabaseOperations.getRecipientsFromConversationParticipants(participants);
+ final long threadId = MmsUtils.getOrCreateThreadId(context, recipients);
+ if (threadId < 0) {
+ return;
+ }
+ final DatabaseWrapper db = DataModel.get().getDatabase();
+ final String conversationId = BugleDatabaseOperations.getOrCreateConversation(
+ db, threadId, false, participants);
+ if (conversationId == null) {
+ return;
+ }
+ final ContentValues values = new ContentValues();
+ values.put(ConversationColumns.NAME, name);
+ values.put(ConversationColumns.SNIPPET_TEXT, snippet);
+ values.put(ConversationColumns.SORT_TIMESTAMP, timestamp);
+ values.put(ConversationColumns.LAST_READ_TIMESTAMP, timestamp);
+ BugleDatabaseOperations.updateConversationRowIfExists(db, conversationId, values);
+ insertSeedMessage(context, db, conversationId, threadId, numbers[0], snippet, timestamp);
+ values.clear();
+ values.put(ConversationColumns.NAME, name);
+ BugleDatabaseOperations.updateConversationRowIfExists(db, conversationId, values);
+ }
+
+ private static void insertSeedMessage(final Context context, final DatabaseWrapper db,
+ final String conversationId, final long threadId, final String sender,
+ final String snippet, final long timestamp) {
+ final ContentValues sms = new ContentValues();
+ sms.put(Telephony.Sms.ADDRESS, sender);
+ sms.put(Telephony.Sms.BODY, snippet);
+ sms.put(Telephony.Sms.DATE, timestamp);
+ sms.put(Telephony.Sms.DATE_SENT, timestamp);
+ sms.put(Telephony.Sms.READ, 1);
+ sms.put(Telephony.Sms.SEEN, 1);
+ sms.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_INBOX);
+ sms.put(Telephony.Sms.THREAD_ID, threadId);
+ final Uri smsUri = context.getContentResolver().insert(Telephony.Sms.CONTENT_URI, sms);
+ if (smsUri == null) {
+ return;
+ }
+ final ConversationListItemData conversation =
+ ConversationListItemData.getExistingConversation(db, conversationId);
+ final ArrayList members =
+ BugleDatabaseOperations.getParticipantsForConversation(db, conversationId);
+ if (conversation == null || members.isEmpty()) {
+ return;
+ }
+ final MessageData message = MessageData.createReceivedSmsMessage(
+ smsUri, conversationId, members.get(0).getId(), conversation.getSelfId(),
+ snippet, null, timestamp, timestamp, true, true);
+ db.beginTransaction();
+ try {
+ BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
+ BugleDatabaseOperations.updateConversationMetadataInTransaction(
+ db, conversationId, message.getMessageId(), timestamp, false, false);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private HomeDemoGroupSeed(final Parcel in) {
+ super(in);
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator<>() {
+ @Override
+ public HomeDemoGroupSeed createFromParcel(final Parcel in) {
+ return new HomeDemoGroupSeed(in);
+ }
+
+ @Override
+ public HomeDemoGroupSeed[] newArray(final int size) {
+ return new HomeDemoGroupSeed[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
+ writeActionToParcel(parcel, flags);
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeFadeScrollView.java b/app/src/main/java/com/android/messaging/ui/home/HomeFadeScrollView.java
new file mode 100644
index 0000000..8ad45fc
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeFadeScrollView.java
@@ -0,0 +1,80 @@
+/*
+ * 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.HorizontalScrollView;
+
+/** Horizontal scroller that shows sibling start/end fades except at the first and last offsets. */
+public final class HomeFadeScrollView extends HorizontalScrollView {
+
+ public HomeFadeScrollView(final Context context) {
+ this(context, null);
+ }
+
+ public HomeFadeScrollView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public HomeFadeScrollView(final Context context, final AttributeSet attrs,
+ final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setHorizontalScrollBarEnabled(false);
+ setHorizontalFadingEdgeEnabled(false);
+ setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS);
+ }
+
+ public void centerChild(final View child) {
+ if (child == null) {
+ return;
+ }
+ post(() -> {
+ if (getWidth() == 0 || child.getWidth() == 0) {
+ return;
+ }
+ final int childCenter = offsetToScrollContent(child) + child.getWidth() / 2;
+ final int target = childCenter - getWidth() / 2;
+ final int max = HomeScrollEdgeFades.maxScrollX(this);
+ smoothScrollTo(Math.max(0, Math.min(target, max)), 0);
+ });
+ }
+
+ private int offsetToScrollContent(final View child) {
+ int offset = child.getLeft();
+ View parent = child.getParent() instanceof View ? (View) child.getParent() : null;
+ final View content = getChildCount() == 0 ? null : getChildAt(0);
+ while (parent != null && parent != this && parent != content) {
+ offset += parent.getLeft();
+ parent = parent.getParent() instanceof View ? (View) parent.getParent() : null;
+ }
+ return offset;
+ }
+
+ @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);
+ HomeScrollEdgeFades.applyFromParent(this);
+ }
+
+ @Override
+ protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ HomeScrollEdgeFades.applyFromParent(this);
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeScrollEdgeFades.java b/app/src/main/java/com/android/messaging/ui/home/HomeScrollEdgeFades.java
new file mode 100644
index 0000000..12c3276
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeScrollEdgeFades.java
@@ -0,0 +1,66 @@
+/*
+ * 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.view.View;
+import android.widget.HorizontalScrollView;
+
+/** Left/right edge fades while scrolling; hidden at the absolute start and end. */
+final class HomeScrollEdgeFades {
+ private static final String TAG_START = "home_fade_start";
+ private static final String TAG_END = "home_fade_end";
+
+ private HomeScrollEdgeFades() {
+ }
+
+ static void applyFromParent(final HorizontalScrollView scroll) {
+ if (scroll == null || !(scroll.getParent() instanceof View)) {
+ return;
+ }
+ final View host = (View) scroll.getParent();
+ apply(scroll, host.findViewWithTag(TAG_START), host.findViewWithTag(TAG_END));
+ }
+
+ static void apply(final HorizontalScrollView scroll, final View start, final View end) {
+ if (scroll == null) {
+ return;
+ }
+ final int max = maxScrollX(scroll);
+ final boolean overflow = max > 0;
+ final int x = scroll.getScrollX();
+ setVisible(start, overflow && x > 0);
+ setVisible(end, overflow && x < max);
+ }
+
+ static int maxScrollX(final HorizontalScrollView scroll) {
+ if (scroll.getChildCount() == 0) {
+ return 0;
+ }
+ final View child = scroll.getChildAt(0);
+ return Math.max(0, child.getWidth()
+ - (scroll.getWidth() - scroll.getPaddingLeft() - scroll.getPaddingRight()));
+ }
+
+ private static void setVisible(final View fade, final boolean visible) {
+ if (fade == null) {
+ return;
+ }
+ final int vis = visible ? View.VISIBLE : View.GONE;
+ if (fade.getVisibility() != vis) {
+ fade.setVisibility(vis);
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeSpamFragment.java b/app/src/main/java/com/android/messaging/ui/home/HomeSpamFragment.java
index 6373c5e..dc7f4fa 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeSpamFragment.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeSpamFragment.java
@@ -402,9 +402,7 @@ public class HomeSpamFragment extends Fragment implements BlockedParticipantsDat
private final TextView mName;
private final TextView mDetails;
private final View mUnblock;
- private final View mIndicator;
- private final View mRing;
- private final ImageView mCheck;
+ private final ImageView mAvatarCheck;
private ParticipantListItemData mData;
SpamHolder(final View itemView) {
@@ -413,9 +411,7 @@ public class HomeSpamFragment extends Fragment implements BlockedParticipantsDat
mName = itemView.findViewById(R.id.home_spam_name);
mDetails = itemView.findViewById(R.id.home_spam_details);
mUnblock = itemView.findViewById(R.id.home_spam_unblock);
- mIndicator = itemView.findViewById(R.id.home_spam_selection_indicator);
- mRing = itemView.findViewById(R.id.home_spam_selection_ring);
- mCheck = itemView.findViewById(R.id.home_spam_selection_check);
+ mAvatarCheck = itemView.findViewById(R.id.home_spam_avatar_check);
itemView.setOnClickListener(v -> onRowClicked(mData));
mUnblock.setOnClickListener(v -> confirmUnblock(mData));
}
@@ -434,18 +430,7 @@ public class HomeSpamFragment extends Fragment implements BlockedParticipantsDat
data.getLookupKey(), data.getNormalizedDestination());
mAvatar.setClickable(!mSelecting);
mUnblock.setVisibility(mSelecting ? View.GONE : View.VISIBLE);
- if (mIndicator != null) {
- mIndicator.setVisibility(mSelecting ? View.VISIBLE : View.GONE);
- }
- final boolean selected = mSelecting && isSelected(data);
- if (mRing != null) {
- mRing.setBackgroundResource(selected
- ? R.drawable.home_bg_selection_ring_checked
- : R.drawable.home_bg_selection_ring);
- }
- if (mCheck != null) {
- mCheck.setVisibility(selected ? View.VISIBLE : View.GONE);
- }
+ HomeAvatarOverlay.apply(mAvatarCheck, mSelecting, mSelecting && isSelected(data));
}
}
}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeSwipeCard.java b/app/src/main/java/com/android/messaging/ui/home/HomeSwipeCard.java
new file mode 100644
index 0000000..4d13f36
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeSwipeCard.java
@@ -0,0 +1,336 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.FrameLayout;
+
+import com.android.messaging.R;
+import com.android.messaging.util.UiUtils;
+
+/** Home-style swipe actions (archive / delete) without the notification mute button. */
+public final class HomeSwipeCard extends FrameLayout {
+
+ public interface Listener {
+ void onArchive(HomeSwipeCard card);
+ void onDelete(HomeSwipeCard card);
+ }
+
+ private static HomeSwipeCard sOpenCard;
+
+ private View mBackground;
+ private View mArchive;
+ private View mDelete;
+ private View mContent;
+ private Listener mListener;
+ private float mTranslation;
+ private float mDownX;
+ private float mDownY;
+ private float mStartTranslation;
+ private boolean mDragging;
+ private boolean mDidSwipe;
+ private final int mTouchSlop;
+ private final int mMinFling;
+ private VelocityTracker mVelocityTracker;
+
+ public HomeSwipeCard(final Context context) {
+ this(context, null);
+ }
+
+ public HomeSwipeCard(final Context context, final AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public HomeSwipeCard(final Context context, final AttributeSet attrs, final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mMinFling = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
+ setClipChildren(true);
+ setClipToPadding(true);
+ LayoutInflater.from(context).inflate(R.layout.home_swipe_card_actions, this, true);
+ mBackground = findViewById(R.id.home_swipe_background);
+ mArchive = findViewById(R.id.home_swipe_archive);
+ mDelete = findViewById(R.id.home_swipe_delete);
+ if (mDelete != null) {
+ mDelete.setOnClickListener(v -> finishDelete());
+ }
+ }
+
+ public static HomeSwipeCard install(final View content, final Listener listener) {
+ if (content == null || !(content.getParent() instanceof ViewGroup)) {
+ return null;
+ }
+ final ViewGroup parent = (ViewGroup) content.getParent();
+ final int index = parent.indexOfChild(content);
+ final ViewGroup.LayoutParams lp = content.getLayoutParams();
+ parent.removeView(content);
+ final HomeSwipeCard card = new HomeSwipeCard(content.getContext());
+ card.mContent = content;
+ card.mListener = listener;
+ content.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT));
+ card.addView(content);
+ parent.addView(card, index, lp);
+ return card;
+ }
+
+ public float getSwipeTranslationX() {
+ return mTranslation;
+ }
+
+ public void setSwipeTranslationX(final float translationX) {
+ mTranslation = translationX;
+ if (mContent != null) {
+ mContent.setTranslationX(translationX);
+ }
+ if (mBackground == null) {
+ return;
+ }
+ if (translationX == 0f) {
+ mBackground.setVisibility(GONE);
+ if (mArchive != null) {
+ mArchive.setVisibility(GONE);
+ }
+ if (mDelete != null) {
+ mDelete.setVisibility(GONE);
+ }
+ return;
+ }
+ mBackground.setVisibility(VISIBLE);
+ if (translationX > 0f) {
+ if (mArchive != null) {
+ mArchive.setVisibility(VISIBLE);
+ }
+ if (mDelete != null) {
+ mDelete.setVisibility(GONE);
+ }
+ } else {
+ if (mArchive != null) {
+ mArchive.setVisibility(GONE);
+ }
+ if (mDelete != null) {
+ mDelete.setVisibility(VISIBLE);
+ }
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(final MotionEvent ev) {
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownX = ev.getX();
+ mDownY = ev.getY();
+ mStartTranslation = mTranslation;
+ mDragging = false;
+ mDidSwipe = false;
+ if (isTouchOnDelete(ev)) {
+ return false;
+ }
+ obtainTracker().addMovement(ev);
+ if (sOpenCard != null && sOpenCard != this) {
+ sOpenCard.animateRestore();
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (isHorizontalDrag(ev)) {
+ mDragging = true;
+ mDidSwipe = true;
+ disallowParent(true);
+ closeOthers();
+ return true;
+ }
+ break;
+ default:
+ break;
+ }
+ return mDragging;
+ }
+
+ @Override
+ public boolean onTouchEvent(final MotionEvent ev) {
+ obtainTracker().addMovement(ev);
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_MOVE:
+ if (mDragging || isHorizontalDrag(ev)) {
+ mDragging = true;
+ mDidSwipe = true;
+ disallowParent(true);
+ setSwipeTranslationX(clamp(ev.getX() - mDownX + mStartTranslation));
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (mDragging) {
+ settle(ev.getActionMasked() == MotionEvent.ACTION_CANCEL);
+ } else if (mTranslation != 0f && !isTouchOnDelete(ev)) {
+ animateRestore();
+ }
+ mDragging = false;
+ disallowParent(false);
+ recycleTracker();
+ return mDidSwipe;
+ default:
+ break;
+ }
+ return mDragging || super.onTouchEvent(ev);
+ }
+
+ private boolean isOpen() {
+ return mTranslation <= -revealWidth() * 0.85f;
+ }
+
+ private float revealWidth() {
+ final int button = getResources().getDimensionPixelSize(
+ R.dimen.home_swipe_action_size);
+ return button + getResources().getDimensionPixelSize(R.dimen.looply_space_lg);
+ }
+
+ private float clamp(final float translationX) {
+ if (translationX < 0f) {
+ return Math.max(translationX, -revealWidth() * 1.2f);
+ }
+ return translationX;
+ }
+
+ 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 isTouchOnDelete(final MotionEvent ev) {
+ if (mDelete == null || mDelete.getVisibility() != VISIBLE || !isOpen()) {
+ return false;
+ }
+ final int[] parentLoc = new int[2];
+ getLocationOnScreen(parentLoc);
+ final int[] deleteLoc = new int[2];
+ mDelete.getLocationOnScreen(deleteLoc);
+ final float x = parentLoc[0] + ev.getX();
+ final float y = parentLoc[1] + ev.getY();
+ return x >= deleteLoc[0] && x <= deleteLoc[0] + mDelete.getWidth()
+ && y >= deleteLoc[1] && y <= deleteLoc[1] + mDelete.getHeight();
+ }
+
+ private void settle(final boolean cancel) {
+ mVelocityTracker.computeCurrentVelocity(1000);
+ final float vx = mVelocityTracker.getXVelocity();
+ final boolean fast = Math.abs(vx) > mMinFling && Math.abs(vx) > Math.abs(
+ mVelocityTracker.getYVelocity());
+ if (!cancel && mTranslation > 0f
+ && (mTranslation > getWidth() * 0.4f || (fast && vx > 0f))) {
+ finishArchive();
+ return;
+ }
+ if (!cancel && mTranslation < 0f
+ && Math.abs(mTranslation) > revealWidth() * 0.45f) {
+ animateReveal();
+ return;
+ }
+ animateRestore();
+ }
+
+ private void animateReveal() {
+ sOpenCard = this;
+ animateTo(-revealWidth(), null);
+ }
+
+ private void animateRestore() {
+ if (sOpenCard == this) {
+ sOpenCard = null;
+ }
+ animateTo(0f, null);
+ }
+
+ private void finishArchive() {
+ if (sOpenCard == this) {
+ sOpenCard = null;
+ }
+ animateTo(getWidth(), () -> {
+ setVisibility(GONE);
+ if (mListener != null) {
+ mListener.onArchive(this);
+ }
+ });
+ }
+
+ private void finishDelete() {
+ if (sOpenCard == this) {
+ sOpenCard = null;
+ }
+ animateTo(-getWidth(), () -> {
+ setVisibility(GONE);
+ if (mListener != null) {
+ mListener.onDelete(this);
+ }
+ });
+ }
+
+ private void animateTo(final float target, final Runnable end) {
+ final ObjectAnimator animator = ObjectAnimator.ofFloat(this, "swipeTranslationX",
+ mTranslation, target);
+ animator.setDuration(getResources().getInteger(R.integer.swipe_duration_ms));
+ animator.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(final Animator animation) {
+ if (end != null) {
+ end.run();
+ }
+ }
+ });
+ animator.start();
+ }
+
+ private void closeOthers() {
+ if (sOpenCard != null && sOpenCard != this) {
+ sOpenCard.animateRestore();
+ }
+ }
+
+ private void disallowParent(final boolean disallow) {
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(disallow);
+ }
+ }
+
+ private VelocityTracker obtainTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ return mVelocityTracker;
+ }
+
+ private void recycleTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/home_bg_automation_icon_on.xml b/app/src/main/res/drawable/home_bg_automation_icon_on.xml
new file mode 100644
index 0000000..097d152
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_automation_icon_on.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_automation_option.xml b/app/src/main/res/drawable/home_bg_automation_option.xml
new file mode 100644
index 0000000..fc55b0c
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_automation_option.xml
@@ -0,0 +1,10 @@
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_automation_option_selected.xml b/app/src/main/res/drawable/home_bg_automation_option_selected.xml
new file mode 100644
index 0000000..f0417c9
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_automation_option_selected.xml
@@ -0,0 +1,13 @@
+
+
+ -
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_automation_pill.xml b/app/src/main/res/drawable/home_bg_automation_pill.xml
new file mode 100644
index 0000000..c74d7cc
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_automation_pill.xml
@@ -0,0 +1,10 @@
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_automation_pill_selected.xml b/app/src/main/res/drawable/home_bg_automation_pill_selected.xml
new file mode 100644
index 0000000..40c5850
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_automation_pill_selected.xml
@@ -0,0 +1,10 @@
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_automation_preview.xml b/app/src/main/res/drawable/home_bg_automation_preview.xml
new file mode 100644
index 0000000..d151a99
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_automation_preview.xml
@@ -0,0 +1,9 @@
+
+
+ -
+
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_automation_recommended.xml b/app/src/main/res/drawable/home_bg_automation_recommended.xml
new file mode 100644
index 0000000..31c3b1f
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_automation_recommended.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_automation_row_ripple.xml b/app/src/main/res/drawable/home_bg_automation_row_ripple.xml
new file mode 100644
index 0000000..ee1f28f
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_automation_row_ripple.xml
@@ -0,0 +1,9 @@
+
+
+ -
+
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_avatar_edit.xml b/app/src/main/res/drawable/home_bg_avatar_edit.xml
new file mode 100644
index 0000000..7857973
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_avatar_edit.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_avatar_selected.xml b/app/src/main/res/drawable/home_bg_avatar_selected.xml
new file mode 100644
index 0000000..4033e6c
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_avatar_selected.xml
@@ -0,0 +1,13 @@
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_avatar_selected_wash.xml b/app/src/main/res/drawable/home_bg_avatar_selected_wash.xml
new file mode 100644
index 0000000..4c573dd
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_avatar_selected_wash.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home_automation_chat.xml b/app/src/main/res/drawable/ic_home_automation_chat.xml
new file mode 100644
index 0000000..dab48aa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home_automation_chat.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home_automation_person.xml b/app/src/main/res/drawable/ic_home_automation_person.xml
new file mode 100644
index 0000000..b1a717e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home_automation_person.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home_automation_phone.xml b/app/src/main/res/drawable/ic_home_automation_phone.xml
new file mode 100644
index 0000000..ac73f09
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home_automation_phone.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home_check_stroke.xml b/app/src/main/res/drawable/ic_home_check_stroke.xml
new file mode 100644
index 0000000..b75f2bf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home_check_stroke.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/layout/contact_list_item_view.xml b/app/src/main/res/layout/contact_list_item_view.xml
index db2009b..b3f95cf 100644
--- a/app/src/main/res/layout/contact_list_item_view.xml
+++ b/app/src/main/res/layout/contact_list_item_view.xml
@@ -44,27 +44,28 @@
android:visibility="gone" />
-
+ android:clickable="false"
+ android:focusable="false"
+ android:src="@drawable/home_bg_avatar_selected"
+ android:visibility="gone" />
+ android:src="@drawable/home_bg_avatar_selected"
+ android:visibility="gone" />
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_archived_rules.xml b/app/src/main/res/layout/home_archived_rules.xml
new file mode 100644
index 0000000..79fc583
--- /dev/null
+++ b/app/src/main/res/layout/home_archived_rules.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_automation_create_activity.xml b/app/src/main/res/layout/home_automation_create_activity.xml
index 4163b5b..ef8fec4 100644
--- a/app/src/main/res/layout/home_automation_create_activity.xml
+++ b/app/src/main/res/layout/home_automation_create_activity.xml
@@ -1,5 +1,6 @@
-
-
-
+ android:gravity="center_vertical"
+ android:minHeight="@dimen/home_header_height"
+ android:orientation="horizontal"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_marginBottom="@dimen/looply_space_md"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_automation_pane.xml b/app/src/main/res/layout/home_automation_pane.xml
index 4a8a3c1..f93b4ce 100644
--- a/app/src/main/res/layout/home_automation_pane.xml
+++ b/app/src/main/res/layout/home_automation_pane.xml
@@ -106,7 +106,7 @@
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
- android:src="@drawable/ic_home_nav_calls"
+ android:src="@drawable/ic_home_automation_phone"
app:tint="@color/home_text_primary" />
-
+ android:layout_marginTop="@dimen/looply_space_md" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_marginTop="@dimen/looply_space_md" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_marginTop="@dimen/looply_space_md" />
diff --git a/app/src/main/res/layout/home_automation_rule_missed.xml b/app/src/main/res/layout/home_automation_rule_missed.xml
new file mode 100644
index 0000000..9f9f9cc
--- /dev/null
+++ b/app/src/main/res/layout/home_automation_rule_missed.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_automation_rule_thanks.xml b/app/src/main/res/layout/home_automation_rule_thanks.xml
new file mode 100644
index 0000000..3dc8ec8
--- /dev/null
+++ b/app/src/main/res/layout/home_automation_rule_thanks.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_automation_rule_weekly.xml b/app/src/main/res/layout/home_automation_rule_weekly.xml
new file mode 100644
index 0000000..226f972
--- /dev/null
+++ b/app/src/main/res/layout/home_automation_rule_weekly.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_automation_target_chip.xml b/app/src/main/res/layout/home_automation_target_chip.xml
new file mode 100644
index 0000000..7f1ab54
--- /dev/null
+++ b/app/src/main/res/layout/home_automation_target_chip.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_automation_target_picker_sheet.xml b/app/src/main/res/layout/home_automation_target_picker_sheet.xml
new file mode 100644
index 0000000..9c8d2ad
--- /dev/null
+++ b/app/src/main/res/layout/home_automation_target_picker_sheet.xml
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_automation_target_row.xml b/app/src/main/res/layout/home_automation_target_row.xml
new file mode 100644
index 0000000..ac1dcc9
--- /dev/null
+++ b/app/src/main/res/layout/home_automation_target_row.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_automation_trigger_row.xml b/app/src/main/res/layout/home_automation_trigger_row.xml
new file mode 100644
index 0000000..f8d857b
--- /dev/null
+++ b/app/src/main/res/layout/home_automation_trigger_row.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_automation_trigger_tile.xml b/app/src/main/res/layout/home_automation_trigger_tile.xml
new file mode 100644
index 0000000..fd06e61
--- /dev/null
+++ b/app/src/main/res/layout/home_automation_trigger_tile.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_bottom_sheet.xml b/app/src/main/res/layout/home_bottom_sheet.xml
index 0ce94a3..c9f061c 100644
--- a/app/src/main/res/layout/home_bottom_sheet.xml
+++ b/app/src/main/res/layout/home_bottom_sheet.xml
@@ -25,7 +25,7 @@
layout="@layout/home_close_button"
android:layout_width="32dp"
android:layout_height="32dp"
- android:layout_gravity="end|center_vertical" />
+ android:layout_gravity="start|center_vertical" />
+ android:background="@color/home_sheet_scrim"
+ android:clickable="true"
+ android:focusable="true" />
+ android:layout_width="@dimen/looply_avatar_size"
+ android:layout_height="@dimen/looply_avatar_size">
diff --git a/app/src/main/res/layout/home_modal.xml b/app/src/main/res/layout/home_modal.xml
index 773bbf0..6b44697 100644
--- a/app/src/main/res/layout/home_modal.xml
+++ b/app/src/main/res/layout/home_modal.xml
@@ -9,14 +9,14 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="12dp"
- android:paddingEnd="12dp">
+ android:paddingStart="12dp">
+ android:layout_gravity="start" />
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_spam_list_item.xml b/app/src/main/res/layout/home_spam_list_item.xml
index 66127d5..0599837 100644
--- a/app/src/main/res/layout/home_spam_list_item.xml
+++ b/app/src/main/res/layout/home_spam_list_item.xml
@@ -14,35 +14,27 @@
android:paddingBottom="@dimen/looply_space_md">
+ android:layout_width="@dimen/home_list_avatar_size"
+ android:layout_height="@dimen/home_list_avatar_size">
-
+ android:contentDescription="@null"
+ app:contactIconSize="normal" />
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-ko/looply_strings.xml b/app/src/main/res/values-ko/looply_strings.xml
index 9ac68d7..edca67e 100644
--- a/app/src/main/res/values-ko/looply_strings.xml
+++ b/app/src/main/res/values-ko/looply_strings.xml
@@ -186,9 +186,54 @@
사진 접근 허용
사진과 동영상을 첨부하려면 접근을 허용하세요.
새 자동화
- 새 자동화
+ 자동화 메세지 작성
자동화 이름
언제 실행하고 무엇을 할지 설정하세요.
+ 자동 발송 메시지는 대화에서 자동화 배지로 구분됩니다.
+ 저장
+ 작성 중인 자동화
+ 이전에 작성하던 규칙이 있습니다. 이어서 작성할까요?
+ 이어서 작성
+ 새로 작성
+ 미리보기
+ 규칙을 켜고 저장
+ 다음: 템플릿과 메시지 선택
+ 추천
+ 1단계
+ 실행 조건
+ 언제 자동으로 보낼까요?
+ 부재중 전화
+ 전화를 못 받으면 바로 맞춤 답장을 보냅니다
+ 통화 후 감사 인사
+ 통화가 끝나면 명함이나 안내를 보냅니다
+ 예약 발송
+ 날짜와 시간을 지정
+ 키워드 자동응답
+ 특정 단어가 오면 답장
+ 2단계
+ 수신 필터
+ 누구에게 보낼까요?
+ 모든 발신자
+ 저장 연락처만
+ 미등록 번호만
+ 가족·개인 연락처 제외
+ 고객 / 업무 태그 (122)
+ + 대상 추가
+ 포함
+ 제외
+ + 연락처 또는 그룹 추가
+ 연락처 선택
+ 삭제
+ 선택한 연락처나 그룹이 없습니다
+ 연락처 또는 그룹 선택
+ 연락처
+ 그룹
+ 완료
+ 연락처 그룹 없음
+ 연락처 앱의 그룹이 여기에 나타납니다. 포함하거나 제외할 수 있습니다.
+
+ - 연락처 %d명
+
관리
자동 발송 메시지는 대화에서 로봇 배지로 구분됩니다.
주요 자동화 모드
@@ -203,6 +248,9 @@
실행 중인 자동화 규칙
3
정렬: 최신순
+ 규칙을 보관했습니다
+ 규칙을 복원했습니다
+ 규칙을 삭제했습니다
통화 자동응답
즉시 발송
부재중 전화 수신 시 답장
@@ -302,6 +350,10 @@
휴지통
보관함
보관한 대화는 복원하거나 삭제할 때까지 여기에 있습니다.
+ 대화
+ 규칙
+ 보관한 규칙 없음
+ 보관한 자동화 규칙이 여기에 나타납니다.
휴지통
삭제한 대화는 30일 동안 여기에 있다가 영구 삭제됩니다.
휴지통이 비어 있습니다
diff --git a/app/src/main/res/values/home_colors.xml b/app/src/main/res/values/home_colors.xml
index 5c65940..4b21fb1 100644
--- a/app/src/main/res/values/home_colors.xml
+++ b/app/src/main/res/values/home_colors.xml
@@ -66,4 +66,6 @@
#E3F8EA
#F6F4FB
@color/home_accent
+ #33FFFFFF
+ #80000000
diff --git a/app/src/main/res/values/looply_dimens.xml b/app/src/main/res/values/looply_dimens.xml
index ae22c44..672a79b 100644
--- a/app/src/main/res/values/looply_dimens.xml
+++ b/app/src/main/res/values/looply_dimens.xml
@@ -13,6 +13,7 @@
8dp
12dp
16dp
+ 40dp
20dp
24dp
20dp
@@ -43,6 +44,8 @@
0dp
34dp
28dp
+ 26dp
+ 18dp
64dp
96dp
96dp
diff --git a/app/src/main/res/values/looply_strings.xml b/app/src/main/res/values/looply_strings.xml
index 0446997..8a05f89 100644
--- a/app/src/main/res/values/looply_strings.xml
+++ b/app/src/main/res/values/looply_strings.xml
@@ -187,9 +187,55 @@
Allow photo access
Allow access to photos and videos to attach them here.
New automation
- New automation
+ Write automation message
Automation name
Set when this task should run and what it should do.
+ Messages sent automatically are marked in chat with an automation badge.
+ Save
+ Unfinished automation
+ You left a rule in progress. Resume where you left off?
+ Resume
+ Start new
+ Preview
+ Activate and save rule
+ Next: Choose template & message
+ Recommended
+ Step 1
+ Trigger
+ When should a message send automatically?
+ On a missed call
+ Send a custom reply as soon as a call is missed
+ After-call thank-you
+ Send a card or follow-up when a call ends
+ Scheduled broadcast
+ A specific day and time
+ Keyword auto-reply
+ Reply when a word matches
+ Step 2
+ Recipient filter
+ Who should receive it?
+ All callers
+ Saved contacts only
+ Unknown numbers only
+ Exclude family and personal contacts
+ Client / business tag (122)
+ + Add target
+ Include
+ Exclude
+ + Add contacts or groups
+ Select contacts
+ Remove
+ No contacts or groups selected
+ Choose contacts or groups
+ Contacts
+ Groups
+ Done
+ No contact groups
+ Groups from Contacts will show up here so you can include or exclude them.
+
+ - %d contact
+ - %d contacts
+
Manage
Messages sent automatically are marked in chat with a robot badge.
Major modes
@@ -204,6 +250,9 @@
Active rules
3
Sort: latest
+ Rule archived
+ Rule restored
+ Rule deleted
Missed-call reply
Immediate
Auto-reply to missed calls
@@ -304,6 +353,10 @@
Trash
Archive
Archived conversations stay here until you restore or delete them.
+ Chats
+ Rules
+ No archived rules
+ Automation rules you archive will show up here.
Trash
Deleted conversations stay here for 30 days, then they are permanently deleted.
Trash is empty
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 590c75b..11745eb 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -55,7 +55,7 @@
- @color/home_bg
- @color/home_bg
- true
- - @color/home_bg
+ - @android:color/transparent
- true
- false
- @style/Home.EditText
diff --git a/com.filabcorp.msgq.allowlist.xml b/com.bnkode.notie.allowlist.xml
similarity index 92%
rename from com.filabcorp.msgq.allowlist.xml
rename to com.bnkode.notie.allowlist.xml
index 5baa5c9..d8dcd92 100644
--- a/com.filabcorp.msgq.allowlist.xml
+++ b/com.bnkode.notie.allowlist.xml
@@ -15,5 +15,5 @@
limitations under the License.
-->
-
+
diff --git a/tools/buglesql b/tools/buglesql
index 82a6103..4187cb5 100755
--- a/tools/buglesql
+++ b/tools/buglesql
@@ -40,4 +40,4 @@ if [ $# -lt 1 ]; then
exit 1
fi
-adb shell su -c sqlite3 $opts data/data/com.filabcorp.msgq/databases/bugle_db "$1"
+adb shell su -c sqlite3 $opts data/data/com.bnkode.notie/databases/bugle_db "$1"
diff --git a/tools/seed_demo_data.ps1 b/tools/seed_demo_data.ps1
index 80d032e..8b510fe 100644
--- a/tools/seed_demo_data.ps1
+++ b/tools/seed_demo_data.ps1
@@ -11,14 +11,14 @@ function Invoke-Adb {
}
Write-Host "== Default SMS + permissions =="
-Invoke-Adb shell cmd role add-role-holder android.app.role.SMS com.filabcorp.msgq | Out-Null
-Invoke-Adb shell settings put secure sms_default_application com.filabcorp.msgq | Out-Null
+Invoke-Adb shell cmd role add-role-holder android.app.role.SMS com.bnkode.notie | Out-Null
+Invoke-Adb shell settings put secure sms_default_application com.bnkode.notie | Out-Null
foreach ($p in @(
"READ_SMS","RECEIVE_SMS","SEND_SMS","RECEIVE_MMS",
"READ_CONTACTS","WRITE_CONTACTS","READ_PHONE_STATE",
"READ_PHONE_NUMBERS","POST_NOTIFICATIONS"
)) {
- Invoke-Adb shell pm grant com.filabcorp.msgq "android.permission.$p" 2>$null | Out-Null
+ Invoke-Adb shell pm grant com.bnkode.notie "android.permission.$p" 2>$null | Out-Null
}
$demo = @(
@@ -79,7 +79,7 @@ for ($i = 0; $i -lt $demo.Count; $i += 2) {
}
Write-Host "== Relaunch =="
-Invoke-Adb shell am force-stop com.filabcorp.msgq | Out-Null
+Invoke-Adb shell am force-stop com.bnkode.notie | Out-Null
Start-Sleep -Seconds 1
-Invoke-Adb shell am start -n com.filabcorp.msgq/com.android.messaging.ui.conversationlist.ConversationListActivity | Out-Null
+Invoke-Adb shell am start -n com.bnkode.notie/com.android.messaging.ui.conversationlist.ConversationListActivity | Out-Null
Write-Host "Done."