2
2
3
3
import android .app .Notification ;
4
4
import android .app .Notification .Action ;
5
+ import android .app .NotificationManager ;
5
6
import android .app .PendingIntent ;
6
7
import android .app .RemoteInput ;
7
8
import android .content .BroadcastReceiver ;
8
9
import android .content .Context ;
9
10
import android .content .Intent ;
10
11
import android .content .IntentFilter ;
12
+ import android .database .Cursor ;
11
13
import android .net .Uri ;
12
14
import android .os .Bundle ;
13
15
import android .os .Parcelable ;
14
- import android .provider .ContactsContract ;
16
+ import android .provider .ContactsContract .Contacts ;
17
+ import android .provider .ContactsContract .Profile ;
15
18
import android .service .notification .StatusBarNotification ;
16
19
import android .text .TextUtils ;
17
20
import android .util .ArrayMap ;
32
35
import androidx .core .app .NotificationCompat .MessagingStyle ;
33
36
import androidx .core .app .NotificationCompat .MessagingStyle .Message ;
34
37
import androidx .core .app .Person ;
38
+ import androidx .core .graphics .drawable .IconCompat ;
35
39
36
40
import static android .app .Notification .EXTRA_REMOTE_INPUT_HISTORY ;
37
41
import static android .app .Notification .EXTRA_TEXT ;
38
42
import static android .app .PendingIntent .FLAG_UPDATE_CURRENT ;
39
43
import static android .os .Build .VERSION .SDK_INT ;
40
44
import static android .os .Build .VERSION_CODES .N ;
45
+ import static android .os .Build .VERSION_CODES .O ;
41
46
import static android .os .Build .VERSION_CODES .P ;
42
- import static com .oasisfeng .nevo .decorators .wechat .ConversationManager .Conversation .TYPE_GROUP_CHAT ;
43
47
import static com .oasisfeng .nevo .decorators .wechat .WeChatDecorator .SENDER_MESSAGE_SEPARATOR ;
44
48
45
49
/**
@@ -61,16 +65,18 @@ class MessagingBuilder {
61
65
62
66
/* From Notification.CarExtender */
63
67
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
65
69
/* 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
+
74
80
private static final String KEY_USERNAME = "key_username" ;
75
81
private static final String MENTION_SEPARATOR = " " ; // Separator between @nick and text. It's not a regular white space, but U+2005.
76
82
@@ -123,7 +129,7 @@ class MessagingBuilder {
123
129
final MessagingStyle messaging = new MessagingStyle (mUserSelf );
124
130
final boolean sender_inline = num_lines_with_colon == lines .size ();
125
131
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 ()));
127
133
return messaging ;
128
134
}
129
135
@@ -136,38 +142,62 @@ class MessagingBuilder {
136
142
Log .w (TAG , EXTRA_CONVERSATION + " is missing" );
137
143
return null ;
138
144
}
139
- final Parcelable [] parcelable_messages = convs .getParcelableArray (KEY_MESSAGES );
145
+ final Parcelable [] parcelable_messages = convs .getParcelableArray (CAR_KEY_MESSAGES );
140
146
if (parcelable_messages == null ) {
141
- Log .w (TAG , KEY_MESSAGES + " is missing" );
147
+ Log .w (TAG , CAR_KEY_MESSAGES + " is missing" );
142
148
return null ;
143
149
}
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
+
145
168
final MessagingStyle messaging = new MessagingStyle (mUserSelf );
146
169
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 );
148
171
messaging .addMessage (message );
149
172
} else for (int i = 0 , num_messages = parcelable_messages .length ; i < num_messages ; i ++) {
150
173
final Parcelable parcelable = parcelable_messages [i ];
151
174
if (! (parcelable instanceof Bundle )) return null ;
152
175
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 );
154
177
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 ));
159
189
}
160
190
161
- final PendingIntent on_read = convs .getParcelable (KEY_ON_READ );
191
+ final PendingIntent on_read = convs .getParcelable (CAR_KEY_ON_READ );
162
192
if (on_read != null ) mMarkReadPendingIntents .put (sbn .getKey (), on_read ); // Mapped by evolved key,
163
193
164
194
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 ) {
166
196
final CharSequence [] input_history = n .extras .getCharSequenceArray (EXTRA_REMOTE_INPUT_HISTORY );
167
197
final PendingIntent proxy = proxyDirectReply (sbn , on_reply , remote_input , input_history , null );
168
198
final RemoteInput .Builder reply_remote_input = new RemoteInput .Builder (remote_input .getResultKey ()).addExtras (remote_input .getExtras ())
169
199
.setAllowFreeFormInput (true ).setChoices (SmartReply .generateChoices (messaging ));
170
- final String [] participants = convs .getStringArray (KEY_PARTICIPANTS );
200
+ final String [] participants = convs .getStringArray (CAR_KEY_PARTICIPANTS );
171
201
if (participants != null && participants .length > 0 ) {
172
202
final StringBuilder label = new StringBuilder ();
173
203
for (final String participant : participants ) label .append (',' ).append (participant );
@@ -179,7 +209,7 @@ class MessagingBuilder {
179
209
if (SDK_INT >= P ) reply_action .setSemanticAction (Action .SEMANTIC_ACTION_REPLY );
180
210
n .addAction (reply_action .build ());
181
211
182
- if (conversation .getType () == TYPE_GROUP_CHAT ) {
212
+ if (conversation .getType () == Conversation . TYPE_GROUP_CHAT ) {
183
213
final List <Message > messages = messaging .getMessages ();
184
214
final Person last_sender = messages .get (messages .size () - 1 ).getPerson ();
185
215
if (last_sender != null && last_sender != mUserSelf ) {
@@ -192,8 +222,8 @@ class MessagingBuilder {
192
222
return messaging ;
193
223
}
194
224
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 ) {
197
227
CharSequence actual_text = text ;
198
228
if (sender == null ) {
199
229
sender = extractSenderFromText (text );
@@ -204,21 +234,13 @@ private Message buildMessage(final Conversation conversation, final long when, f
204
234
}
205
235
actual_text = EmojiTranslator .translate (actual_text );
206
236
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 ) {
218
240
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 );
221
242
} else return new Message (actual_text , when , conversation .sender );
243
+ return new Message (actual_text , when , person );
222
244
}
223
245
224
246
private static @ Nullable String extractSenderFromText (final CharSequence text ) {
@@ -318,13 +340,24 @@ interface Controller { void recastNotification(String key, Bundle addition); }
318
340
MessagingBuilder (final Context context , final Controller controller ) {
319
341
mContext = context ;
320
342
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 );
323
344
324
345
final IntentFilter filter = new IntentFilter (ACTION_REPLY ); filter .addAction (ACTION_MENTION ); filter .addDataScheme (SCHEME_KEY );
325
346
context .registerReceiver (mReplyReceiver , filter );
326
347
}
327
348
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
+
328
361
void close () {
329
362
try { mContext .unregisterReceiver (mReplyReceiver ); } catch (final RuntimeException ignored ) {}
330
363
}
0 commit comments