Android 15 AOSP Notification分析

Android 15 AOSP 通知系统架构


目录

  1. 通知系统总体架构
  2. [NotificationManagerService 核心架构](#NotificationManagerService 核心架构)
  3. 通知核心流程
  4. [Toast 系统架构](#Toast 系统架构)
  5. [SystemUI 通知展示机制](#SystemUI 通知展示机制)
  6. 通知高级特性
  7. 通知属性和类型系统
  8. 应用开发指南
  9. [Framework 定制指南](#Framework 定制指南)

1. 通知系统总体架构

1.1 系统架构概览

Android 通知系统是一个复杂的多层架构,涉及应用层、Framework 层和 SystemUI 层的协同工作。
Persistence
SystemUI Layer
Framework Layer - System Server
Application Layer
NotificationManager API
Toast API
Binder IPC
Binder IPC
管理
管理
协作
协作
协作
协作
协作
NotificationListenerService
接收通知
展示
展示
展示
读写
读写
读写
App
NotificationManager
Toast
NotificationManagerService
NotificationRecord
ToastRecord
RankingHelper
GroupHelper
PreferencesHelper
SnoozeHelper
ZenModeHelper
NotificationListener
NotificationEntryManager
NotificationPanel
HeadsUpManager
StatusBar
NotificationHistory
Policy File
Preferences XML

1.2 关键组件说明

组件 路径 职责
NotificationManagerService frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java 通知系统的核心服务,负责通知的生命周期管理
NotificationManager frameworks/base/core/java/android/app/NotificationManager.java 应用层 API,提供发送通知的接口
Notification frameworks/base/core/java/android/app/Notification.java 通知数据模型类
NotificationRecord frameworks/base/services/core/java/com/android/server/notification/NotificationRecord.java 系统内部通知记录,包含通知的所有元数据
RankingHelper frameworks/base/services/core/java/com/android/server/notification/RankingHelper.java 负责通知排序和重要性计算
GroupHelper frameworks/base/services/core/java/com/android/server/notification/GroupHelper.java 负责通知分组和自动分组逻辑
NotificationListenerService frameworks/base/core/java/android/service/notification/NotificationListenerService.java SystemUI 和第三方监听器的基类

1.3 核心数据流

SystemUI NotificationListenerService RankingHelper NotificationManagerService NotificationManager 应用程序 SystemUI NotificationListenerService RankingHelper NotificationManagerService NotificationManager 应用程序 notify(id, notification) enqueueNotification() [Binder] 权限检查 创建 NotificationRecord extractSignals() 返回排序信息 添加到通知列表 onNotificationPosted() 更新通知界面 显示通知


2. NotificationManagerService 核心架构

2.1 类结构和继承关系

文件路径 : frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
总行数: 13564 行
manages
uses
uses
uses
uses
uses
implements
NotificationManagerService
+INotificationManager.Stub mService
+WorkerHandler mHandler
+RankingHandler mRankingHandler
+ArrayList<NotificationRecord> mNotificationList
+ArrayMap<String,NotificationRecord> mNotificationsByKey
+ArrayList<NotificationRecord> mEnqueuedNotifications
+ArrayList<ToastRecord> mToastQueue
+enqueueNotificationWithTag()
+cancelNotificationWithTag()
+postNotification()
+buzzBeepBlink()
+handleRankingReconsideration()
<<abstract>>
SystemService
+onStart()
+onBootPhase()
<<interface>>
INotificationManager
NotificationRecord
+StatusBarNotification sbn
+NotificationChannel channel
+int importance
+long rankingTimeMs
+NotificationStats stats
RankingHelper
+extractSignals()
+sort()
+indexOf()
GroupHelper
+onNotificationPosted()
+maybeAutoGroupNotifications()
+getAutogroupSummary()
PreferencesHelper
+getNotificationChannel()
+getImportance()
+canShowBadge()
SnoozeHelper
+snooze()
+repost()
+cancel()
ZenModeHelper
+applyRestrictions()
+getZenMode()
+shouldIntercept()

2.2 核心成员变量

java 复制代码
// 源码位置: NotificationManagerService.java:669-762

// 线程和 Handler
final HandlerThread mRankingThread = new HandlerThread("ranker", 
    Process.THREAD_PRIORITY_BACKGROUND);
WorkerHandler mHandler;
RankingHandler mRankingHandler;

// 通知存储 (使用 mNotificationLock 同步)
final Object mNotificationLock = new Object();
@GuardedBy("mNotificationLock")
final ArrayList<NotificationRecord> mNotificationList = new ArrayList<>();
@GuardedBy("mNotificationLock")
final ArrayMap<String, NotificationRecord> mNotificationsByKey = new ArrayMap<>();
@GuardedBy("mNotificationLock")
final ArrayList<NotificationRecord> mEnqueuedNotifications = new ArrayList<>();
@GuardedBy("mNotificationLock")
final ArrayMap<Integer, ArrayMap<String, String>> mAutobundledSummaries = new ArrayMap<>();

// Toast 队列
final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();
@GuardedBy("mToastQueue")
private boolean mIsCurrentToastShown = false;
private MultiRateLimiter mToastRateLimiter;

// 核心辅助类
RankingHelper mRankingHelper;
PreferencesHelper mPreferencesHelper;
NotificationListeners mListeners;
NotificationAssistants mAssistants;
ConditionProviders mConditionProviders;
GroupHelper mGroupHelper;
SnoozeHelper mSnoozeHelper;
ZenModeHelper mZenModeHelper;
NotificationAttentionHelper mAttentionHelper;

// 限流和配额
float mMaxPackageEnqueueRate = DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE; // 5/s
static final int MAX_PACKAGE_NOTIFICATIONS = 50;
static final int MAX_PACKAGE_TOASTS = 5;

2.3 服务初始化流程

ZenModeHelper GroupHelper RankingHelper NotificationManagerService SystemServer ZenModeHelper GroupHelper RankingHelper NotificationManagerService SystemServer new NotificationManagerService(context) 初始化成员变量 onStart() publishBinderService("notification", mService) publishLocalService(NotificationManagerInternal) onBootPhase(PHASE_SYSTEM_SERVICES_READY) 获取其他系统服务引用 new RankingHelper() new GroupHelper() new ZenModeHelper() 启动 WorkerThread 启动 RankingThread 注册广播接收器 加载持久化配置 onBootPhase(PHASE_THIRD_PARTY_APPS_CAN_START) 绑定 NotificationListeners systemReady = true

关键代码片段:

java 复制代码
// 源码位置: NotificationManagerService.java:2333-2560

@Override
public void onStart() {
    // 创建核心辅助类
    mRankingHelper = new RankingHelper(getContext(), mRankingThread.getLooper(),
            mPackageManagerClient, mRankingHandler, mUsageStats,
            mPreferencesHelper.createExtractors());
    
    mGroupHelper = new GroupHelper(getContext(), mPackageManagerInternal, 
            mHandler.getLooper(), new GroupHelper.Callback() {
        @Override
        public void addAutoGroup(String key, String overrideGroupKey) {
            addAutogroupKeyLocked(key, overrideGroupKey, true);
        }
        // ... 其他回调
    });
    
    // 初始化 ZenMode
    mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), 
            mConditionProviders, new SystemUiSystemPropertiesFlags.FlagResolver());
    
    // 发布 Binder 服务
    publishBinderService(Context.NOTIFICATION_SERVICE, mService);
    publishLocalService(NotificationManagerInternal.class, mInternalService);
}

2.4 内部类和接口

NotificationManagerService 包含多个重要的内部类:

2.4.1 NotificationListeners
java 复制代码
// 源码位置: NotificationManagerService.java:11888-13032

public class NotificationListeners extends ManagedServices {
    static final String TAG_ENABLED_NOTIFICATION_LISTENERS = "enabled_listeners";
    
    @Override
    protected void onServiceAdded(ManagedServiceInfo info) {
        final INotificationListener listener = (INotificationListener) info.service;
        final NotificationRankingUpdate update;
        synchronized (mNotificationLock) {
            update = makeRankingUpdateLocked(info);
        }
        try {
            listener.onListenerConnected(update);
        } catch (RemoteException e) {
            // 处理异常
        }
    }
}

职责:

  • 管理所有注册的 NotificationListenerService
  • 向监听器分发通知事件 (posted, removed, updated)
  • 处理监听器的权限和生命周期
2.4.2 NotificationAssistants
java 复制代码
// 源码位置: NotificationManagerService.java:11280-11887

public class NotificationAssistants extends ManagedServices {
    // 通知助手服务,用于智能通知排序和调整
    
    @GuardedBy("mNotificationLock")
    void onNotificationEnqueuedLocked(NotificationRecord r) {
        final StatusBarNotification sbn = r.getSbn();
        notifyAssistantLocked(
            sbn,
            true /* sameUserOnly */,
            (assistant, sbnHolder) -> {
                try {
                    assistant.onNotificationEnqueued(sbnHolder);
                } catch (RemoteException ex) {
                    Log.e(TAG, "unable to notify assistant", ex);
                }
            });
    }
}

职责:

  • 管理 NotificationAssistantService (如 Google Assistant)
  • 提供智能通知调整建议
  • 处理 Adjustment (调整) 信息
2.4.3 Archive
java 复制代码
// 源码位置: NotificationManagerService.java:772-912

static class Archive {
    final SparseArray<Boolean> mEnabled;
    final int mBufferSize;
    final LinkedList<Pair<StatusBarNotification, Integer>> mBuffer;
    
    public void record(StatusBarNotification sbn, int reason) {
        if (!mEnabled.get(sbn.getNormalizedUserId(), false)) {
            return;
        }
        synchronized (mBufferLock) {
            if (mBuffer.size() == mBufferSize) {
                mBuffer.removeFirst();
            }
            mBuffer.addLast(new Pair<>(sbn.cloneLight(), reason));
        }
    }
}

职责:

  • 维护最近通知的内存缓存
  • 用于 dumpsys 和调试
  • 支持通知历史查询

3. 通知核心流程

3.1 通知添加流程 (Enqueue)

3.1.1 完整流程图

NotificationListeners AttentionHelper GroupHelper RankingHelper PostNotificationRunnable EnqueueNotificationRunnable NotificationManagerService NotificationManager 应用程序 NotificationListeners AttentionHelper GroupHelper RankingHelper PostNotificationRunnable EnqueueNotificationRunnable NotificationManagerService NotificationManager 应用程序 WorkerHandler 线程 WorkerHandler 线程 notify(id, notification) 构建 Notification enqueueNotificationWithTag() [Binder] checkCallingUid() 权限检查 fixNotification() 创建 NotificationRecord new EnqueueNotificationRunnable() mHandler.post(ENQ) enqueueNotification() 检查是否 snoozed mEnqueuedNotifications.add(r) scheduleTimeoutLocked() handleGroupedNotificationLocked() 通知 Assistant new PostNotificationRunnable() mHandler.post(POST) [延迟 200ms] postNotification() 检查 app banned 检查 blocked channel 从 mEnqueuedNotifications 移除 添加到 mNotificationList mNotificationsByKey.put() extractSignals(r) 运行 SignalExtractors 返回信号 sort(mNotificationList) onNotificationPosted() buzzBeepBlinkLocked() 播放声音/震动/LED notifyPostedLocked() 通知所有监听器

3.1.2 关键代码分析

第一阶段: 应用层调用

java 复制代码
// 源码位置: frameworks/base/core/java/android/app/NotificationManager.java

public void notify(int id, Notification notification) {
    notify(null, id, notification);
}

public void notify(String tag, int id, Notification notification) {
    notifyAsUser(tag, id, notification, mContext.getUser());
}

private void notifyAsUser(String tag, int id, Notification notification, UserHandle user) {
    INotificationManager service = getService();
    String pkg = mContext.getPackageName();
    
    // 修复通知对象
    fixLegacySmallIcon(notification, pkg);
    
    // Binder 调用
    try {
        service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(),
                tag, id, notification, user.getIdentifier());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

第二阶段: NMS 接收和验证

java 复制代码
// 源码位置: NotificationManagerService.java:3882-3941

@Override
public void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id,
        Notification notification, int userId) {
    enqueueNotification(pkg, opPkg, Binder.getCallingUid(),
            Binder.getCallingPid(), tag, id, notification, userId, true);
}

public void enqueueNotification(String pkg, String opPkg, int callingUid, int callingPid,
        String tag, int id, Notification notification, int userId, boolean isAppProvided) {
    // 1. 权限检查
    checkCallerIsSystemOrSameApp(pkg);
    
    // 2. 用户 ID 检查
    final int notificationUid = resolveNotificationUid(opPkg, callingUid, userId);
    
    // 3. 检查通知数量限制
    if (mNotificationsByKey.size() >= MAX_PACKAGE_NOTIFICATIONS) {
        // 限流逻辑
    }
    
    // 4. 修复通知内容
    fixNotification(notification, pkg, tag, id, userId);
    
    // 5. 创建 NotificationRecord
    final NotificationRecord r = new NotificationRecord(getContext(), 
            new StatusBarNotification(pkg, opPkg, id, tag, notificationUid, 
                    callingPid, notification, user, null, 
                    System.currentTimeMillis()),
            channel);
    
    // 6. 提交到 WorkerHandler
    mHandler.post(new EnqueueNotificationRunnable(userId, r, isAppForeground, 
            isAppProvided, tracker));
}

第三阶段: 入队处理

java 复制代码
// 源码位置: NotificationManagerService.java:8820-8943

protected class EnqueueNotificationRunnable implements Runnable {
    @Override
    public void run() {
        enqueued = enqueueNotification();
    }
    
    private boolean enqueueNotification() {
        synchronized (mNotificationLock) {
            // 1. 检查是否被 snooze
            final long snoozeAt = mSnoozeHelper.getSnoozeTimeForUnpostedNotification(
                    r.getUser().getIdentifier(), r.getSbn().getPackageName(), 
                    r.getSbn().getKey());
            if (snoozeAt > currentTime) {
                (new SnoozeNotificationRunnable(r.getSbn().getKey(),
                        snoozeAt - currentTime, null)).snoozeLocked(r);
                return false;
            }
            
            // 2. 添加到入队列表
            mEnqueuedNotifications.add(r);
            
            // 3. 设置超时
            scheduleTimeoutLocked(r);
            
            // 4. 处理分组
            handleGroupedNotificationLocked(r, old, callingUid, callingPid);
            
            // 5. 通知 Assistant
            if (mAssistants.isEnabled()) {
                mAssistants.onNotificationEnqueuedLocked(r);
                mHandler.postDelayed(
                    new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(),
                            r.getUid(), mTracker),
                    DELAY_FOR_ASSISTANT_TIME); // 200ms
            } else {
                mHandler.post(new PostNotificationRunnable(...));
            }
            
            return true;
        }
    }
}

第四阶段: 发布通知

java 复制代码
// 源码位置: NotificationManagerService.java:8960-9201

protected class PostNotificationRunnable implements Runnable {
    @Override
    public void run() {
        posted = postNotification();
    }
    
    private boolean postNotification() {
        boolean appBanned = !areNotificationsEnabledForPackageInt(pkg, uid);
        
        synchronized (mNotificationLock) {
            // 1. 从入队列表查找
            NotificationRecord r = findNotificationByListLocked(
                    mEnqueuedNotifications, key);
            if (r == null) {
                return false;
            }
            
            // 2. 检查是否被屏蔽
            if (appBanned || isRecordBlockedLocked(r)) {
                mUsageStats.registerBlocked(r);
                return false;
            }
            
            // 3. 添加到通知列表
            int index = indexOfNotificationLocked(n.getKey());
            if (index < 0) {
                mNotificationList.add(r);
                mUsageStats.registerPostedByApp(r);
            } else {
                old = mNotificationList.get(index);
                mNotificationList.set(index, r);
                mUsageStats.registerUpdatedByApp(r, old);
            }
            
            mNotificationsByKey.put(n.getKey(), r);
            
            // 4. 信号提取和排序
            mRankingHelper.extractSignals(r);
            mRankingHelper.sort(mNotificationList);
            
            // 5. 处理自动分组
            mGroupHelper.onNotificationPosted(r, hasAutoGroupSummaryLocked(r));
            
            // 6. 声音/震动/LED
            if (!r.isHidden()) {
                buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r,
                        new NotificationAttentionHelper.Signals(
                                mUserProfiles.isCurrentProfile(r.getUserId()),
                                mListenerHints));
            }
            
            // 7. 通知监听器
            notifyListenersPostedAndLogLocked(r, old, mTracker, maybeReport);
            
            return true;
        }
    }
}

