Skip to content

Commit 2fd11a7

Browse files
committed
UPDATE: Major update to improve shortcut and bubble support.
Bubble on Android 10 is not supported yet. Shortcuts and bubble no longer require CarExtender.
1 parent d1a1aad commit 2fd11a7

File tree

5 files changed

+173
-88
lines changed

5 files changed

+173
-88
lines changed
Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
package com.oasisfeng.nevo.decorators.wechat
22

33
import android.annotation.SuppressLint
4-
import android.content.ComponentName
5-
import android.content.Context
6-
import android.content.Intent
7-
import android.content.LocusId
4+
import android.content.*
5+
import android.content.pm.LauncherApps
86
import android.content.pm.PackageManager
7+
import android.content.pm.PackageManager.GET_ACTIVITIES
98
import android.content.pm.ShortcutInfo
109
import android.content.pm.ShortcutManager
10+
import android.graphics.drawable.Icon
11+
import android.graphics.drawable.Icon.TYPE_RESOURCE
1112
import android.os.Build.VERSION.SDK_INT
1213
import android.os.Build.VERSION_CODES.N
1314
import android.os.Build.VERSION_CODES.N_MR1
1415
import android.os.Build.VERSION_CODES.Q
1516
import android.os.Process
1617
import android.os.UserHandle
1718
import android.os.UserManager
19+
import android.util.ArrayMap
1820
import android.util.Log
1921
import android.util.LruCache
2022
import androidx.annotation.RequiresApi
23+
import androidx.core.content.getSystemService
24+
import androidx.core.graphics.drawable.IconCompat
2125
import com.oasisfeng.nevo.decorators.wechat.ConversationManager.Conversation
2226
import com.oasisfeng.nevo.decorators.wechat.WeChatDecorator.AGENT_PACKAGE
2327
import com.oasisfeng.nevo.decorators.wechat.WeChatDecorator.TAG
@@ -30,9 +34,8 @@ import java.lang.reflect.Method
3034
fun buildShortcutId(key: String) = "C:$key"
3135
}
3236

