Skip to content

Commit 81cda1d

Browse files
committed
UPDATE: Guess conversation type before username-based detection is performed asynchronously.
1 parent b4d6fd0 commit 81cda1d

File tree

3 files changed

+108
-61
lines changed

3 files changed

+108
-61
lines changed

src/main/java/com/oasisfeng/nevo/decorators/wechat/ConversationManager.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,15 @@ static class Conversation {
3939

4040
int getType() { return mType; }
4141

42-
void setType(final int type) {
43-
if (type == this.mType) return;
44-
this.mType = type;
42+
/** @return previous type */
43+
int setType(final int type) {
44+
if (type == mType) return type;
45+
final int previous_type = mType;
46+
mType = type;
4547
sender = type == TYPE_UNKNOWN || type == TYPE_GROUP_CHAT ? SENDER_PLACEHOLDER
4648
: sender.toBuilder().setKey(key).setBot(type == TYPE_BOT_MESSAGE).build(); // Always set key as it may change
4749
if (type != TYPE_GROUP_CHAT) mParticipants.clear();
50+
return previous_type;
4851
}
4952

5053
CharSequence getTitle() { return mTitle; }

src/main/java/com/oasisfeng/nevo/decorators/wechat/MessagingBuilder.java

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22

33
import android.app.Notification;
44
import android.app.Notification.Action;
5+
import android.app.NotificationManager;
56
import android.app.PendingIntent;
67
import android.app.RemoteInput;
78
import android.content.BroadcastReceiver;
89
import android.content.Context;
910
import android.content.Intent;
1011
import android.content.IntentFilter;
12+
import android.database.Cursor;
1113
import android.net.Uri;
1214
import android.os.Bundle;
1315
import android.os.Parcelable;
14-
import android.provider.ContactsContract;
16+
import android.provider.ContactsContract.Contacts;
17+
import android.provider.ContactsContract.Profile;
1518
import android.service.notification.StatusBarNotification;
1619
import android.text.TextUtils;
1720
import android.util.ArrayMap;
@@ -32,14 +35,15 @@
3235
import androidx.core.app.NotificationCompat.MessagingStyle;
3336
import androidx.core.app.NotificationCompat.MessagingStyle.Message;
3437
import androidx.core.app.Person;
38+
import androidx.core.graphics.drawable.IconCompat;
3539

3640
import static android.app.Notification.EXTRA_REMOTE_INPUT_HISTORY;
3741
import static android.app.Notification.EXTRA_TEXT;
3842
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
3943
import static android.os.Build.VERSION.SDK_INT;
4044
import static android.os.Build.VERSION_CODES.N;
45+
import static android.os.Build.VERSION_CODES.O;
4146
import static android.os.Build.VERSION_CODES.P;
42-
import static com.oasisfeng.nevo.decorators.wechat.ConversationManager.Conversation.TYPE_GROUP_CHAT;
4347
import static com.oasisfeng.nevo.decorators.wechat.WeChatDecorator.SENDER_MESSAGE_SEPARATOR;
4448

4549
/**
@@ -61,16 +65,18 @@ class MessagingBuilder {
6165

6266
/* From Notification.CarExtender */
6367
private static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS";
64-
private static final String EXTRA_CONVERSATION = "car_conversation";
68+
private static final String EXTRA_CONVERSATION = "car_conversation"; // In the bundle of EXTRA_CAR_EXTENDER
6569
/* From Notification.CarExtender.UnreadConversation */
66-
private static final String KEY_MESSAGES = "messages";
67-
private static final String KEY_AUTHOR = "author"; // In the bundle of KEY_MESSAGES
68-
private static final String KEY_TEXT = "text"; // In the bundle of KEY_MESSAGES
69-
private static final String KEY_REMOTE_INPUT = "remote_input";
70-
private static final String KEY_ON_REPLY = "on_reply";
71-
private static final String KEY_ON_READ = "on_read";
72-
private static final String KEY_PARTICIPANTS = "participants";
73-
private static final String KEY_TIMESTAMP = "timestamp";
70+
private static final String CAR_KEY_REMOTE_INPUT = "remote_input"; // In the bundle of EXTRA_CONVERSATION
71+
private static final String CAR_KEY_ON_REPLY = "on_reply";
72+
private static final String CAR_KEY_ON_READ = "on_read";
73+
private static final String CAR_KEY_PARTICIPANTS = "participants";
74+
private static final String CAR_KEY_MESSAGES = "messages";
75+
76+
private static final String CAR_KEY_AUTHOR = "author"; // In the bundle of CAR_KEY_MESSAGES
77+
private static final String CAR_KEY_TEXT = "text";
78+
private static final String CAR_KEY_TIMESTAMP = "timestamp";
79+
7480
private static final String KEY_USERNAME = "key_username";
7581
private static final String MENTION_SEPARATOR = " "; // Separator between @nick and text. It's not a regular white space, but U+2005.
7682

@@ -123,7 +129,7 @@ class MessagingBuilder {
123129
final MessagingStyle messaging = new MessagingStyle(mUserSelf);
124130
final boolean sender_inline = num_lines_with_colon == lines.size();
125131
for (int i = 0, size = lines.size(); i < size; i++) // All lines have colon in text
126-
messaging.addMessage(buildMessage(conversation, lines.keyAt(i), n.tickerText, lines.valueAt(i), sender_inline ? null : title.toString(), null));
132+
messaging.addMessage(buildMessage(conversation, lines.keyAt(i), n.tickerText, lines.valueAt(i), sender_inline ? null : title.toString()));
127133
return messaging;
128134
}
129135

@@ -136,38 +142,62 @@ class MessagingBuilder {
136142
Log.w(TAG, EXTRA_CONVERSATION + " is missing");
137143
return null;
138144
}
139-
final Parcelable[] parcelable_messages = convs.getParcelableArray(KEY_MESSAGES);
145+
final Parcelable[] parcelable_messages = convs.getParcelableArray(CAR_KEY_MESSAGES);
140146
if (parcelable_messages == null) {
141-
Log.w(TAG, KEY_MESSAGES + " is missing");
147+
Log.w(TAG, CAR_KEY_MESSAGES + " is missing");
142148
return null;
143149
}
144-
final PendingIntent on_reply = convs.getParcelable(KEY_ON_REPLY);
150+
151+
final PendingIntent on_reply = convs.getParcelable(CAR_KEY_ON_REPLY);
152+
if (conversation.key == null) try {
153+
if (on_reply != null) on_reply.send(mContext, 0, null, (p, intent, r, d, b) -> {
154+
final String key = conversation.key = intent.getStringExtra(KEY_USERNAME); // setType() below will trigger rebuilding of conversation sender.
155+
final int detected_type = key.endsWith("@chatroom") || key.endsWith("@im.chatroom"/* WeWork */)
156+
? Conversation.TYPE_GROUP_CHAT : key.startsWith("gh_") ? Conversation.TYPE_BOT_MESSAGE : Conversation.TYPE_DIRECT_MESSAGE;
157+
final int previous_type = conversation.setType(detected_type);
158+
if (BuildConfig.DEBUG && SDK_INT >= O && previous_type != Conversation.TYPE_UNKNOWN && detected_type != previous_type) {
159+
final Notification clone = sbn.getNotification().clone();
160+
final Notification.Builder dn = Notification.Builder.recoverBuilder(mContext, clone).setStyle(null).setSubText(clone.tickerText);
161+
mContext.getSystemService(NotificationManager.class).notify(sbn.getTag(), sbn.getId(), dn.setChannelId("guide").build());
162+
}
163+
}, null);
164+
} catch (final PendingIntent.CanceledException e) {
165+
Log.e(TAG, "Error parsing reply intent.", e);
166+
}
167+
145168
final MessagingStyle messaging = new MessagingStyle(mUserSelf);
146169
if (parcelable_messages.length == 0) { // When only one message in this conversation
147-
final Message message = buildMessage(conversation, n.when, n.tickerText, n.extras.getCharSequence(EXTRA_TEXT), null, on_reply);
170+
final Message message = buildMessage(conversation, n.when, n.tickerText, n.extras.getCharSequence(EXTRA_TEXT), null);
148171
messaging.addMessage(message);
149172
} else for (int i = 0, num_messages = parcelable_messages.length; i < num_messages; i ++) {
150173
final Parcelable parcelable = parcelable_messages[i];
151174
if (! (parcelable instanceof Bundle)) return null;
152175
final Bundle car_message = (Bundle) parcelable;
153-
final String text = car_message.getString(KEY_TEXT);
176+
final String text = car_message.getString(CAR_KEY_TEXT);
154177
if (text == null) continue;
155-
final long timestamp = car_message.getLong(KEY_TIMESTAMP);
156-
final @Nullable String author = car_message.getString(KEY_AUTHOR); // Apparently always null (not yet implemented by WeChat)
157-
final Message message = buildMessage(conversation, timestamp, i == num_messages - 1 ? n.tickerText : null, text, author, on_reply);
158-
messaging.addMessage(message);
178+
final long timestamp = car_message.getLong(CAR_KEY_TIMESTAMP); // Appears always 0 (not yet implemented by WeChat)
179+
final @Nullable String author = car_message.getString(CAR_KEY_AUTHOR); // Appears always null (not yet implemented by WeChat)
180+
final CharSequence n_text = n.extras.getCharSequence(EXTRA_TEXT);
181+
if (conversation.getType() == Conversation.TYPE_UNKNOWN && num_messages == 1 && TextUtils.equals(text, n_text))
182+
conversation.setType(Conversation.TYPE_DIRECT_MESSAGE); // Extra chance to detect direct message indistinguishable from bot message.
183+
if (i == num_messages - 1 && TextUtils.indexOf(n.tickerText, n_text) >= 0 && TextUtils.indexOf(n.tickerText, text) < 0
184+
&& TextUtils.indexOf(text, n_text) < 0) { // The last check for case: text="[Link] ABC", n_text="ABC" (commonly seen in bot messages)
185+
// The last message inside car extender is inconsistent with the outer ticker and content text, it should be a reply sent by the user.
186+
messaging.addMessage(buildMessage(conversation, 0, n.tickerText, n_text, null));
187+
messaging.addMessage(buildMessage(conversation, timestamp, null, text, ""/* special mark for "self" */));
188+
} else messaging.addMessage(buildMessage(conversation, timestamp, i == num_messages - 1 ? n.tickerText : null, text, author));
159189
}
160190

161-
final PendingIntent on_read = convs.getParcelable(KEY_ON_READ);
191+
final PendingIntent on_read = convs.getParcelable(CAR_KEY_ON_READ);
162192
if (on_read != null) mMarkReadPendingIntents.put(sbn.getKey(), on_read); // Mapped by evolved key,
163193

164194
final RemoteInput remote_input;
165-
if (SDK_INT >= N && on_reply != null && (remote_input = convs.getParcelable(KEY_REMOTE_INPUT)) != null) {
195+
if (SDK_INT >= N && on_reply != null && (remote_input = convs.getParcelable(CAR_KEY_REMOTE_INPUT)) != null) {
166196
final CharSequence[] input_history = n.extras.getCharSequenceArray(EXTRA_REMOTE_INPUT_HISTORY);
167197
final PendingIntent proxy = proxyDirectReply(sbn, on_reply, remote_input, input_history, null);
168198
final RemoteInput.Builder reply_remote_input = new RemoteInput.Builder(remote_input.getResultKey()).addExtras(remote_input.getExtras())
169199
.setAllowFreeFormInput(true).setChoices(SmartReply.generateChoices(messaging));
170-
final String[] participants = convs.getStringArray(KEY_PARTICIPANTS);
200+
final String[] participants = convs.getStringArray(CAR_KEY_PARTICIPANTS);
171201
if (participants != null && participants.length > 0) {
172202
final StringBuilder label = new StringBuilder();
173203
for (final String participant : participants) label.append(',').append(participant);
@@ -179,7 +209,7 @@ class MessagingBuilder {
179209
if (SDK_INT >= P) reply_action.setSemanticAction(Action.SEMANTIC_ACTION_REPLY);
180210
n.addAction(reply_action.build());
181211

182-
if (conversation.getType() == TYPE_GROUP_CHAT) {
212+
if (conversation.getType() == Conversation.TYPE_GROUP_CHAT) {
183213
final List<Message> messages = messaging.getMessages();
184214
final Person last_sender = messages.get(messages.size() - 1).getPerson();
185215
if (last_sender != null && last_sender != mUserSelf) {
@@ -192,8 +222,8 @@ class MessagingBuilder {
192222
return messaging;
193223
}
194224

195-
private Message buildMessage(final Conversation conversation, final long when, final @Nullable CharSequence ticker,
196-
final CharSequence text, @Nullable String sender, final @Nullable PendingIntent on_reply) {
225+
private static Message buildMessage(final Conversation conversation, final long when, final @Nullable CharSequence ticker,
226+
final CharSequence text, @Nullable String sender) {
197227
CharSequence actual_text = text;
198228
if (sender == null) {
199229
sender = extractSenderFromText(text);
@@ -204,21 +234,13 @@ private Message buildMessage(final Conversation conversation, final long when, f
204234
}
205235
actual_text = EmojiTranslator.translate(actual_text);
206236

207-
if (conversation.key == null) try {
208-
if (on_reply != null) on_reply.send(mContext, 0, null, (p, intent, r, d, b) -> {
209-
final String key = conversation.key = intent.getStringExtra(KEY_USERNAME); // setType() below will trigger rebuilding of conversation sender.
210-
conversation.setType(key.endsWith("@chatroom") || key.endsWith("@im.chatroom"/* WeWork */) ? TYPE_GROUP_CHAT
211-
: key.startsWith("gh_") ? Conversation.TYPE_BOT_MESSAGE : Conversation.TYPE_DIRECT_MESSAGE);
212-
}, null);
213-
} catch (final PendingIntent.CanceledException e) {
214-
Log.e(TAG, "Error parsing reply intent.", e);
215-
}
216-
217-
if (conversation.getType() == TYPE_GROUP_CHAT) {
237+
final Person person;
238+
if (sender != null && sender.isEmpty()) person = null; // Empty string as a special mark for "self"
239+
else if (conversation.getType() == Conversation.TYPE_GROUP_CHAT) {
218240
final String ticker_sender = ticker != null ? extractSenderFromText(ticker) : null; // Group nick is used in ticker while original nick in sender.
219-
final Person person = sender == null ? null : conversation.getGroupParticipant(sender, ticker_sender != null ? ticker_sender : sender);
220-
return new Message(actual_text, when, person);
241+
person = sender == null ? null : conversation.getGroupParticipant(sender, ticker_sender != null ? ticker_sender : sender);
221242
} else return new Message(actual_text, when, conversation.sender);
243+
return new Message(actual_text, when, person);
222244
}
223245

224246
private static @Nullable String extractSenderFromText(final CharSequence text) {
@@ -318,13 +340,24 @@ interface Controller { void recastNotification(String key, Bundle addition); }
318340
MessagingBuilder(final Context context, final Controller controller) {
319341
mContext = context;
320342
mController = controller;
321-
final Uri profile_lookup = ContactsContract.Contacts.getLookupUri(context.getContentResolver(), ContactsContract.Profile.CONTENT_URI);
322-
mUserSelf = new Person.Builder().setUri(profile_lookup != null ? profile_lookup.toString() : null).setName(context.getString(R.string.self_display_name)).build();
343+
mUserSelf = buildPersonFromProfile(context);
323344

324345
final IntentFilter filter = new IntentFilter(ACTION_REPLY); filter.addAction(ACTION_MENTION); filter.addDataScheme(SCHEME_KEY);
325346
context.registerReceiver(mReplyReceiver, filter);
326347
}
327348

349+
private static Person buildPersonFromProfile(final Context context) {
350+
final Person.Builder self = new Person.Builder().setName(context.getString(R.string.self_display_name));
351+
try (final Cursor cursor = context.getContentResolver().query(Profile.CONTENT_URI,
352+
new String[] { Contacts._ID, Contacts.LOOKUP_KEY, Contacts.PHOTO_THUMBNAIL_URI }, null, null, null)) {
353+
if (cursor == null || ! cursor.moveToFirst()) return self.build();
354+
final long id = cursor.getLong(0); final String lookup_key = cursor.getString(1);
355+
final String photo = cursor.getString(2);
356+
final Uri lookup = lookup_key == null ? null : Contacts.getLookupUri(id, lookup_key);
357+
return self.setUri(lookup != null ? lookup.toString() : null).setIcon(photo == null ? null : IconCompat.createWithContentUri(photo)).build();
358+
}
359+
}
360+
328361
void close() {
329362
try { mContext.unregisterReceiver(mReplyReceiver); } catch (final RuntimeException ignored) {}
330363
}

0 commit comments

Comments
 (0)