3.2 通知更新流程

通知更新实际上是通过相同的 enqueueNotification 流程实现的。NMS 会检查是否存在相同 key 的通知:

java 复制代码
// Key 的构成: pkg + "|" + tag + "|" + id + "|" + userId

String key = r.getSbn().getKey();
NotificationRecord old = mNotificationsByKey.get(key);

if (old != null) {
    // 这是更新操作
    r.copyRankingInformation(old); // 保留排序信息
    r.isUpdate = true;
    
    // 检查是否需要重新播放声音
    final boolean isInterruptive = isVisuallyInterruptive(old, r);
    r.setTextChanged(isInterruptive);
}

3.3 通知移除流程 (Cancel)

3.3.1 取消流程图

NotificationListeners GroupHelper CancelNotificationRunnable NotificationManagerService 应用程序/用户 NotificationListeners GroupHelper CancelNotificationRunnable NotificationManagerService 应用程序/用户 WorkerHandler 线程 alt [是分组摘要] 可能通知被 snooze 了 alt [找到通知] [未找到通知] cancelNotification(tag, id) new CancelNotificationRunnable() mHandler.post(CNL) run() findNotificationLocked() 返回 NotificationRecord 检查 mustHaveFlags/mustNotHaveFlags removeFromNotificationListsLocked(r) mNotificationList.remove(r) mNotificationsByKey.remove(key) mEnqueuedNotifications.remove(r) cancelNotificationLocked() 取消声音/震动/LED notifyRemovedLocked() 通知所有监听器 cancelGroupChildrenLocked() 递归取消子通知 mSnoozeHelper.cancel()

3.3.2 关键代码
java 复制代码
// 源码位置: NotificationManagerService.java:8638-8780

protected class CancelNotificationRunnable implements Runnable {
    private final String mPkg;
    private final String mTag;
    private final int mId;
    private final int mMustHaveFlags;
    private final int mMustNotHaveFlags;
    private final int mReason;
    
    @Override
    public void run() {
        synchronized (mNotificationLock) {
            // 1. 查找通知
            NotificationRecord r = findNotificationLocked(mPkg, mTag, mId, mUserId);
            
            if (r != null) {
                // 2. 检查 flag 条件
                if ((r.getNotification().flags & mMustHaveFlags) != mMustHaveFlags) {
                    return; // 不满足必须包含的 flag
                }
                if ((r.getNotification().flags & mMustNotHaveFlags) != 0) {
                    return; // 包含不应该有的 flag
                }
                
                // 3. 从列表中移除
                boolean wasPosted = removeFromNotificationListsLocked(r);
                
                // 4. 取消通知
                cancelNotificationLocked(r, mSendDelete, mReason, mRank, 
                        mCount, wasPosted, listenerName, mCancellationElapsedTimeMs);
                
                // 5. 如果是组摘要,取消所有子通知
                if (r.getNotification().isGroupSummary()) {
                    cancelGroupChildrenLocked(mUserId, mPkg, mCallingUid, 
                            mCallingPid, listenerName, mSendDelete, 
                            childrenFlagChecker, groupKeyChecker, 
                            r.getGroupKey(), mReason, mCancellationElapsedTimeMs);
                }
                
                // 6. 更新 LED
                mAttentionHelper.updateLightsLocked();
            } else {
                // 可能被 snooze,尝试取消
                mSnoozeHelper.cancel(mUserId, mPkg, mTag, mId);
            }
        }
    }
}

@GuardedBy("mNotificationLock")
void cancelNotificationLocked(NotificationRecord r, boolean sendDelete, int reason,
        int rank, int count, boolean wasPosted, String listenerName,
        long cancellationElapsedTimeMs) {
    final String canceledKey = r.getKey();
    
    // 1. 取消超时任务
    if (Flags.allNotifsNeedTtl()) {
        mTtlHelper.cancelScheduledTimeoutLocked(r);
    } else {
        cancelTimeoutLocked(r);
    }
    
    // 2. 记录统计
    recordCallerLocked(r);
    mUsageStats.registerDismissedByUser(r);
    
    // 3. 取消注意力效果
    mAttentionHelper.clearEffectsLocked(canceledKey, r);
    
    // 4. 发送删除 Intent
    if (sendDelete) {
        PendingIntent deleteIntent = r.getNotification().deleteIntent;
        if (deleteIntent != null) {
            try {
                deleteIntent.send();
            } catch (PendingIntent.CanceledException ex) {
                Slog.w(TAG, "canceled PendingIntent for " + r.getSbn().getPackageName(), ex);
            }
        }
    }
    
    // 5. 记录到 Archive
    mArchive.record(r.getSbn(), reason);
    
    // 6. 通知监听器
    if (wasPosted) {
        mListeners.notifyRemovedLocked(r, reason, r.getStats());
    }
    
    // 7. 保存到历史
    mHistoryManager.addNotification(new HistoricalNotification.Builder()
            .setPackage(r.getSbn().getPackageName())
            .setChannelId(r.getChannel().getId())
            .setUid(r.getUid())
            .setUserId(r.getUserId())
            .setPostedTimeMs(r.getSbn().getPostTime())
            .setTitle(getTitleString(r.getNotification()))
            .setText(getTextString(r.getNotification()))
            .setIcon(r.getNotification().getSmallIcon())
            .build());
}

3.4 通知限制机制

3.4.1 频率限制 (Rate Limiting)
java 复制代码
// 源码位置: NotificationManagerService.java:428-429

static final int MAX_PACKAGE_NOTIFICATIONS = 50;
static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 5f; // 每秒 5 个

// 检查频率限制
private void checkRateLimiting(String pkg, int uid, NotificationRecord r) {
    long now = SystemClock.elapsedRealtime();
    
    synchronized (mNotificationLock) {
        // 检查是否超过最大通知数
        int count = getNotificationCountLocked(pkg, userId, id, tag);
        if (count >= MAX_PACKAGE_NOTIFICATIONS) {
            throw new IllegalStateException("Package has exceeded max notification count");
        }
        
        // 检查发送频率
        float rate = mRateEstimator.getRate(pkg, uid);
        if (rate > mMaxPackageEnqueueRate) {
            // 超过频率限制
            limitRate(pkg, uid);
        }
    }
}
3.4.2 权限检查
java 复制代码
// 检查通知权限
private void checkNotificationOp(String pkg, int uid) {
    int mode = mAppOps.noteOpNoThrow(
            AppOpsManager.OP_POST_NOTIFICATION, uid, pkg);
    if (mode != AppOpsManager.MODE_ALLOWED) {
        throw new SecurityException("Package " + pkg + " does not have permission");
    }
}

// 检查 Channel 是否被屏蔽
@GuardedBy("mNotificationLock")
private boolean isRecordBlockedLocked(NotificationRecord r) {
    final String pkg = r.getSbn().getPackageName();
    final int uid = r.getUid();
    
    // 检查应用级别屏蔽
    if (!areNotificationsEnabledForPackageInt(pkg, uid)) {
        return true;
    }
    
    // 检查 Channel 屏蔽
    if (r.getChannel().getImportance() == IMPORTANCE_NONE) {
        return true;
    }
    
    // 检查 DND 拦截
    if (mZenModeHelper.shouldIntercept(r)) {
        r.setIntercept(true);
        return true;
    }
    
    return false;
}
3.4.3 配额管理
java 复制代码
// 源码位置: NotificationManagerService.java