33-
private fun updateShortcut(conversation: Conversation, profile: UserHandle): Boolean {
34-
val key = conversation.key ?: return false.also { Log.i(TAG, "Postpone shortcut update until conversation key is fetched.") }
35-
val agentContext = createAgentContext(profile) ?: return false
37+
/** @return true if shortcut is ready */
38+
private fun updateShortcut(id: String, conversation: Conversation, agentContext: Context): Boolean {
3639
if (SDK_INT >= N && agentContext.getSystemService(UserManager::class.java)?.isUserUnlocked == false) return false // Shortcuts cannot be changed if user is locked.
3740

3841
val activity = agentContext.packageManager.resolveActivity(Intent(Intent.ACTION_MAIN) // Use agent context to resolve in proper user.
@@ -41,25 +44,35 @@ import java.lang.reflect.Method
4144

4245
val sm = agentContext.getShortcutManager() ?: return false
4346
if (sm.isRateLimitingActive)
44-
return false.also { Log.w(TAG, "Due to rate limit, shortcut is not updated: $key") }
47+
return false.also { Log.w(TAG, "Due to rate limit, shortcut is not updated: $id") }
4548

4649
val shortcuts = sm.dynamicShortcuts.apply { sortBy { it.rank }}; val count = shortcuts.size
47-
shortcuts.forEach { shortcut -> if (buildShortcutId(key) == shortcut.id) return true }
4850
if (count >= sm.maxShortcutCountPerActivity - sm.manifestShortcuts.size)
49-
sm.removeDynamicShortcuts(listOf(shortcuts.removeAt(0).id))
51+
sm.removeDynamicShortcuts(listOf(shortcuts.removeAt(0).id.also { Log.i(TAG, "Evict excess shortcut: $it") }))
5052

51-
val intent = Intent().setComponent(ComponentName(WECHAT_PACKAGE, "com.tencent.mm.ui.LauncherUI"))
52-
.putExtra("Main_User", key).putExtra(@Suppress("SpellCheckingInspection") "Intro_Is_Muti_Talker", false)
53+
val intent = if (conversation.ext != null) Intent().setClassName(WECHAT_PACKAGE, "com.tencent.mm.ui.LauncherUI")
54+
.putExtra("Main_User", conversation.key).putExtra(@Suppress("SpellCheckingInspection") "Intro_Is_Muti_Talker", false)
5355
.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
54-
val shortcut = ShortcutInfo.Builder(agentContext, buildShortcutId(key)).setActivity(ComponentName(AGENT_PACKAGE, activity))
56+
else {
57+
val bubbleActivity = (mAgentBubbleActivity
58+
?: try { context.packageManager.getPackageInfo(AGENT_PACKAGE, GET_ACTIVITIES).activities
59+
.firstOrNull { it.enabled && it.flags.and(FLAG_ALLOW_EMBEDDED) != 0 }?.name ?: "" }
60+
catch (e: PackageManager.NameNotFoundException) { "" }.also { mAgentBubbleActivity = it }) // "" to indicate N/A
61+
if (bubbleActivity.isNotEmpty()) {
62+
Intent(Intent.ACTION_VIEW_LOCUS).putExtra(Intent.EXTRA_LOCUS_ID, id).setClassName(AGENT_PACKAGE, bubbleActivity)
63+
} else Intent().setClassName(AGENT_PACKAGE, activity)
64+
}
65+
66+
val shortcut = ShortcutInfo.Builder(agentContext, id).setActivity(ComponentName(AGENT_PACKAGE, activity))
5567
.setShortLabel(conversation.title).setRank(if (conversation.isGroupChat) 1 else 0) // Always keep last direct message conversation on top.
5668
.setIntent(intent.apply { if (action == null) action = Intent.ACTION_MAIN })
5769
.setCategories(setOf(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION)).apply {
58-
if (conversation.icon != null) setIcon(IconHelper.convertToAdaptiveIcon(context, sm, conversation.icon))
70+
if (conversation.icon != null) setIcon(conversation.icon.toLocalAdaptiveIcon(context, sm))
5971
if (SDK_INT >= Q) @SuppressLint("RestrictedApi") {
60-
setLongLived(true).setLocusId(LocusId(key))
61-
if (! conversation.isGroupChat) setPerson(conversation.sender().build().toAndroidPerson()) }}
62-
return if (sm.addDynamicShortcuts(listOf(shortcut.build()))) true.also { Log.i(TAG, "Shortcut updated for $key") }
72+
setLongLived(true).setLocusId(LocusId(id))
73+
if (! conversation.isGroupChat) setPerson(conversation.sender().build().toAndroidPerson()) }}.build()
74+
if (BuildConfig.DEBUG) { Log.i(TAG, "Updating shortcut \"${shortcut.id}\": ${shortcut.intent.toString()}") }
75+
return if (sm.addDynamicShortcuts(listOf(shortcut))) true.also { Log.i(TAG, "Shortcut updated: $id") }
6376
else false.also { Log.e(TAG, "Unexpected rate limit.") }
6477
}
6578

@@ -70,20 +83,50 @@ import java.lang.reflect.Method
7083
catch (e: PackageManager.NameNotFoundException) { null }
7184
catch (e: RuntimeException) { null.also { Log.e(TAG, "Error creating context for agent in user ${profile.hashCode()}", e) }}
7285

