From 0b2f7dfdb5cec3185424c293a85e6d2e7615c4be Mon Sep 17 00:00:00 2001 From: "MINIPC\\frivaong" Date: Tue, 27 Oct 2026 04:55:27 +0900 Subject: [PATCH] Messaging: Add automation compose and rebrand as com.bnkode.notie. Co-authored-by: Cursor --- Android.bp | 8 +- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 18 +- .../datamodel/BugleDatabaseOperations.java | 11 +- .../datamodel/MediaScratchFileProvider.java | 2 +- .../datamodel/MessagingContentProvider.java | 2 +- .../messaging/datamodel/MmsFileProvider.java | 2 +- .../datamodel/action/ActionServiceImpl.java | 2 +- .../receiver/ScheduledMessageReceiver.java | 2 +- .../receiver/SendStatusReceiver.java | 8 +- .../com/android/messaging/ui/UIIntents.java | 4 +- .../ui/contact/ContactListItemView.java | 42 +- .../ui/contact/ContactPickerFragment.java | 7 + .../ArchivedConversationListActivity.java | 92 ++++ .../ConversationListFragment.java | 5 + .../ConversationListItemView.java | 152 +++---- .../ui/home/HomeAutomationCreateActivity.java | 369 +++++++++++++++- .../ui/home/HomeAutomationDraftStore.java | 130 ++++++ .../messaging/ui/home/HomeAutomationHome.java | 98 ++++- .../ui/home/HomeAutomationRuleStore.java | 120 +++++ .../ui/home/HomeAutomationTarget.java | 86 ++++ .../home/HomeAutomationTargetPickerSheet.java | 372 ++++++++++++++++ .../messaging/ui/home/HomeAvatarOverlay.java | 73 ++++ .../ui/home/HomeCategoryScroller.java | 13 +- .../ui/home/HomeComposeAttachPanel.java | 35 +- .../messaging/ui/home/HomeContactGroups.java | 209 +++++++++ .../ui/home/HomeContactListAdapter.java | 19 +- .../ui/home/HomeContactsFragment.java | 68 ++- .../messaging/ui/home/HomeDemoGroupSeed.java | 173 ++++++++ .../messaging/ui/home/HomeFadeScrollView.java | 80 ++++ .../ui/home/HomeScrollEdgeFades.java | 66 +++ .../messaging/ui/home/HomeSpamFragment.java | 21 +- .../messaging/ui/home/HomeSwipeCard.java | 336 ++++++++++++++ .../drawable/home_bg_automation_icon_on.xml | 6 + .../drawable/home_bg_automation_option.xml | 10 + .../home_bg_automation_option_selected.xml | 13 + .../res/drawable/home_bg_automation_pill.xml | 10 + .../home_bg_automation_pill_selected.xml | 10 + .../drawable/home_bg_automation_preview.xml | 9 + .../home_bg_automation_recommended.xml | 6 + .../home_bg_automation_row_ripple.xml | 9 + .../main/res/drawable/home_bg_avatar_edit.xml | 5 + .../res/drawable/home_bg_avatar_selected.xml | 13 + .../drawable/home_bg_avatar_selected_wash.xml | 5 + .../res/drawable/ic_home_automation_chat.xml | 14 + .../drawable/ic_home_automation_person.xml | 18 + .../res/drawable/ic_home_automation_phone.xml | 14 + .../res/drawable/ic_home_check_stroke.xml | 14 + .../res/layout/contact_list_item_view.xml | 21 +- .../layout/conversation_list_item_view.xml | 8 +- .../res/layout/home_archived_activity.xml | 44 ++ .../main/res/layout/home_archived_rules.xml | 50 +++ .../home_automation_create_activity.xml | 409 +++++++++++++++++- .../main/res/layout/home_automation_pane.xml | 267 +----------- .../layout/home_automation_rule_missed.xml | 87 ++++ .../layout/home_automation_rule_thanks.xml | 87 ++++ .../layout/home_automation_rule_weekly.xml | 87 ++++ .../layout/home_automation_target_chip.xml | 41 ++ .../home_automation_target_picker_sheet.xml | 140 ++++++ .../res/layout/home_automation_target_row.xml | 64 +++ .../layout/home_automation_trigger_row.xml | 78 ++++ .../layout/home_automation_trigger_tile.xml | 38 ++ app/src/main/res/layout/home_bottom_sheet.xml | 2 +- .../layout/home_compose_attach_overlay.xml | 6 +- .../res/layout/home_contacts_list_item.xml | 16 +- app/src/main/res/layout/home_modal.xml | 4 +- .../res/layout/home_selection_indicator.xml | 20 + .../main/res/layout/home_spam_list_item.xml | 32 +- app/src/main/res/layout/home_sub_header.xml | 10 +- .../res/layout/home_swipe_card_actions.xml | 41 ++ app/src/main/res/values-ko/looply_strings.xml | 54 ++- app/src/main/res/values/home_colors.xml | 2 + app/src/main/res/values/looply_dimens.xml | 3 + app/src/main/res/values/looply_strings.xml | 55 ++- app/src/main/res/values/styles.xml | 2 +- ...list.xml => com.bnkode.notie.allowlist.xml | 2 +- tools/buglesql | 2 +- tools/seed_demo_data.ps1 | 10 +- 78 files changed, 3955 insertions(+), 512 deletions(-) create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeAutomationDraftStore.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeAutomationRuleStore.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeAutomationTarget.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeAutomationTargetPickerSheet.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeAvatarOverlay.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeContactGroups.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeDemoGroupSeed.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeFadeScrollView.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeScrollEdgeFades.java create mode 100644 app/src/main/java/com/android/messaging/ui/home/HomeSwipeCard.java create mode 100644 app/src/main/res/drawable/home_bg_automation_icon_on.xml create mode 100644 app/src/main/res/drawable/home_bg_automation_option.xml create mode 100644 app/src/main/res/drawable/home_bg_automation_option_selected.xml create mode 100644 app/src/main/res/drawable/home_bg_automation_pill.xml create mode 100644 app/src/main/res/drawable/home_bg_automation_pill_selected.xml create mode 100644 app/src/main/res/drawable/home_bg_automation_preview.xml create mode 100644 app/src/main/res/drawable/home_bg_automation_recommended.xml create mode 100644 app/src/main/res/drawable/home_bg_automation_row_ripple.xml create mode 100644 app/src/main/res/drawable/home_bg_avatar_edit.xml create mode 100644 app/src/main/res/drawable/home_bg_avatar_selected.xml create mode 100644 app/src/main/res/drawable/home_bg_avatar_selected_wash.xml create mode 100644 app/src/main/res/drawable/ic_home_automation_chat.xml create mode 100644 app/src/main/res/drawable/ic_home_automation_person.xml create mode 100644 app/src/main/res/drawable/ic_home_automation_phone.xml create mode 100644 app/src/main/res/drawable/ic_home_check_stroke.xml create mode 100644 app/src/main/res/layout/home_archived_rules.xml create mode 100644 app/src/main/res/layout/home_automation_rule_missed.xml create mode 100644 app/src/main/res/layout/home_automation_rule_thanks.xml create mode 100644 app/src/main/res/layout/home_automation_rule_weekly.xml create mode 100644 app/src/main/res/layout/home_automation_target_chip.xml create mode 100644 app/src/main/res/layout/home_automation_target_picker_sheet.xml create mode 100644 app/src/main/res/layout/home_automation_target_row.xml create mode 100644 app/src/main/res/layout/home_automation_trigger_row.xml create mode 100644 app/src/main/res/layout/home_automation_trigger_tile.xml create mode 100644 app/src/main/res/layout/home_selection_indicator.xml create mode 100644 app/src/main/res/layout/home_swipe_card_actions.xml rename com.filabcorp.msgq.allowlist.xml => com.bnkode.notie.allowlist.xml (92%) 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."