// 每个应用的通知配额
private static final int NOTIFICATION_QUOTA_WINDOW_MS = 60000; // 1 分钟
private static final int NOTIFICATION_QUOTA_COUNT = 50; // 每分钟 50 个

// 检查配额
private boolean checkQuota(String pkg, int uid) {
    long now = SystemClock.elapsedRealtime();
    long windowStart = now - NOTIFICATION_QUOTA_WINDOW_MS;
    
    synchronized (mQuotaTracker) {
        List<Long> timestamps = mQuotaTracker.get(pkg);
        if (timestamps == null) {
            timestamps = new ArrayList<>();
            mQuotaTracker.put(pkg, timestamps);
        }
        
        // 清理过期时间戳
        timestamps.removeIf(ts -> ts < windowStart);
        
        // 检查是否超过配额
        if (timestamps.size() >= NOTIFICATION_QUOTA_COUNT) {
            Slog.w(TAG, "Package " + pkg + " exceeded notification quota");
            return false;
        }
        
        timestamps.add(now);
        return true;
    }
}

4. Toast 系统架构

4.1 Toast 类层次结构

creates
Toast
+LENGTH_SHORT
+LENGTH_LONG
+show()
+cancel()
+makeText()
<<abstract>>
ToastRecord
+int uid
+int pid
+String pkg
+IBinder token
+show()
+hide()
+getDuration()
TextToastRecord
+ITransientNotificationCallback callback
+show()
+hide()
+isAppRendered()
CustomToastRecord
+ITransientNotification callback
+show()
+hide()
+keepProcessAlive()

4.2 Toast 显示流程

WindowManager Toast Queue NotificationManagerService Toast 应用程序 WindowManager Toast Queue NotificationManagerService Toast 应用程序 alt [队列为空或当前 Toast 已显示] 等待超时或用户取消 alt [队列中还有 Toast] Toast.makeText(context, text, duration) new Toast(context) show() enqueueToast() [Binder] 检查权限和频率限制 创建 ToastRecord mToastQueue.add(record) showNextToastLocked() addView(toastView) 显示 Toast scheduleTimeoutLocked() handleTimeout() removeView(toastView) showNextToastLocked() 显示下一个 Toast

4.3 Toast 核心代码

应用层 API:

java 复制代码
// 源码位置: frameworks/base/core/java/android/widget/Toast.java

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    Toast result = new Toast(context, null);
    
    LayoutInflater inflate = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
    TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
    tv.setText(text);
    
    result.mNextView = v;
    result.mDuration = duration;
    
    return result;
}

public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }
    
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    
    try {
        service.enqueueToast(pkg, mToken, tn, mDuration, mContext.getDisplayId());
    } catch (RemoteException e) {
        // Empty
    }
}

NMS Toast 管理:

java 复制代码
// 源码位置: NotificationManagerService.java:3727-3824

@Override
public void enqueueToast(String pkg, IBinder token, ITransientNotification callback,
        int duration, int displayId) {
    // 1. 权限检查
    checkCallerIsSystemOrSameApp(pkg);
    
    // 2. 检查是否是系统 Toast
    boolean isSystemToast = isCallerSystemOrPhone() 
            || PackageManager.PERMISSION_SYSTEM_ALERT_WINDOW 
                == mPackageManager.checkPermission(pkg, Binder.getCallingUid());
    
    // 3. 检查后台限制
    if (!isSystemToast && !isPackageForeground(pkg, Binder.getCallingUid())) {
        // Android Q+ 限制后台自定义 Toast
        if (CompatChanges.isChangeEnabled(CHANGE_BACKGROUND_CUSTOM_TOAST_BLOCK, 
                Binder.getCallingUid())) {
            Slog.w(TAG, "Blocking custom toast from package " + pkg);
            return;
        }
    }
    
    // 4. 频率限制
    synchronized (mToastQueue) {
        int callingPid = Binder.getCallingPid();
        long callingId = Binder.clearCallingIdentity();
        try {
            // 检查是否超过频率限制
            if (!mToastRateLimitingDisabledUids.contains(Binder.getCallingUid())) {
                if (!mToastRateLimiter.noteEvent(Binder.getCallingUid(), pkg, 
                        TOAST_QUOTA_TAG)) {
                    Slog.w(TAG, "Package " + pkg + " has exceeded toast rate limit");
                    return;
                }
            }
            
            ToastRecord record;
            int index = indexOfToastLocked(pkg, token);
            
            // 5. 如果已存在,更新 duration
            if (index >= 0) {
                record = mToastQueue.get(index);
                record.update(duration);
            } else {
                // 6. 创建新的 ToastRecord
                Binder windowToken = new Binder();
                mWindowManagerInternal.addWindowToken(windowToken, 
                        TYPE_TOAST, displayId, null);
                
                record = new CustomToastRecord(this, Binder.getCallingUid(), 
                        callingPid, pkg, isSystemToast, token, callback, 
                        duration, windowToken, displayId);
                
                mToastQueue.add(record);
                index = mToastQueue.size() - 1;
            }
            
            // 7. 如果队列中只有一个,立即显示
            if (index == 0) {
                showNextToastLocked(false);
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}

@GuardedBy("mToastQueue")
void showNextToastLocked(boolean lastToastWasTextRecord) {
    ToastRecord record = mToastQueue.get(0);
    
    while (record != null) {
        if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.token);
        
        try {
            // 显示 Toast
            if (record.show()) {
                // 成功显示,设置超时
                scheduleDurationReachedLocked(record, lastToastWasTextRecord);
                mIsCurrentToastShown = true;
                return;
            }
        } catch (RemoteException e) {
            Slog.w(TAG, "Toast callback failed", e);
        }
        
        // 显示失败,移除并尝试下一个
        int index = mToastQueue.indexOf(record);
        if (index >= 0) {
            mToastQueue.remove(index);
        }
        record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
    }
}

@GuardedBy("mToastQueue")
void cancelToastLocked(int index) {
    ToastRecord record = mToastQueue.get(index);
    
    try {
        record.hide();
    } catch (RemoteException e) {
        Slog.w(TAG, "Toast callback hide() failed", e);
    }
    
    // 从队列移除
    ToastRecord lastToast = mToastQueue.remove(index);
    
    // 清理 WindowToken
    mWindowManagerInternal.removeWindowToken(record.windowToken, 
            true, record.displayId);
    
    // 如果移除的是队首,显示下一个
    if (index == 0) {
        mIsCurrentToastShown = false;
        showNextToastLocked(lastToast instanceof TextToastRecord);
    }
}

4.4 Toast 限流机制

java 复制代码
// 源码位置: NotificationManagerService.java:492-496

// Toast 频率限制规则
private static final MultiRateLimiter.RateLimit[] TOAST_RATE_LIMITS = {
    MultiRateLimiter.RateLimit.create(3, Duration.ofSeconds(20)),  // 20 秒内最多 3 个
    MultiRateLimiter.RateLimit.create(5, Duration.ofSeconds(42)),  // 42 秒内最多 5 个
    MultiRateLimiter.RateLimit.create(6, Duration.ofSeconds(68)),  // 68 秒内最多 6 个
};

// 初始化限流器
mToastRateLimiter = new MultiRateLimiter.Builder(context)
        .addRateLimits(TOAST_RATE_LIMITS)
        .build();

限流算法: 使用滑动窗口算法,在指定时间窗口内限制 Toast 数量。每次超过限制后,等待时间会递增,防止应用滥用 Toast。

4.5 Toast 与 Notification 的区别

特性 Toast Notification
持久性 临时显示 (2-3.5 秒) 持久化,直到被取消
交互性 不可交互 可点击、可操作
位置 屏幕底部/中间 通知栏
优先级 无优先级概念 支持多级 importance
权限 POST_NOTIFICATIONS (Android 13+) POST_NOTIFICATIONS
后台限制 Android Q+ 限制后台自定义 Toast 无后台限制
队列管理 全局队列,按包排队 无队列,直接显示
频率限制 严格限流 (3/20s) 宽松限流 (50/min)

5. SystemUI 通知展示机制

5.1 NotificationListenerService 机制

5.1.1 监听器架构

updates
<<abstract>>
NotificationListenerService
+onListenerConnected()
+onNotificationPosted()
+onNotificationRemoved()
+onNotificationRankingUpdate()
+onListenerDisconnected()
+cancelNotification()
+snoozeNotification()
<<interface>>
INotificationListener
+onNotificationPosted()
+onNotificationRemoved()
+onNotificationRankingUpdate()
<<SystemUI>>
NotificationListener
+onNotificationPosted()
+onNotificationRemoved()
+onNotificationChannelModified()
NotificationEntryManager
+addNotification()
+updateNotification()
+removeNotification()

5.1.2 监听器注册和连接流程

NotificationListeners NotificationManagerService NotificationListenerService SystemUI NotificationListeners NotificationManagerService NotificationListenerService SystemUI 当有新通知时 onCreate() registerAsSystemService() [Binder] registerSystemService() 创建 ManagedServiceInfo onListenerConnected() [Binder] 初始化 NotificationEntryManager notifyPostedLocked() 遍历所有监听器 onNotificationPosted() [Binder] 更新通知列表 显示通知

5.1.3 核心代码

NotificationListenerService 基类:

java 复制代码
// 源码位置: frameworks/base/core/java/android/service/notification/NotificationListenerService.java:112-2000

public abstract class NotificationListenerService extends Service {
    
    @Override
    public IBinder onBind(Intent intent) {
        if (mWrapper == null) {
            mWrapper = new NotificationListenerWrapper();
        }
        return mWrapper;
    }
    
    /**
     * 当监听器连接到 NMS 时调用
     */
    public void onListenerConnected() {
        // 子类实现,如 SystemUI 的 NotificationListener
    }
    
    /**
     * 当有新通知发布时调用
     */
    public void onNotificationPosted(StatusBarNotification sbn) {
        // 子类实现
    }
    
    /**
     * 当通知被移除时调用
     */
    public void onNotificationRemoved(StatusBarNotification sbn) {
        // 子类实现
    }
    
    /**
     * 当通知排序更新时调用
     */
    public void onNotificationRankingUpdate(RankingMap rankingMap) {
        // 子类实现
    }
    
    /**
     * Binder 包装类
     */
    private class NotificationListenerWrapper extends INotificationListener.Stub {
        @Override
        public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
                NotificationRankingUpdate update) {
            try {
                StatusBarNotification sbn = sbnHolder.get();
                if (sbn == null) {
                    Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification");
                    return;
                }
                
                // 更新 RankingMap
                synchronized (mLock) {
                    applyUpdateLocked(update);
                }
                
                // 回调到主线程
                mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_POSTED,
                        new Pair<StatusBarNotification, RankingMap>(sbn, getRankingMap()))
                        .sendToTarget();
            } catch (RemoteException e) {
                Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e);
            }
        }
        
        @Override
        public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder,
                NotificationRankingUpdate update, 
                NotificationStats stats, int reason) {
            // 类似实现
        }
    }
}

NMS 通知监听器:

java 复制代码
// 源码位置: NotificationManagerService.java:11888-13032

public class NotificationListeners extends ManagedServices {
    