73-
fun updateShortcutIfNeeded(conversation: Conversation, profile: UserHandle) {
74-
val key = conversation.key
75-
if (SDK_INT < N_MR1 || key == null || ! conversation.isChat || conversation.isBotMessage) return
76-
if (mDynamicShortcutContacts.get(key) == null) {
77-
try { if (updateShortcut(conversation, profile)) mDynamicShortcutContacts.put(key, Unit) }
78-
catch (e: RuntimeException) { Log.e(TAG, "Error publishing shortcut for $key", e) }}
86+
/** @return whether shortcut is ready */
87+
@RequiresApi(N_MR1) fun updateShortcutIfNeeded(id: String, conversation: Conversation, profile: UserHandle): Boolean {
88+
if (! conversation.isChat || conversation.isBotMessage) return false
89+
val agentContext = mAgentContextByProfile[profile] ?: return false
90+
if (mDynamicShortcutContacts.get(id) != null) return true
91+
try { if (updateShortcut(id, conversation, agentContext))
92+
return true.also { if (conversation.icon.type != TYPE_RESOURCE) mDynamicShortcutContacts.put(id, Unit) }} // If no large icon, wait for the next update
93+
catch (e: RuntimeException) { Log.e(TAG, "Error publishing shortcut: $id", e) }
94+
return false
7995
}
8096

8197
private fun Context.getShortcutManager() = getSystemService(ShortcutManager::class.java)
8298

99+
private var mAgentBubbleActivity: String? = null
100+
private val mPackageEventReceiver = object: LauncherApps.Callback() {
101+
102+
private fun update(pkg: String, user: UserHandle) {
103+
if (pkg == AGENT_PACKAGE) mAgentContextByProfile[user] = createAgentContext(user)
104+
}
105+
106+
override fun onPackageRemoved(pkg: String, user: UserHandle) { update(pkg, user) }
107+
override fun onPackageAdded(pkg: String, user: UserHandle) { update(pkg, user) }
108+
override fun onPackageChanged(pkg: String, user: UserHandle) { update(pkg, user) }
109+
override fun onPackagesAvailable(pkgs: Array<out String>, user: UserHandle, replacing: Boolean) { pkgs.forEach { update(it, user) }}
110+
override fun onPackagesUnavailable(pkgs: Array<out String>, user: UserHandle, replacing: Boolean) { pkgs.forEach { update(it, user) }}
111+
}
112+
83113
/** Local mark to reduce repeated shortcut updates */
84-
private val mDynamicShortcutContacts = LruCache<String, Unit>(3) // Do not rely on maxShortcutCountPerActivity(), as most launcher only display top 4 shortcuts (including manifest shortcuts)
114+
private val mDynamicShortcutContacts = LruCache<String/* shortcut ID */, Unit>(3) // Do not rely on maxShortcutCountPerActivity(), as most launcher only display top 4 shortcuts (including manifest shortcuts)
85115

86-
// private val mShortcutIdsByProfile = SparseArray<Set<Int>>()
87116
private val mMethodCreatePackageContextAsUser: Method? by lazy {
88117
try { Context::class.java.getMethod("createPackageContextAsUser") } catch (e: ReflectiveOperationException) { null }}
118+
private val mAgentContextByProfile = ArrayMap<UserHandle, Context?>()
119+
120+
init {
121+
context.getSystemService<LauncherApps>()?.registerCallback(mPackageEventReceiver)
122+
context.getSystemService<UserManager>()?.userProfiles?.forEach {
123+
mAgentContextByProfile[it] = createAgentContext(it) }
124+
}
125+
126+
fun close() {
127+
context.getSystemService<LauncherApps>()?.unregisterCallback(mPackageEventReceiver)
128+
mAgentContextByProfile.clear()
129+
}
89130
}
131+
132+
const val FLAG_ALLOW_EMBEDDED = -0x80000000

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public static class Conversation {
3939
private static final String SCHEME_ORIGINAL_NAME = "ON:";
4040

4141
final int id;
42-
@Nullable String key; // The user name in WeChat
42+
volatile @Nullable String key; // The user name in WeChat
4343
int count;
4444
CharSequence title;
4545
CharSequence summary;
Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
package com.oasisfeng.nevo.decorators.wechat
22

3-
import android.annotation.SuppressLint
43
import android.content.Context
54
import android.content.pm.ShortcutManager
65
import android.graphics.Bitmap
76
import android.graphics.Canvas
87
import android.graphics.drawable.AdaptiveIconDrawable
98
import android.graphics.drawable.Icon
10-
import android.os.Build
9+
import android.os.Build.VERSION.SDK_INT
1110
import android.os.Build.VERSION_CODES.O
1211
import androidx.annotation.RequiresApi
12+
import androidx.core.content.getSystemService
1313
import androidx.core.graphics.drawable.IconCompat
1414

15-
object IconHelper {
15+
@RequiresApi(O) object IconHelper {
1616

17-
@JvmStatic fun convertToAdaptiveIcon(context: Context, sm: ShortcutManager, source: IconCompat): Icon
18-
= if (Build.VERSION.SDK_INT < O) @Suppress("DEPRECATION") source.toIcon()
19-
else @SuppressLint("SwitchIntDef") when (source.type) {
20-
Icon.TYPE_ADAPTIVE_BITMAP, Icon.TYPE_RESOURCE -> @Suppress("DEPRECATION") source.toIcon()
21-
else -> Icon.createWithAdaptiveBitmap(drawableToBitmap(context, sm, source)) }
17+
@JvmStatic fun convertToAdaptiveIcon(context: Context, source: IconCompat): Icon
18+
= if (source.type == Icon.TYPE_RESOURCE) source.toIcon(null)
19+
else source.toLocalAdaptiveIcon(context, context.getSystemService()!!)
2220

23-
@RequiresApi(O) private fun drawableToBitmap(context: Context, sm: ShortcutManager, icon: IconCompat): Bitmap {
21+
fun drawableToBitmap(context: Context, sm: ShortcutManager, icon: IconCompat): Bitmap {
2422
val extraInsetFraction = AdaptiveIconDrawable.getExtraInsetFraction()
2523
val width = sm.iconMaxWidth; val height = sm.iconMaxHeight
2624
val xInset = (width * extraInsetFraction).toInt(); val yInset = (height * extraInsetFraction).toInt()
@@ -29,4 +27,9 @@ object IconHelper {
2927
setBounds(xInset, yInset, width + xInset, height + yInset)
3028
draw(Canvas(bitmap)) }}
3129
}
32-
}
30+
}
31+
32+
fun IconCompat.toLocalAdaptiveIcon(context: Context, sm: ShortcutManager): Icon
33+
= @Suppress("CascadeIf") if (SDK_INT < O) toIcon(null)
34+
else if (type == Icon.TYPE_ADAPTIVE_BITMAP) toIcon(null)
35+
else Icon.createWithAdaptiveBitmap(IconHelper.drawableToBitmap(context, sm, this))

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import androidx.core.app.NotificationCompat.MessagingStyle;
3131
import androidx.core.app.NotificationCompat.MessagingStyle.Message;
3232
import androidx.core.app.Person;
33-
import androidx.core.util.Consumer;
3433

3534
import com.oasisfeng.nevo.decorators.wechat.ConversationManager.Conversation;
3635
import com.oasisfeng.nevo.sdk.MutableNotification;
@@ -81,8 +80,6 @@ class MessagingBuilder {
8180
private static final String KEY_DATA_URI= "uri";
8281
private static final String KEY_EXTRAS_BUNDLE = "extras";
8382

84-
private static final String KEY_USERNAME = "key_username";
85-
8683
@Nullable MessagingStyle buildFromArchive(final Conversation conversation, final Notification n, final CharSequence title, final List<StatusBarNotification> archive) {
8784
// Chat history in big content view
8885
if (archive.isEmpty()) {
@@ -135,23 +132,14 @@ class MessagingBuilder {
135132
return messaging;
136133
}
137134

138-
@Nullable MessagingStyle buildFromConversation(final Conversation conversation,
139-
final MutableStatusBarNotification sbn, final Consumer<String> conversation_key_receiver) {
135+
@Nullable MessagingStyle buildFromConversation(final Conversation conversation, final MutableStatusBarNotification sbn) {
140136
final CarExtender.UnreadConversation ext = conversation.ext;
141137
if (ext == null) return null;
142138
final MutableNotification n = sbn.getNotification();
143139
final long latest_timestamp = ext.getLatestTimestamp();
144140
if (latest_timestamp > 0) n.when = conversation.timestamp = latest_timestamp;
145141

146142
final PendingIntent on_reply = ext.getReplyPendingIntent();
147-
if (conversation.key == null && on_reply != null) try {
148-
final PendingIntent.OnFinished callback = (p, intent, r, d, b) ->
149-
conversation_key_receiver.accept(intent.getStringExtra(KEY_USERNAME));
150-
on_reply.send(mContext, 0, new Intent(""/* noop */), callback, null);
151-
} catch (final PendingIntent.CanceledException e) {
152-
Log.e(TAG, "Error parsing reply intent.", e);
153-
}
154-
155143
final PendingIntent on_read = ext.getReadPendingIntent();
156144
if (on_read != null) mMarkReadPendingIntents.put(sbn.getKey(), on_read); // Mapped by evolved key,
157145

0 commit comments

Comments
 (0)