22
33import android .app .Notification ;
44import android .app .Notification .Action ;
5+ import android .app .NotificationManager ;
56import android .app .PendingIntent ;
67import android .app .RemoteInput ;
78import android .content .BroadcastReceiver ;
89import android .content .Context ;
910import android .content .Intent ;
1011import android .content .IntentFilter ;
12+ import android .database .Cursor ;
1113import android .net .Uri ;
1214import android .os .Bundle ;
1315import android .os .Parcelable ;
14- import android .provider .ContactsContract ;
16+ import android .provider .ContactsContract .Contacts ;
17+ import android .provider .ContactsContract .Profile ;
1518import android .service .notification .StatusBarNotification ;
1619import android .text .TextUtils ;
1720import android .util .ArrayMap ;
3235import androidx .core .app .NotificationCompat .MessagingStyle ;
3336import androidx .core .app .NotificationCompat .MessagingStyle .Message ;
3437import androidx .core .app .Person ;
38+ import androidx .core .graphics .drawable .IconCompat ;
3539
3640import static android .app .Notification .EXTRA_REMOTE_INPUT_HISTORY ;
3741import static android .app .Notification .EXTRA_TEXT ;
3842import static android .app .PendingIntent .FLAG_UPDATE_CURRENT ;
3943import static android .os .Build .VERSION .SDK_INT ;
4044import static android .os .Build .VERSION_CODES .N ;
45+ import static android .os .Build .VERSION_CODES .O ;
4146import static android .os .Build .VERSION_CODES .P ;
42- import static com .oasisfeng .nevo .decorators .wechat .ConversationManager .Conversation .TYPE_GROUP_CHAT ;
4347import 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