    @GuardedBy("mNotificationLock")
    void notifyPostedLocked(NotificationRecord r, NotificationRecord old,
            boolean notifyAllListeners) {
        try {
            StatusBarNotification sbn = r.getSbn();
            StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
            
            // 创建轻量级通知对象 (用于跨进程传输)
            final StatusBarNotification sbnLight = sbn.cloneLight();
            
            // 遍历所有注册的监听器
            for (final ManagedServiceInfo info : getServices()) {
                boolean sbnVisible = isVisibleToListener(sbn, info);
                boolean oldSbnVisible = oldSbn != null ? isVisibleToListener(oldSbn, info) : false;
                
                // 判断是否需要通知此监听器
                if (!sbnVisible && !oldSbnVisible) {
                    continue; // 新旧通知都不可见,跳过
                }
                
                final NotificationRankingUpdate update = makeRankingUpdateLocked(info);
                
                // 异步回调监听器
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        notifyPosted(info, sbnLight, update);
                    }
                });
            }
        } catch (Exception e) {
            Slog.e(TAG, "Could not notify listeners", e);
        }
    }
    
    private void notifyPosted(final ManagedServiceInfo info,
            final StatusBarNotification sbn,
            final NotificationRankingUpdate rankingUpdate) {
        final INotificationListener listener = (INotificationListener) info.service;
        
        try {
            listener.onNotificationPosted(
                    new StatusBarNotificationHolder(sbn), rankingUpdate);
        } catch (DeadObjectException ex) {
            Slog.wtf(TAG, "Listener " + info.component + " died");
            unbindService(info.service, info.userid);
        } catch (RemoteException ex) {
            Log.e(TAG, "Error notifying listener", ex);
        }
    }
    
    /**
     * 判断通知对于某个监听器是否可见
     */
    private boolean isVisibleToListener(StatusBarNotification sbn, ManagedServiceInfo info) {
        if (!info.enabledAndUserMatches(sbn.getUserId())) {
            return false;
        }
        
        // 检查监听器的过滤器
        if (info.mListenerFilter != null) {
            int types = info.mListenerFilter.getTypes();
            
            // 检查是否匹配过滤类型
            if ((types & FLAG_FILTER_TYPE_ONGOING) == 0 
                    && r.getNotification().isForegroundService()) {
                return false;
            }
            // 其他过滤逻辑...
        }
        
        return true;
    }
}

5.2 SystemUI 通知处理流程

5.2.1 NotificationEntryManager

SystemUI 的 NotificationEntryManager 是核心组件,负责管理所有通知条目。

文件路径 : frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java

java 复制代码
// NotificationEntry 代表 SystemUI 中的一个通知条目
public final class NotificationEntry {
    private StatusBarNotification mSbn;
    private Ranking mRanking;
    private NotificationChannel mChannel;
    private int mImportance;
    private boolean mSensitive;
    private long mLastAudiblyAlertedMs;
    
    // UI 相关状态
    private ExpandableNotificationRow mRow;
    private boolean mExpanded;
    private boolean mUserLocked;
    
    // Heads-Up 状态
    private boolean mIsHeadsUp;
    private long mHeadsUpStartTime;
}
5.2.2 通知显示流程

HeadsUpManager NotificationPanel ExpandableNotificationRow NotificationInflater NotificationEntryManager NotificationListener NotificationManagerService HeadsUpManager NotificationPanel ExpandableNotificationRow NotificationInflater NotificationEntryManager NotificationListener NotificationManagerService alt [是 Heads-Up 通知] onNotificationPosted(sbn) addNotification(entry) 创建 NotificationEntry inflateViews(entry) inflate content views inflate expanded views inflate heads-up views 创建 ExpandableNotificationRow 应用通知内容 updateNotificationViews() 添加/更新通知行 showNotification(entry) 显示悬浮通知

5.3 NotificationPanel 实现

5.3.1 通知列表结构

NotificationPanel
NotificationStackScrollLayout
NotificationSection: Silent
NotificationSection: Alerting
NotificationSection: Conversations
ExpandableNotificationRow 1
ExpandableNotificationRow 2
ExpandableNotificationRow 3
ExpandableNotificationRow 4
ExpandableNotificationRow 5
ExpandableNotificationRow 6

5.3.2 通知排序和分组

排序规则 (按优先级):

  1. Heads-Up 通知: 最高优先级,显示在顶部
  2. 对话通知: 重要的消息类通知
  3. 告警通知: 普通优先级通知
  4. 静默通知: 低优先级通知
  5. 已清除通知: 历史通知

代码实现:

java 复制代码
// 源码位置: frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/

public class NotificationSectionsManager {
    
    private static final int BUCKET_HEADS_UP = 0;
    private static final int BUCKET_FOREGROUND_SERVICE = 1;
    private static final int BUCKET_PEOPLE = 2;
    private static final int BUCKET_ALERTING = 3;
    private static final int BUCKET_SILENT = 4;
    
    /**
     * 确定通知应该属于哪个分区
     */
    public int getBucket(NotificationEntry entry) {
        if (entry.isHeadsUp()) {
            return BUCKET_HEADS_UP;
        }
        
        if (entry.isConversation()) {
            return BUCKET_PEOPLE;
        }
        
        if (entry.getImportance() >= IMPORTANCE_DEFAULT) {
            return BUCKET_ALERTING;
        }
        
        return BUCKET_SILENT;
    }
    
    /**
     * 比较两个通知的排序位置
     */
    public int compare(NotificationEntry a, NotificationEntry b) {
        int bucketA = getBucket(a);
        int bucketB = getBucket(b);
        
        if (bucketA != bucketB) {
            return Integer.compare(bucketA, bucketB);
        }
        
        // 同一分区内按时间排序
        return Long.compare(b.getRankingTimeMs(), a.getRankingTimeMs());
    }
}

5.4 Heads-Up Notification (HUN)

5.4.1 HUN 触发条件

Heads-Up 通知是一种在屏幕顶部短暂显示的高优先级通知。触发条件:

java 复制代码
// 源码位置: NotificationManagerService.java -> NotificationAttentionHelper.java

public boolean shouldHeadsUp(NotificationRecord r) {
    // 1. 必须是高优先级
    if (r.getImportance() < IMPORTANCE_HIGH) {
        return false;
    }
    
    // 2. 不能被 DND 拦截
    if (r.isIntercepted()) {
        return false;
    }
    
    // 3. 必须有声音或震动
    if (!r.getSound() != null || r.getVibration() != null) {
        return false;
    }
    
    // 4. 不能是静默更新
    if (r.isUpdate && !r.isInterruptive()) {
        return false;
    }
    
    // 5. 检查应用前台状态
    if (!isAppForeground && !r.getChannel().canBypassDnd()) {
        return false;
    }
    
    // 6. 检查屏幕状态
    if (!mPowerManager.isInteractive() && !r.getNotification().fullScreenIntent) {
        return false;
    }
    
    return true;
}
5.4.2 HUN 显示流程

HeadsUpNotificationView HeadsUpManager NotificationListener AttentionHelper NotificationManagerService HeadsUpNotificationView HeadsUpManager NotificationListener AttentionHelper NotificationManagerService 显示 4-5 秒 alt [满足 HUN 条件] buzzBeepBlinkLocked(record) shouldHeadsUp(record)? 标记为 HeadsUp showNotification(entry) 创建 HeadsUpEntry 创建 HUN View 从顶部滑入 scheduleRemoval(entry, duration) removeNotification() 滑出动画 更新通知状态

HeadsUpManager 核心代码:

java 复制代码
// 源码位置: frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManager.java

public class HeadsUpManager {
    private static final int HEADS_UP_NOTIFICATION_DECAY = 4000; // 4 秒
    private final ArrayMap<String, HeadsUpEntry> mHeadsUpNotifications = new ArrayMap<>();
    
    public void showNotification(NotificationEntry entry) {
        String key = entry.getKey();
        
        // 如果已经在显示,延长时间
        HeadsUpEntry headsUpEntry = mHeadsUpNotifications.get(key);
        if (headsUpEntry != null) {
            headsUpEntry.updateEntry(true);
            return;
        }
        
        // 创建新的 HeadsUp 条目
        headsUpEntry = new HeadsUpEntry();
        headsUpEntry.setEntry(entry);
        
        mHeadsUpNotifications.put(key, headsUpEntry);
        
        // 通知 UI 显示
        mListeners.forEach(listener -> listener.onHeadsUpStateChanged(entry, true));
        
        // 设置自动移除定时器
        mHandler.postDelayed(() -> {
            removeHeadsUpNotification(key);
        }, HEADS_UP_NOTIFICATION_DECAY);
    }
    
    private void removeHeadsUpNotification(String key) {
        HeadsUpEntry headsUpEntry = mHeadsUpNotifications.remove(key);
        if (headsUpEntry != null) {
            NotificationEntry entry = headsUpEntry.getEntry();
            mListeners.forEach(listener -> listener.onHeadsUpStateChanged(entry, false));
        }
    }
}

6. 通知高级特性

6.1 通知分组 (Grouping)

6.1.1 分组概念

Android 支持将多个相关通知分组显示,提高通知栏的整洁度。分组有两种方式:

  1. 手动分组 : 应用显式设置 group 属性
  2. 自动分组: 系统自动将同一应用的多个通知分组

自动分组
Auto Summary
Notification 1
Notification 2
Notification 3
Notification 4
手动分组
Group Summary
Child 1
Child 2
Child 3

6.1.2 手动分组实现

应用层代码:

java 复制代码
// 创建组摘要通知
Notification summaryNotification = new Notification.Builder(context, CHANNEL_ID)
        .setContentTitle("5 new messages")
        .setContentText("alice@example.com")
        .setSmallIcon(R.drawable.ic_notification)
        .setGroup("email_group")  // 设置组 ID
        .setGroupSummary(true)    // 标记为组摘要
        .build();

notificationManager.notify(SUMMARY_ID, summaryNotification);

// 创建子通知
for (int i = 0; i < 5; i++) {
    Notification childNotification = new Notification.Builder(context, CHANNEL_ID)
            .setContentTitle("Email " + i)
            .setContentText("Message content " + i)
            .setSmallIcon(R.drawable.ic_notification)
            .setGroup("email_group")  // 相同的组 ID
            .build();
    
    notificationManager.notify(i, childNotification);
}

NMS 分组处理:

java 复制代码
// 源码位置: NotificationManagerService.java:7700-7850

@GuardedBy("mNotificationLock")
private void handleGroupedNotificationLocked(NotificationRecord r, 
        NotificationRecord old, int callingUid, int callingPid) {
    StatusBarNotification sbn = r.getSbn();
    Notification n = sbn.getNotification();
    
    if (n.isGroupSummary()) {
        // 这是组摘要
        String groupKey = sbn.getGroupKey();
        mSummaryByGroupKey.put(groupKey, r);
    }
    
    if (n.isGroupChild()) {
        // 这是组子通知
        // 检查是否有对应的摘要
        String groupKey = sbn.getGroupKey();
        NotificationRecord summary = mSummaryByGroupKey.get(groupKey);
        
        if (summary == null && mGroupHelper.isAutoGrouped(groupKey)) {
            // 没有摘要但是自动分组,创建自动摘要
            createAutoGroupSummaryLocked(
                    userId, sbn.getPackageName(), groupKey, 
                    AutoGroupSummary, callingUid);
        }
    }
}
6.1.3 自动分组 (Auto-grouping)

当应用发送 4 个或更多未分组的通知时,系统会自动将它们分组。

java 复制代码
// 源码位置: GroupHelper.java:63-1498

public class GroupHelper {
    private static final String AUTOGROUP_KEY = "ranker_group";
    private int mAutoGroupAtCount = 4; // 自动分组阈值
    
    /**
     * 检查是否应该自动分组
     */
    public boolean onNotificationPosted(NotificationRecord r, 
            boolean hasAutoGroupSummary) {
        String pkg = r.getSbn().getPackageName();
        int userId = r.getUserId();
        
        // 获取该应用的所有未分组通知
        ArrayList<NotificationRecord> ungroupedNotifications = 
                getUngroupedNotificationsForPackage(pkg, userId);
        
        if (ungroupedNotifications.size() >= mAutoGroupAtCount) {
            // 达到自动分组阈值
            
            if (!hasAutoGroupSummary) {
                // 创建自动分组摘要
                createAutoGroupSummary(pkg, userId);
            }
            
            // 将所有通知添加自动分组 key
            for (NotificationRecord nr : ungroupedNotifications) {
                addAutoGroupKey(nr.getKey());
            }
            
            return true; // 触发了自动分组
        }
        
        return false;
    }
    
    /**
     * 创建自动分组摘要
     */
    private void createAutoGroupSummary(String pkg, int userId) {
        Notification summaryNotification = new Notification.Builder(mContext, 
                DEFAULT_CHANNEL_ID)
                .setSmallIcon(getAppIcon(pkg))
                .setGroupSummary(true)
                .setGroup(AUTOGROUP_KEY)
                .setFlag(FLAG_AUTOGROUP_SUMMARY, true)
                .build();
        
        // 通过 NMS 内部方法发送摘要通知
        mCallback.addAutoGroupSummary(pkg, userId, AUTOGROUP_KEY, summaryNotification);
    }
}

6.2 通知折叠 (Bundling)

通知折叠是指在通知栏中收起多个通知,显示为一个简洁的摘要。

展开/折叠状态:

java 复制代码
// ExpandableNotificationRow 管理单个通知的展开状态
public class ExpandableNotificationRow extends ActivatableNotificationView {
    private boolean mExpanded = false;
    private boolean mUserLocked = false; // 用户是否手动锁定展开状态
    
    public void setExpanded(boolean expanded) {
        if (mExpanded == expanded) {
            return;
        }
        
        mExpanded = expanded;
        
        if (mExpanded) {
            // 显示详细内容视图
            showExpandedContent();
        } else {
            // 显示简要内容视图
            showCollapsedContent();
        }
        
        notifyHeightChanged(true);
    }
}

6.3 通知排序 (Ranking)

6.3.1 排序算法

NMS 使用 RankingHelper 进行通知排序,综合考虑多个因素:

java 复制代码
// 源码位置: RankingHelper.java:43-700

public class RankingHelper {
    
    /**
     * 通知排序
     */
    public void sort(ArrayList<NotificationRecord> notificationList) {
        Collections.sort(notificationList, mNotificationComparator);
    }
    
    private final Comparator<NotificationRecord> mNotificationComparator = 
            new Comparator<NotificationRecord>() {
        @Override
        public int compare(NotificationRecord left, NotificationRecord right) {
            // 1. 比较 Criticality (关键性)
            int leftCriticality = left.getCriticality();
            int rightCriticality = right.getCriticality();
            if (leftCriticality != rightCriticality) {
                return Integer.compare(rightCriticality, leftCriticality);
            }
            
            // 2. 比较 Importance
            int leftImportance = left.getImportance();
            int rightImportance = right.getImportance();
            if (leftImportance != rightImportance) {
                return Integer.compare(rightImportance, leftImportance);
            }
            
            // 3. 比较 Ranking Score
            float leftScore = left.getRankingScore();
            float rightScore = right.getRankingScore();
            if (leftScore != rightScore) {
                return Float.compare(rightScore, leftScore);
            }
            
            // 4. 比较时间 (最新的在前)
            long leftTime = left.getRankingTimeMs();
            long rightTime = right.getRankingTimeMs();
            return Long.compare(rightTime, leftTime);
        }
    };
}
6.3.2 Signal Extractors

RankingHelper 使用一系列 Signal Extractors 提取通知的各种信号:

java 复制代码
// Signal Extractor 接口
public interface NotificationSignalExtractor {
    void initialize(Context context, NotificationUsageStats usageStats);
    RankingReconsideration process(NotificationRecord record);
    void setConfig(RankingConfig config);
}

// 示例: ImportanceExtractor
public class ImportanceExtractor implements NotificationSignalExtractor {
    @Override
    public RankingReconsideration process(NotificationRecord record) {
        if (record.getImportance() == IMPORTANCE_UNSPECIFIED) {
            // 从 Channel 获取 importance
            NotificationChannel channel = record.getChannel();
            if (channel != null) {
                record.setImportance(channel.getImportance());
            }
        }
        return null;
    }
}

// 其他 Extractors:
// - BadgeExtractor: 提取角标信息
// - BubbleExtractor: 提取气泡信息
// - CriticalNotificationExtractor: 提取关键性
// - PriorityExtractor: 提取优先级
// - ValidateNotificationPeople: 验证联系人信息
// - VisibilityExtractor: 提取可见性
// - ZenModeExtractor: 提取 DND 拦截信息

6.4 通知重要性 (Importance)

6.4.1 Importance 级别
java 复制代码
// 源码位置: frameworks/base/core/java/android/app/NotificationManager.java

public class NotificationManager {
    // 不显示,不播放声音,不出现在状态栏
    public static final int IMPORTANCE_NONE = 0;
    
    // 到达通知栏底部,不播放声音
    public static final int IMPORTANCE_MIN = 1;
    
    // 到达通知栏,不播放声音
    public static final int IMPORTANCE_LOW = 2;
    
    // 到达通知栏,播放声音
    public static final int IMPORTANCE_DEFAULT = 3;
    
    // 到达通知栏,播放声音,可能显示 HUN
    public static final int IMPORTANCE_HIGH = 4;
    
    // Android 15 已弃用
    // public static final int IMPORTANCE_MAX = 5;
}
6.4.2 Importance 影响
Importance 通知栏 状态栏图标 声音 震动 HUN 全屏 Intent
NONE
MIN ✅ (底部)
LOW
DEFAULT
HIGH

7. 通知属性和类型系统

7.1 Notification.Builder 详解

7.1.1 常用属性
java 复制代码
// 源码位置: frameworks/base/core/java/android/app/Notification.java

public static class Builder {
    
    // 基本属性
    public Builder setSmallIcon(int icon);                    // 小图标 (必须)
    public Builder setContentTitle(CharSequence title);       // 标题
    public Builder setContentText(CharSequence text);         // 内容
    public Builder setSubText(CharSequence text);             // 子文本
    public Builder setContentInfo(CharSequence info);         // 额外信息
    
    // 交互
    public Builder setContentIntent(PendingIntent intent);   // 点击 Intent
    public Builder setDeleteIntent(PendingIntent intent);    // 删除 Intent
    public Builder addAction(Action action);                 // 操作按钮
    
    // 时间
    public Builder setShowWhen(boolean show);                // 是否显示时间
    public Builder setWhen(long when);                       // 时间戳
    public Builder setUsesChronometer(boolean b);            // 使用计时器
    
    // 优先级和重要性
    public Builder setPriority(int pri);                     // 优先级 (已废弃)
    public Builder setCategory(String category);             // 分类
    
    // 通知 Channel (Android 8.0+)
    public Builder setChannelId(String channelId);           // Channel ID
    
    // 视觉效果
    public Builder setLargeIcon(Icon icon);                  // 大图标
    public Builder setColor(int argb);                       // 颜色
    public Builder setColorized(boolean colorize);           // 是否着色
    public Builder setLights(int argb, int onMs, int offMs); // LED 灯
    
    // 声音和震动
    public Builder setSound(Uri sound);                      // 声音
    public Builder setSound(Uri sound, AudioAttributes attr); // 声音 + 属性
    public Builder setVibrate(long[] pattern);               // 震动模式
    
    // 行为标志
    public Builder setAutoCancel(boolean autoCancel);        // 点击后自动取消
    public Builder setOngoing(boolean ongoing);              // 正在进行中
    public Builder setOnlyAlertOnce(boolean onlyAlertOnce); // 只提醒一次
    public Builder setLocalOnly(boolean localOnly);          // 仅本地显示
    
    // 分组
    public Builder setGroup(String groupKey);                // 分组 Key
    public Builder setGroupSummary(boolean isGroupSummary); // 是否为组摘要
    public Builder setSortKey(String sortKey);              // 排序 Key
    
    // 高级功能
    public Builder setStyle(Style style);                    // 通知样式
    public Builder setPublicVersion(Notification n);         // 锁屏公开版本
    public Builder setTimeoutAfter(long durationMs);        // 超时自动取消
    public Builder setBubbleMetadata(BubbleMetadata data);  // 气泡元数据
    public Builder setShortcutId(String shortcutId);        // 快捷方式 ID
    public Builder setLocusId(LocusId locusId);            // Locus ID
    
    // 前台服务
    public Builder setForegroundServiceBehavior(int behavior); // 前台服务行为
    
    // 构建
    public Notification build();
}
7.1.2 通知样式 (Styles)

Android 提供多种通知样式:

java 复制代码
// 1. BigTextStyle - 大文本样式
Notification notification = new Notification.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_notification)
        .setContentTitle("Long Message")
        .setStyle(new Notification.BigTextStyle()
                .bigText("This is a very long message that will be expanded " +
                        "when the user expands the notification. It can contain " +
                        "multiple lines of text and will scroll if too long.")
                .setBigContentTitle("Expanded Title")
                .setSummaryText("Summary"))
        .build();

// 2. BigPictureStyle - 大图片样式
Notification notification = new Notification.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_notification)
        .setContentTitle("Image")
        .setStyle(new Notification.BigPictureStyle()
                .bigPicture(bitmap)
                .bigLargeIcon((Icon) null) // 展开后隐藏大图标
                .setBigContentTitle("Photo Title")
                .setSummaryText("Photo description"))
        .build();

// 3. InboxStyle - 收件箱样式
Notification notification = new Notification.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_notification)
        .setContentTitle("5 new emails")
        .setStyle(new Notification.InboxStyle()
                .addLine("Alice: Meeting at 3pm?")
                .addLine("Bob: Lunch tomorrow?")
                .addLine("Charlie: Review my code")
                .addLine("David: Happy birthday!")
                .addLine("Eve: Coffee break?")
                .setBigContentTitle("Inbox")
                .setSummaryText("alice@example.com"))
        .build();

// 4. MessagingStyle - 消息样式
Person user = new Person.Builder()
        .setName("Me")
        .setIcon(Icon.createWithResource(context, R.drawable.ic_person))
        .build();

Person sender = new Person.Builder()
        .setName("Alice")
        .setIcon(Icon.createWithResource(context, R.drawable.ic_alice))
        .build();

Notification notification = new Notification.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_notification)
        .setStyle(new Notification.MessagingStyle(user)
                .addMessage("Hi there!", System.currentTimeMillis(), sender)
                .addMessage("How are you?", System.currentTimeMillis(), sender)
                .addMessage("I'm good, thanks!", System.currentTimeMillis(), user)
                .setConversationTitle("Chat with Alice"))
        .build();

// 5. MediaStyle - 媒体样式
Notification notification = new Notification.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_music)
        .setContentTitle("Song Title")
        .setContentText("Artist Name")
        .setStyle(new Notification.MediaStyle()
                .setMediaSession(mediaSession.getSessionToken())
                .setShowActionsInCompactView(0, 1, 2))
        .addAction(R.drawable.ic_skip_previous, "Previous", prevPendingIntent)
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent)
        .addAction(R.drawable.ic_skip_next, "Next", nextPendingIntent)
        .build();

// 6. CallStyle - 通话样式 (Android 12+)
Person caller = new Person.Builder()
        .setName("Alice")
        .setImportant(true)
        .build();

Notification notification = new Notification.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_call)
        .setStyle(Notification.CallStyle.forIncomingCall(
                caller, declinePendingIntent, answerPendingIntent))
        .build();

7.2 NotificationChannel (通知渠道)

7.2.1 Channel 概念

Android 8.0 (API 26) 引入了 NotificationChannel,用户可以精细控制每个渠道的通知行为。
应用
Channel Group 1: 社交
Channel Group 2: 工作
Channel: 聊天消息
Channel: 朋友动态
Channel: 邮件
Channel: 日历提醒

7.2.2 创建和使用 Channel
java 复制代码
// 1. 创建 NotificationChannel
NotificationChannel channel = new NotificationChannel(
        "chat_channel",                    // Channel ID
        "Chat Messages",                   // 用户可见名称
        NotificationManager.IMPORTANCE_HIGH // 重要性级别
);

// 2. 配置 Channel 属性
channel.setDescription("Messages from your contacts");
channel.enableLights(true);
channel.setLightColor(Color.BLUE);
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{0, 500, 250, 500});
channel.setSound(soundUri, audioAttributes);
channel.setShowBadge(true);
channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
channel.setBypassDnd(false); // 是否允许绕过勿扰模式

// 3. 注册 Channel
NotificationManager manager = 
        (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);

// 4. 使用 Channel 发送通知
Notification notification = new Notification.Builder(context, "chat_channel")
        .setSmallIcon(R.drawable.ic_chat)
        .setContentTitle("New Message")
        .setContentText("Hello!")
        .build();

manager.notify(1, notification);
7.2.3 NotificationChannelGroup

将相关的 Channel 分组管理:

java 复制代码
// 创建 Channel Group
NotificationChannelGroup group = new NotificationChannelGroup(
        "social_group",
        "Social"
);
group.setDescription("All social notifications");

NotificationManager manager = 
        (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannelGroup(group);

// 创建属于该 Group 的 Channels
NotificationChannel chatChannel = new NotificationChannel(
        "chat_channel", "Chat", IMPORTANCE_HIGH);
chatChannel.setGroup("social_group");

NotificationChannel feedChannel = new NotificationChannel(
        "feed_channel", "Feed", IMPORTANCE_LOW);
feedChannel.setGroup("social_group");

manager.createNotificationChannels(Arrays.asList(chatChannel, feedChannel));
7.2.4 Channel 特性

文件路径 : frameworks/base/core/java/android/app/NotificationChannel.java

java 复制代码
public final class NotificationChannel implements Parcelable {
    private String mId;                      // Channel ID (不可变)
    private String mName;                    // 名称
    private String mDesc;                    // 描述
    private int mImportance;                 // 重要性
    private boolean mBypassDnd;              // 绕过 DND
    private int mLockscreenVisibility;       // 锁屏可见性
    private Uri mSound;                      // 声音
    private boolean mLights;                 // LED 灯
    private int mLightColor;                 // LED 颜色
    private long[] mVibration;               // 震动模式
    private int mUserLockedFields;           // 用户锁定的字段
    private boolean mShowBadge;              // 显示角标
    private boolean mDeleted;                // 是否已删除
    private String mGroup;                   // 所属 Group
    private AudioAttributes mAudioAttributes; // 音频属性
    private String mConversationId;          // 对话 ID
    private String mParentId;                // 父 Channel ID
    private boolean mDemoted;                // 是否被降级
    private boolean mImportantConvo;         // 是否重要对话
    
    // 用户锁定字段标志
    public static final int USER_LOCKED_IMPORTANCE = 0x00000001;
    public static final int USER_LOCKED_VISIBILITY = 0x00000002;
    public static final int USER_LOCKED_SOUND = 0x00000020;
    public static final int USER_LOCKED_VIBRATION = 0x00000040;
    public static final int USER_LOCKED_LIGHTS = 0x00000080;
    public static final int USER_LOCKED_SHOW_BADGE = 0x00000100;
}

注意事项:

  1. Channel 创建后,重要性级别只能降低不能提高 (用户可以修改)
  2. Channel ID 不可变,删除后不能重用
  3. 用户对 Channel 的修改会覆盖应用的设置

7.3 通知类型 (Categories)

java 复制代码
// 源码位置: Notification.java

public class Notification {
    // 通知类别
    public static final String CATEGORY_CALL = "call";           // 来电
    public static final String CATEGORY_MESSAGE = "msg";         // 消息
    public static final String CATEGORY_EMAIL = "email";         // 邮件
    public static final String CATEGORY_EVENT = "event";         // 日历事件
    public static final String CATEGORY_PROMO = "promo";         // 促销
    public static final String CATEGORY_ALARM = "alarm";         // 闹钟
    public static final String CATEGORY_PROGRESS = "progress";   // 进度
    public static final String CATEGORY_SOCIAL = "social";       // 社交
    public static final String CATEGORY_ERROR = "err";           // 错误
    public static final String CATEGORY_TRANSPORT = "transport"; // 媒体传输
    public static final String CATEGORY_SYS = "sys";            // 系统
    public static final String CATEGORY_SERVICE = "service";     // 服务
    public static final String CATEGORY_RECOMMENDATION = "recommendation"; // 推荐
    public static final String CATEGORY_STATUS = "status";       // 状态
    public static final String CATEGORY_REMINDER = "reminder";   // 提醒
    public static final String CATEGORY_WORKOUT = "workout";     // 运动 (Android 13+)
    public static final String CATEGORY_LOCATION_SHARING = "location_sharing"; // 位置分享 (Android 14+)
    public static final String CATEGORY_STOPWATCH = "stopwatch"; // 秒表 (Android 14+)
}

Category 用途:

  • 影响通知的 DND 过滤规则
  • 影响通知助手的智能分类
  • 影响通知的默认行为 (如 CATEGORY_CALL 默认为 IMPORTANCE_HIGH)

7.4 Notification Flags

java 复制代码
// 源码位置: Notification.java:750-850

public class Notification {
    // 基本标志
    public static final int FLAG_SHOW_LIGHTS        = 0x00000001; // 显示 LED
    public static final int FLAG_ONGOING_EVENT      = 0x00000002; // 正在进行
    public static final int FLAG_INSISTENT          = 0x00000004; // 持续提醒
    public static final int FLAG_ONLY_ALERT_ONCE    = 0x00000008; // 只提醒一次
    public static final int FLAG_AUTO_CANCEL        = 0x00000010; // 点击后取消
    public static final int FLAG_NO_CLEAR           = 0x00000020; // 不可清除
    public static final int FLAG_FOREGROUND_SERVICE = 0x00000040; // 前台服务
    public static final int FLAG_HIGH_PRIORITY      = 0x00000080; // 高优先级 (已废弃)
    public static final int FLAG_LOCAL_ONLY         = 0x00000100; // 仅本地
    public static final int FLAG_GROUP_SUMMARY      = 0x00000200; // 组摘要
    public static final int FLAG_AUTOGROUP_SUMMARY  = 0x00000400; // 自动组摘要 (系统内部)
    public static final int FLAG_BUBBLE             = 0x00001000; // 气泡
    public static final int FLAG_USER_INITIATED_JOB = 0x00002000; // 用户发起的作业
    public static final int FLAG_NO_DISMISS         = 0x00004000; // 不可划掉 (Android 15+)
    
    // 内部标志
    public static final int FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY = 0x00010000; // 生命周期扩展
    public static final int FLAG_FSI_REQUESTED_BUT_DENIED = 0x00020000; // FSI 被拒绝
}

7.5 可见性 (Visibility)

控制通知在锁屏上的显示方式:

java 复制代码
// 源码位置: Notification.java:1200-1250

public class Notification {
    // 可见性级别
    public static final int VISIBILITY_PUBLIC = 1;    // 完全显示
    public static final int VISIBILITY_PRIVATE = 0;   // 隐藏敏感内容
    public static final int VISIBILITY_SECRET = -1;   // 完全隐藏
    
    // 设置可见性
    public Builder setVisibility(int visibility);
    
    // 设置锁屏公开版本
    public Builder setPublicVersion(Notification n);
}

// 使用示例
Notification publicNotification = new Notification.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_chat)
        .setContentTitle("New Message")
        .setContentText("You have a new message") // 不显示具体内容
        .build();

Notification privateNotification = new Notification.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_chat)
        .setContentTitle("Alice")
        .setContentText("Hey, are you free tonight?") // 具体内容
        .setVisibility(Notification.VISIBILITY_PRIVATE)
        .setPublicVersion(publicNotification) // 锁屏显示公开版本
        .build();

manager.notify(1, privateNotification);

8. 应用开发指南

8.1 发送标准通知

java 复制代码
// 完整示例
public class NotificationHelper {
    private static final String CHANNEL_ID = "default_channel";
    private static final int NOTIFICATION_ID = 1;
    
    public static void sendBasicNotification(Context context) {
        // 1. 创建 NotificationChannel (Android 8.0+)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                    CHANNEL_ID,
                    "General Notifications",
                    NotificationManager.IMPORTANCE_DEFAULT
            );
            channel.setDescription("General app notifications");
            
            NotificationManager manager = 
                    context.getSystemService(NotificationManager.class);
            manager.createNotificationChannel(channel);
        }
        
        // 2. 创建点击 Intent
        Intent intent = new Intent(context, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(
                context, 0, intent, 
                PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
        );
        
        // 3. 构建通知
        Notification.Builder builder = new Notification.Builder(context, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle("Title")
                .setContentText("This is the notification content")
                .setContentIntent(pendingIntent)
                .setAutoCancel(true) // 点击后自动取消
                .setWhen(System.currentTimeMillis())
                .setShowWhen(true);
        
        // 4. 发送通知
        NotificationManager manager = 
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        manager.notify(NOTIFICATION_ID, builder.build());
    }
}

8.2 发送分组通知

java 复制代码
public static void sendGroupedNotifications(Context context) {
    NotificationManager manager = 
            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    
    String GROUP_KEY = "email_group";
    
    // 1. 发送多个子通知
    for (int i = 1; i <= 3; i++) {
        Notification notification = new Notification.Builder(context, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_email)
                .setContentTitle("Email " + i)
                .setContentText("Message content " + i)
                .setGroup(GROUP_KEY) // 设置组 Key
                .build();
        
        manager.notify(i, notification);
    }
    
    // 2. 发送组摘要通知
    Notification summaryNotification = new Notification.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_email)
            .setContentTitle("3 new emails")
            .setContentText("You have 3 unread emails")
            .setGroup(GROUP_KEY) // 相同的组 Key
            .setGroupSummary(true) // 标记为摘要
            .setStyle(new Notification.InboxStyle()
                    .addLine("Email 1: Subject 1")
                    .addLine("Email 2: Subject 2")
                    .addLine("Email 3: Subject 3")
                    .setBigContentTitle("Inbox")
                    .setSummaryText("3 new emails"))
            .build();
    
    manager.notify(0, summaryNotification); // 使用不同的 ID
}

8.3 发送高优先级通知 (HUN)

java 复制代码
public static void sendHeadsUpNotification(Context context) {
    // 1. 创建高重要性 Channel
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        NotificationChannel channel = new NotificationChannel(
                "urgent_channel",
                "Urgent Notifications",
                NotificationManager.IMPORTANCE_HIGH // 高重要性
        );
        channel.setDescription("Urgent notifications that require immediate attention");
        channel.enableVibration(true);
        channel.setSound(
                RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
                new AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_NOTIFICATION)
                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                        .build()
        );
        
        NotificationManager manager = context.getSystemService(NotificationManager.class);
        manager.createNotificationChannel(channel);
    }
    
    // 2. 构建高优先级通知
    Notification notification = new Notification.Builder(context, "urgent_channel")
            .setSmallIcon(R.drawable.ic_urgent)
            .setContentTitle("Urgent!")
            .setContentText("This is an urgent notification")
            .setPriority(Notification.PRIORITY_HIGH) // Android 7.1 及以下
            .setCategory(Notification.CATEGORY_MESSAGE)
            .setFullScreenIntent(pendingIntent, true) // 全屏 Intent (可选)
            .build();
    
    NotificationManager manager = 
            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    manager.notify(999, notification);
}

8.4 发送前台服务通知

java 复制代码
public class MyForegroundService extends Service {
    private static final int FOREGROUND_NOTIFICATION_ID = 1;
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 创建通知
        Notification notification = new Notification.Builder(this, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_service)
                .setContentTitle("Service Running")
                .setContentText("Your service is running in the foreground")
                .setOngoing(true) // 标记为正在进行
                .build();
        
        // 启动前台服务
        startForeground(FOREGROUND_NOTIFICATION_ID, notification);
        
        // 执行服务逻辑
        doWork();
        
        return START_STICKY;
    }
    
    private void doWork() {
        // 服务逻辑
    }
    
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

8.5 发送进度通知

java 复制代码
public static void sendProgressNotification(Context context) {
    NotificationManager manager = 
            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    
    final int PROGRESS_MAX = 100;
    final int PROGRESS_CURRENT = 0;
    
    Notification.Builder builder = new Notification.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_download)
            .setContentTitle("Downloading")
            .setContentText("Download in progress")
            .setProgress(PROGRESS_MAX, PROGRESS_CURRENT, false) // 不是不确定进度
            .setOngoing(true);
    
    manager.notify(1, builder.build());
    
    // 模拟进度更新
    new Thread(() -> {
        for (int progress = 0; progress <= 100; progress += 10) {
            builder.setProgress(PROGRESS_MAX, progress, false)
                    .setContentText("Downloaded " + progress + "%");
            manager.notify(1, builder.build());
            
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        // 完成后更新通知
        builder.setContentText("Download complete")
                .setProgress(0, 0, false)
                .setOngoing(false)
                .setAutoCancel(true);
        manager.notify(1, builder.build());
    }).start();
}

8.6 使用 MessagingStyle (对话通知)

java 复制代码
public static void sendMessagingNotification(Context context) {
    // 1. 创建 Person 对象
    Person me = new Person.Builder()
            .setName("Me")
            .setKey("user_me")
            .build();
    
    Person alice = new Person.Builder()
            .setName("Alice")
            .setKey("user_alice")
            .setIcon(Icon.createWithResource(context, R.drawable.avatar_alice))
            .setImportant(true) // 标记为重要联系人
            .build();
    
    // 2. 创建消息列表
    List<Notification.MessagingStyle.Message> messages = new ArrayList<>();
    messages.add(new Notification.MessagingStyle.Message(
            "Hey, how are you?",
            System.currentTimeMillis() - 60000,
            alice
    ));
    messages.add(new Notification.MessagingStyle.Message(
            "I'm good, thanks! How about you?",
            System.currentTimeMillis() - 30000,
            me
    ));
    messages.add(new Notification.MessagingStyle.Message(
            "Great! Want to grab coffee later?",
            System.currentTimeMillis(),
            alice
    ));
    
    // 3. 创建 MessagingStyle
    Notification.MessagingStyle style = new Notification.MessagingStyle(me)
            .setConversationTitle("Chat with Alice") // 可选,群聊时使用
            .setGroupConversation(false);
    
    for (Notification.MessagingStyle.Message message : messages) {
        style.addMessage(message);
    }
    
    // 4. 添加快捷回复
    RemoteInput remoteInput = new RemoteInput.Builder("key_reply")
            .setLabel("Reply")
            .build();
    
    Intent replyIntent = new Intent(context, ReplyReceiver.class);
    PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
            context, 0, replyIntent,
            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
    );
    
    Notification.Action replyAction = new Notification.Action.Builder(
            R.drawable.ic_reply, "Reply", replyPendingIntent)
            .addRemoteInput(remoteInput)
            .setAllowGeneratedReplies(true) // 允许智能回复
            .build();
    
    // 5. 构建通知
    Notification notification = new Notification.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_chat)
            .setStyle(style)
            .addAction(replyAction)
            .setShortcutId("alice_shortcut") // 关联快捷方式
            .setLocusId(new LocusId("alice_chat")) // 设置 Locus ID
            .setCategory(Notification.CATEGORY_MESSAGE)
            .build();
    
    NotificationManager manager = 
            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    manager.notify(1, notification);
}

9. Framework 定制指南

9.1 定制 NotificationManagerService

9.1.1 修改通知限制

场景: 增加每个应用的最大通知数量

java 复制代码
// 文件位置: frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

// 修改前:
static final int MAX_PACKAGE_NOTIFICATIONS = 50;

// 修改后:
static final int MAX_PACKAGE_NOTIFICATIONS = 100; // 增加到 100

// 或者通过系统属性动态配置:
private int getMaxPackageNotifications() {
    return SystemProperties.getInt(
            "persist.sys.max_package_notifications", 
            50  // 默认值
    );
}

场景: 修改通知频率限制

java 复制代码
// 修改前:
static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 5f; // 每秒 5 个

// 修改后:
static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 10f; // 每秒 10 个

// 或者添加白名单应用:
private boolean isRateLimitExempt(String pkg, int uid) {
    return pkg.equals("com.example.myapp") 
            || uid == Process.SYSTEM_UID;
}
9.1.2 定制通知排序

场景: 为特定应用提供更高的通知优先级

java 复制代码
// 文件位置: frameworks/base/services/core/java/com/android/server/notification/RankingHelper.java

public class CustomRankingHelper extends RankingHelper {
    
    private static final String[] PRIORITY_PACKAGES = {
        "com.android.systemui",
        "com.android.phone",
        "com.example.important.app"
    };
    
    @Override
    public void sort(ArrayList<NotificationRecord> notificationList) {
        Collections.sort(notificationList, new Comparator<NotificationRecord>() {
            @Override
            public int compare(NotificationRecord left, NotificationRecord right) {
                // 优先处理特权应用的通知
                boolean leftPriority = isPriorityPackage(left.getSbn().getPackageName());
                boolean rightPriority = isPriorityPackage(right.getSbn().getPackageName());
                
                if (leftPriority && !rightPriority) return -1;
                if (!leftPriority && rightPriority) return 1;
                
                // 其他按默认规则排序
                return super.compare(left, right);
            }
        });
    }
    
    private boolean isPriorityPackage(String pkg) {
        for (String priorityPkg : PRIORITY_PACKAGES) {
            if (priorityPkg.equals(pkg)) {
                return true;
            }
        }
        return false;
    }
}
9.1.3 添加自定义 SignalExtractor

场景: 根据通知内容提取自定义信号

java 复制代码
// 文件位置: frameworks/base/services/core/java/com/android/server/notification/CustomSignalExtractor.java

public class CustomSignalExtractor implements NotificationSignalExtractor {
    private static final String TAG = "CustomSignalExtractor";
    
    @Override
    public void initialize(Context context, NotificationUsageStats usageStats) {
        // 初始化逻辑
    }
    
    @Override
    public RankingReconsideration process(NotificationRecord record) {
        if (record == null || record.getNotification() == null) {
            return null;
        }
        
        Notification notification = record.getNotification();
        Bundle extras = notification.extras;
        
        // 示例: 检测紧急关键词
        String title = extras.getString(Notification.EXTRA_TITLE);
        String text = extras.getString(Notification.EXTRA_TEXT);
        
        if (containsUrgentKeyword(title) || containsUrgentKeyword(text)) {
            // 提升重要性
            record.setSystemImportance(IMPORTANCE_HIGH);
            Slog.d(TAG, "Detected urgent notification: " + record.getKey());
        }
        
        return null;
    }
    
    private boolean containsUrgentKeyword(String text) {
        if (text == null) return false;
        String lowerText = text.toLowerCase();
        return lowerText.contains("urgent") 
                || lowerText.contains("emergency")
                || lowerText.contains("critical");
    }
    
    @Override
    public void setConfig(RankingConfig config) {
        // 配置更新
    }
}

// 在 RankingHelper 中注册:
// frameworks/base/services/core/java/com/android/server/notification/RankingHelper.java
public NotificationSignalExtractor[] createExtractors() {
    return new NotificationSignalExtractor[] {
        new NotificationChannelExtractor(),
        new ImportanceExtractor(),
        new PriorityExtractor(),
        new CustomSignalExtractor(), // 添加自定义 Extractor
        // ... 其他 Extractors
    };
}
9.1.4 拦截和修改通知

场景: 在通知发送前修改内容

java 复制代码
// 文件位置: NotificationManagerService.java

// 在 EnqueueNotificationRunnable.enqueueNotification() 中添加:
private boolean enqueueNotification() {
    synchronized (mNotificationLock) {
        // ... 原有代码 ...
        
        // 自定义拦截和修改逻辑
        if (shouldInterceptNotification(r)) {
            Slog.w(TAG, "Intercepted notification: " + r.getKey());
            return false; // 拦截该通知
        }
        
        modifyNotificationIfNeeded(r);
        
        // ... 继续原有流程 ...
    }
}

private boolean shouldInterceptNotification(NotificationRecord r) {
    String pkg = r.getSbn().getPackageName();
    
    // 示例: 拦截某些应用在特定时间段的通知
    if (pkg.equals("com.example.noisy.app")) {
        Calendar now = Calendar.getInstance();
        int hour = now.get(Calendar.HOUR_OF_DAY);
        if (hour >= 22 || hour < 8) { // 晚上 10 点到早上 8 点
            return true; // 拦截
        }
    }
    
    return false;
}

private void modifyNotificationIfNeeded(NotificationRecord r) {
    Notification notification = r.getNotification();
    Bundle extras = notification.extras;
    
    // 示例: 为特定应用的通知添加前缀
    if (r.getSbn().getPackageName().equals("com.example.app")) {
        String originalTitle = extras.getString(Notification.EXTRA_TITLE);
        if (originalTitle != null) {
            extras.putString(Notification.EXTRA_TITLE, "[Custom] " + originalTitle);
        }
    }
}

9.2 定制 SystemUI 通知展示

9.2.1 修改通知布局

场景: 自定义通知卡片的外观

xml 复制代码
<!-- 文件位置: frameworks/base/packages/SystemUI/res/layout/status_bar_notification_row.xml -->

<!-- 在原有布局基础上添加自定义元素 -->
<com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    
    <!-- 原有内容 -->
    
    <!-- 添加自定义指示器 -->
    <ImageView
        android:id="@+id/custom_priority_indicator"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_gravity="end|top"
        android:visibility="gone"
        android:src="@drawable/ic_priority" />
</com.android.systemui.statusbar.notification.row.ExpandableNotificationRow>
java 复制代码
// 文件位置: frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java

public class ExpandableNotificationRow extends ActivatableNotificationView {
    private ImageView mCustomPriorityIndicator;
    
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mCustomPriorityIndicator = findViewById(R.id.custom_priority_indicator);
    }
    
    public void setEntry(NotificationEntry entry) {
        super.setEntry(entry);
        
        // 根据自定义逻辑显示优先级指示器
        if (isHighPriorityNotification(entry)) {
            mCustomPriorityIndicator.setVisibility(View.VISIBLE);
        } else {
            mCustomPriorityIndicator.setVisibility(View.GONE);
        }
    }
    
    private boolean isHighPriorityNotification(NotificationEntry entry) {
        return entry.getImportance() >= IMPORTANCE_HIGH
                && entry.getSbn().getNotification().category.equals(Notification.CATEGORY_MESSAGE);
    }
}
9.2.2 定制通知分组逻辑

场景: 按自定义规则对通知进行分组显示

java 复制代码
// 文件位置: frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.java

public class CustomNotificationSectionsManager extends NotificationSectionsManager {
    
    private static final int BUCKET_CUSTOM_PRIORITY = 0;
    private static final int BUCKET_CONVERSATIONS = 1;
    private static final int BUCKET_ALERTING = 2;
    private static final int BUCKET_SILENT = 3;
    
    @Override
    public int getBucket(NotificationEntry entry) {
        // 自定义分桶逻辑
        String pkg = entry.getSbn().getPackageName();
        
        // 特权应用单独一组
        if (isPrivilegedApp(pkg)) {
            return BUCKET_CUSTOM_PRIORITY;
        }
        
        // 对话通知
        if (entry.isConversation()) {
            return BUCKET_CONVERSATIONS;
        }
        
        // 根据重要性分组
        if (entry.getImportance() >= IMPORTANCE_DEFAULT) {
            return BUCKET_ALERTING;
        }
        
        return BUCKET_SILENT;
    }
    
    private boolean isPrivilegedApp(String pkg) {
        return pkg.equals("com.android.phone")
                || pkg.equals("com.android.systemui")
                || pkg.equals("com.android.messaging");
    }
}
9.2.3 自定义 Heads-Up 行为

场景: 修改 HUN 的显示时长和条件

java 复制代码
// 文件位置: frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManager.java

public class CustomHeadsUpManager extends HeadsUpManager {
    
    // 修改默认显示时长
    private static final int HEADS_UP_NOTIFICATION_DECAY_SHORT = 3000;  // 3 秒
    private static final int HEADS_UP_NOTIFICATION_DECAY_LONG = 6000;   // 6 秒
    
    @Override
    public void showNotification(NotificationEntry entry) {
        // 自定义显示时长
        int duration = getCustomDuration(entry);
        
        HeadsUpEntry headsUpEntry = new HeadsUpEntry();
        headsUpEntry.setEntry(entry);
        
        mHeadsUpNotifications.put(entry.getKey(), headsUpEntry);
        mListeners.forEach(listener -> listener.onHeadsUpStateChanged(entry, true));
        
        // 使用自定义时长
        mHandler.postDelayed(() -> {
            removeHeadsUpNotification(entry.getKey());
        }, duration);
    }
    
    private int getCustomDuration(NotificationEntry entry) {
        // 根据通知类型返回不同时长
        Notification n = entry.getSbn().getNotification();
        
        if (n.category.equals(Notification.CATEGORY_CALL)) {
            return HEADS_UP_NOTIFICATION_DECAY_LONG; // 来电显示更久
        }
        
        if (entry.isConversation()) {
            return HEADS_UP_NOTIFICATION_DECAY_SHORT; // 消息显示较短
        }
        
        return HEADS_UP_NOTIFICATION_DECAY; // 默认 4 秒
    }
}

9.3 常见定制场景

9.3.1 实现通知白名单
java 复制代码
// 场景: 某些应用的通知永远不会被限流或拦截

// 在 NotificationManagerService.java 中添加:
private static final String[] WHITELIST_PACKAGES = {
    "com.android.phone",
    "com.android.systemui",
    "com.example.critical.app"
};

private boolean isWhitelistedPackage(String pkg) {
    for (String whitelistPkg : WHITELIST_PACKAGES) {
        if (whitelistPkg.equals(pkg)) {
            return true;
        }
    }
    return false;
}

// 在频率检查时跳过白名单应用:
private void checkRateLimiting(String pkg, int uid, NotificationRecord r) {
    if (isWhitelistedPackage(pkg)) {
        return; // 白名单应用不受限流限制
    }
    
    // 原有限流逻辑
    // ...
}
9.3.2 添加通知统计和监控
java 复制代码
// 场景: 记录和分析通知使用情况

public class NotificationStatsManager {
    private final ArrayMap<String, NotificationStats> mStatsMap = new ArrayMap<>();
    
    public static class NotificationStats {
        public int totalCount;
        public int canceledCount;
        public int clickedCount;
        public long totalDuration;
        public long lastPostTime;
    }
    
    public void recordNotificationPosted(String pkg) {
        NotificationStats stats = getOrCreateStats(pkg);
        stats.totalCount++;
        stats.lastPostTime = System.currentTimeMillis();
    }
    
    public void recordNotificationCanceled(String pkg) {
        NotificationStats stats = getOrCreateStats(pkg);
        stats.canceledCount++;
    }
    
    public void recordNotificationClicked(String pkg) {
        NotificationStats stats = getOrCreateStats(pkg);
        stats.clickedCount++;
    }
    
    private NotificationStats getOrCreateStats(String pkg) {
        NotificationStats stats = mStatsMap.get(pkg);
        if (stats == null) {
            stats = new NotificationStats();
            mStatsMap.put(pkg, stats);
        }
        return stats;
    }
    
    public void dumpStats(PrintWriter pw) {
        pw.println("Notification Statistics:");
        for (int i = 0; i < mStatsMap.size(); i++) {
            String pkg = mStatsMap.keyAt(i);
            NotificationStats stats = mStatsMap.valueAt(i);
            pw.println(String.format("  %s: posted=%d, canceled=%d, clicked=%d",
                    pkg, stats.totalCount, stats.canceledCount, stats.clickedCount));
        }
    }
}
9.3.3 实现智能通知管理
java 复制代码
// 场景: 根据用户行为自动调整通知优先级

public class IntelligentNotificationManager {
    private final ArrayMap<String, UserBehaviorStats> mBehaviorStats = new ArrayMap<>();
    
    public static class UserBehaviorStats {
        public int shown;           // 显示次数
        public int dismissed;       // 划掉次数
        public int clicked;         // 点击次数
        public float engagementRate; // 参与率 = clicked / shown
    }
    
    public void adjustImportanceBasedOnBehavior(NotificationRecord r) {
        String key = getStatKey(r);
        UserBehaviorStats stats = mBehaviorStats.get(key);
        
        if (stats == null || stats.shown < 10) {
            return; // 数据不足,不调整
        }
        
        // 计算参与率
        stats.engagementRate = (float) stats.clicked / stats.shown;
        
        // 根据参与率调整重要性
        if (stats.engagementRate > 0.5) {
            // 高参与率,提升重要性
            if (r.getImportance() < IMPORTANCE_HIGH) {
                r.setSystemImportance(IMPORTANCE_HIGH);
                Slog.d(TAG, "Boosted importance for " + key);
            }
        } else if (stats.engagementRate < 0.1) {
            // 低参与率,降低重要性
            if (r.getImportance() > IMPORTANCE_LOW) {
                r.setSystemImportance(IMPORTANCE_LOW);
                Slog.d(TAG, "Reduced importance for " + key);
            }
        }
    }
    
    private String getStatKey(NotificationRecord r) {
        return r.getSbn().getPackageName() + ":" + r.getChannel().getId();
    }
    
    public void recordShown(NotificationRecord r) {
        String key = getStatKey(r);
        UserBehaviorStats stats = getOrCreateStats(key);
        stats.shown++;
    }
    
    public void recordDismissed(NotificationRecord r) {
        String key = getStatKey(r);
        UserBehaviorStats stats = getOrCreateStats(key);
        stats.dismissed++;
    }
    
    public void recordClicked(NotificationRecord r) {
        String key = getStatKey(r);
        UserBehaviorStats stats = getOrCreateStats(key);
        stats.clicked++;
    }
    
    private UserBehaviorStats getOrCreateStats(String key) {
        UserBehaviorStats stats = mBehaviorStats.get(key);
        if (stats == null) {
            stats = new UserBehaviorStats();
            mBehaviorStats.put(key, stats);
        }
        return stats;
    }
}

9.4 编译和测试

9.4.1 编译修改后的 Framework
bash 复制代码
# 1. 设置编译环境
cd /path/to/aosp
source build/envsetup.sh
lunch aosp_<device>-userdebug

# 2. 编译 framework
make framework

# 3. 编译 SystemUI
make SystemUI

# 4. 编译服务
make services

# 5. 打包并刷入设备
make -j$(nproc)
adb reboot bootloader
fastboot flashall -w
9.4.2 测试和验证
bash 复制代码
# 查看 NotificationManagerService 日志
adb logcat -s NotificationService:V

# 查看 SystemUI 日志
adb logcat -s SystemUI:V NotifCollection:V

# Dump 通知状态
adb shell dumpsys notification

# Dump 特定包的通知
adb shell dumpsys notification --package com.example.app

# 测试发送通知
adb shell cmd notification post -S bigtext -t "Test Title" "Tag1" "Test notification body"

# 取消通知
adb shell cmd notification cancel "Tag1"
9.4.3 调试技巧
java 复制代码
// 1. 添加详细日志
private void debugNotification(NotificationRecord r) {
    Slog.d(TAG, "=== Notification Debug ===");
    Slog.d(TAG, "Key: " + r.getKey());
    Slog.d(TAG, "Package: " + r.getSbn().getPackageName());
    Slog.d(TAG, "Importance: " + r.getImportance());
    Slog.d(TAG, "Channel: " + r.getChannel().getId());
    Slog.d(TAG, "Flags: " + Integer.toHexString(r.getNotification().flags));
    Slog.d(TAG, "Group: " + r.getGroupKey());
    Slog.d(TAG, "=======================");
}

// 2. 添加性能追踪
private void traceNotificationPost(String key) {
    Trace.beginSection("postNotification:" + key);
    try {
        // 执行发送逻辑
        postNotificationInternal(key);
    } finally {
        Trace.endSection();
    }
}

// 3. 添加错误处理
private void safeExecute(Runnable runnable, String description) {
    try {
        runnable.run();
    } catch (Exception e) {
        Slog.e(TAG, "Error executing " + description, e);
        // 记录错误到分析系统
        recordError(description, e);
    }
}

10. 总结

10.1 关键要点回顾

  1. 架构层次: Android 通知系统分为应用层 (NotificationManager)、Framework 层 (NotificationManagerService) 和 SystemUI 层
  2. 核心流程: 通知从应用发送到 NMS,经过验证、排序、分组后,通过 NotificationListenerService 传递给 SystemUI 显示
  3. 通知限制: 包括频率限制 (5/s)、数量限制 (50/app)、权限检查、DND 拦截等多层防护
  4. Toast 系统: 独立的临时通知系统,使用全局队列管理,有严格的频率限制
  5. 高级特性: 分组、排序、HUN、Channel 等提供丰富的通知管理能力

10.2 架构优势

  • 解耦设计: 通过 Binder IPC 和 NotificationListenerService 实现了应用层、服务层和 UI 层的解耦
  • 可扩展性: SignalExtractor 机制允许灵活添加新的通知处理逻辑
  • 安全性: 多层权限检查和配额管理防止通知滥用
  • 性能优化: 使用独立的 Worker 线程和 Ranking 线程处理通知,避免阻塞主线程

10.3 未来演进方向

根据 Android 15 的代码分析,可以看到以下演进趋势:

  1. AI 增强: NotificationAssistantService 提供更智能的通知分类和优先级调整
  2. 隐私保护: 增强锁屏通知的隐私控制 (redactSensitiveNotifications)
  3. 对话优化: MessagingStyle 和 ConversationChannel 进一步优化消息类通知
  4. 气泡通知: BubbleMetadata 提供类似聊天头像的浮动通知
  5. 生命周期管理: 更精细的通知生命周期控制 (lifetime extension refactor)

10.4 参考资料

AOSP 源码路径:

  • NotificationManagerService: frameworks/base/services/core/java/com/android/server/notification/
  • Notification API: frameworks/base/core/java/android/app/Notification*.java
  • SystemUI: frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/

官方文档:


致敬前辈,砥砺前行!

相关推荐
特立独行的猫a3 小时前
腾讯Kuikly多端框架(KMP)实战:轮播图的完整实现
android·harmonyos·轮播图·jetpack compose·kuikly
2501_915921433 小时前
iOS 抓包怎么绕过 SSL Pinning 证书限制,抓取app上的包
android·网络协议·ios·小程序·uni-app·iphone·ssl
陈健平3 小时前
用 Kimi 2.5 Agent 从 0 搭建「宇宙吞噬,ps:和球球大作战这种差不多」对抗小游戏(Canvas 粒子特效 + AI Bot + 排行榜)
android·人工智能·agent·kimi2.5
hewence13 小时前
重构千行Callback:Android异步回调无损迁移协程Suspend完全指南
android·重构·kotlin
我命由我123453 小时前
Android多进程开发 - AIDL 参数方向、AIDL 传递自定义对象、AIDL 传递自定义对象(参数方向)
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
龚礼鹏3 小时前
android 13 Launcher中应用列表数量,大小如何控制的?
android
决胜万里3 小时前
zephyr上实现Android Fence机制
android·嵌入式
程序员陆业聪12 小时前
2025 年客户端技术盘点与 2026 年技术展望
android
xhBruce13 小时前
Android USB 存储 冷启动(开机自动插着 U 盘)场景
android·usb·vold