如何在不显示通知的情况下启动 startForeground()?

110

我想创建一个服务并使它在前台运行。

大多数示例代码都带有通知,但我不想显示任何通知。这可能吗?

你能给我一些例子吗?还有其他替代方案吗?

我的应用服务正在执行播放器功能。如何让系统不杀死我的服务,除非应用程序自己终止它(例如通过按钮暂停或停止音乐)?


13
没有通知,你就无法创建一个“前台”服务。没得商量。 - Jug6ernaut
1
怎么样发送一个“假”的通知?这是一个技巧吗? - Muhammad Resna Rizki Pratama
一旦设置了前台通知,您可以使用Rubber应用程序删除“在后台运行的通知”。 - Laurent
1
@MuhammadResnaRizkiPratama,你是否考虑更改接受的答案?这个问题非常受欢迎,但接受的答案已经过时、过于强硬,并且假设开发者需要它。我建议使用这个答案,因为它可靠,不利用操作系统漏洞,并且适用于大多数Android设备。 - Sam
16个回答

106
作为 Android 平台的安全功能, 没有任何情况下可以在前台运行服务而没有通知。 这是因为前台服务消耗更多资源并受到不同的调度限制(即不会很快被杀死)而背景服务不同,用户需要知道可能影响电池寿命的原因。 因此,请勿这样做。
但是,"伪装" 通知是可能的,例如,您可以创建一个透明的通知图标。这对用户非常不诚实,您没有理由这样做,除了损害他们的电池寿命,从而制造恶意软件。

4
谢谢,这让我明白了为什么setForeground需要通知。 - Muhammad Resna Rizki Pratama
29
好的,看起来你有一个做这件事的理由,因为用户已经要求了。虽然正如你所说,这可能有些不诚实,但测试组要求我正在开发的产品具有这个功能。也许Android应该有一个选项可以隐藏一些你知道始终在运行的服务的通知。否则你的通知栏会像圣诞树一样繁华。 - Radu
4
@Radu,事实上,你不应该在前台运行太多应用程序:这会消耗你的电池寿命,因为它们被安排的方式不同(基本上,它们不会死亡)。 这对于一个修改版而言可能是可以接受的,但我不认为这个选项会很快进入原版Android。 “用户”通常不知道什么最适合他们。 - Kristopher Micinski
2
@Radu,这并不支持你的观点。通常,“获奖应用程序”是那些通过解决Android系统不一致性(如任务杀手)来“修复”漏洞的应用程序。如果你必须使用前台服务,只是因为“获奖应用程序”这样做,那么你就承认在消耗用户电池方面存在问题。 - Kristopher Micinski
2
显然,从4.3版本开始,这种方法将不再起作用。https://plus.google.com/u/0/105051985738280261832/posts/MTinJWdNL8t - erbi
显示剩余7条评论

83
自从4.3版本更新以来,使用startForeground()启动服务而不显示通知基本上是不可能的
然而,您可以使用官方API隐藏图标... 不需要透明图标: (使用NotificationCompat来支持旧版本)
NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder.setPriority(Notification.PRIORITY_MIN);

我已经接受了通知本身仍然需要存在的事实,但是对于任何仍然想要隐藏它的人,我可能已经找到了一个解决办法:

方法一 (旧答案)

更新:这个问题在Android 7.1上已经“修复”了。 https://code.google.com/p/android/issues/detail?id=213309

  1. 使用startForeground()启动一个带有通知和其他内容的虚假服务。
  2. 使用startForeground()启动你想要运行的真实服务(使用相同的通知ID)。
  3. 停止第一个(虚假)服务(你可以调用stopSelf(),并在onDestroy中调用stopForeground(true))。

大功告成!没有任何通知,你的第二个服务仍然在运行。

方法二

自Android 13+(API 33+)开始,即使是普通通知,您也需要权限才能显示,但测试显示,无论用户拒绝权限,还是根本不请求权限,前台服务仍然可以正常运行。

25
谢谢这个。我认为有些人没有意识到Android开发也适用于内部业务使用(即,它不打算出现在市场上或销售给Joe Blow)。有时人们会要求像“停止显示服务通知”之类的东西,即使对一般用户来说不合适,但看到解决方案还是很好的。 - JamieB
5
@JamieB 我完全同意...我建议Dianne Hackborn重新考虑整个后台服务的概念,或许可以增加一项权限来允许在后台运行服务而无需通知。对于我们开发者来说,这并不重要是否有通知 - 用户请求删除它!:) 顺便问一下,你也可以验证它是否适用于你吗? - Lior Iluz
1
@JamieB 用户875707建议将图标参数(图标的资源ID)设置为0,而不是通知的ID(如果这样做,如文档所述,“startForeground”将无法工作)。 - wangqi060934
9
当然,你应该假设这在平台的未来版本中将不起作用。 - hackbod
1
@JamieB 我刚刚在 Android 5.0.1 上测试了一下,对我来说仍然可行。 - Lior Iluz
显示剩余17条评论

23
这在Android 7.1版本及以上已经不再适用,而且可能会违反Google Play开发者政策。相反,建议用户屏蔽服务通知

这是我按照Lior Iluz答案实现的技术。

代码

ForegroundService.java

public class ForegroundService extends Service {

    static ForegroundService instance;

    @Override
    public void onCreate() {
        super.onCreate();

        instance = this;

        if (startService(new Intent(this, ForegroundEnablingService.class)) == null)
            throw new RuntimeException("Couldn't find " + ForegroundEnablingService.class.getSimpleName());
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        instance = null;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

}

ForegroundEnablingService.java

public class ForegroundEnablingService extends Service {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (ForegroundService.instance == null)
            throw new RuntimeException(ForegroundService.class.getSimpleName() + " not running");

        //Set both services to foreground using the same notification id, resulting in just one notification
        startForeground(ForegroundService.instance);
        startForeground(this);

        //Cancel this service's notification, resulting in zero notifications
        stopForeground(true);

        //Stop this service so we don't waste RAM.
        //Must only be called *after* doing the work or the notification won't be hidden.
        stopSelf();

        return START_NOT_STICKY;
    }

    private static final int NOTIFICATION_ID = 10;

    private static void startForeground(Service service) {
        Notification notification = new Notification.Builder(service).getNotification();
        service.startForeground(NOTIFICATION_ID, notification);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

}

AndroidManifest.xml

<service android:name=".ForegroundEnablingService" />
<service android:name=".ForegroundService" />

兼容性

已测试并可在以下设备上正常运行:

  • 官方模拟器
    • 4.0.2
    • 4.1.2
    • 4.2.2
    • 4.3.1
    • 4.4.2
    • 5.0.2
    • 5.1.1
    • 6.0
    • 7.0
  • Sony Xperia M
    • 4.1.2
    • 4.3
  • 三星 Galaxy ?
    • 4.4.2
    • 5.X
  • Genymotion
    • 5.0
    • 6.0
  • CyanogenMod
    • 5.1.1

从Android 7.1开始不再支持。


2
还没有测试过,但至少为此提供一个权限会很好!!! 支持“去谷歌”部分,我的用户抱怨需要通知才能让应用程序在后台运行,我也抱怨需要 Skype/Llama 和其他任何应用程序的通知。 - Rami Dabain
抱歉,请问这个例子如何用来监听闹铃。我正在开发一个应用程序,使用alarmManager设置闹铃,但问题是当我关闭应用程序(最近的应用->清除)时,所有的闹铃也被删除了,我正在尝试找到一种方法来防止这种情况发生。 - Ares91
@Ares91,请尝试为此单独创建一个StackOverflow问题,并确保包括尽可能多的信息,包括相关代码。我对闹钟不太了解,而这个问题只涉及服务。 - Sam
@Sam 应该先调用哪个服务,ForegroundEnablingService 还是 Foreground? - K Guru
@AndroidMan ForegroundService 前台服务 - Sam
@Sam,哪些是实际运行在后台的服务? - K Guru

16

警告: 尽管这个答案看起来有效,但实际上它会在不知不觉中阻止您的服务成为前台服务

原始答案:


只需将通知的ID设置为零即可:

// field for notification ID
private static final int NOTIF_ID = 0;

    ...
    startForeground(NOTIF_ID, mBuilder.build());
    NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    mNotificationManager.cancel(NOTIF_ID);
    ...

您可以获得的好处是,一个“服务”将能够在高优先级下运行,而不会被Android系统销毁,除非内存压力很大。
为了使其与Pre-Honeycomb和Android 4.4及更高版本配合使用,请确保使用由Support Library v7提供的“NotificationCompat.Builder”,而不是“Notification.Builder”。
编辑:
由于较新的API级别中的安全原因,此代码将不再起作用。
“NotificationId”不能设置为“0”(这将导致应用程序崩溃)。
startForeground(1, notification)

这是展示通知的完美方式(推荐使用)。
但如果你需要无论如何都要显示通知,请尝试从你的代码中删除“notificationManager.createNotificationChannel(“channel_id”)”或者 使用 notificationManager.removeNotificationChannel(channel)

1
这对我来说在4.3(18) @vlad sol上完美运行。为什么其他人还没有点赞这个答案呢? - Rob McFeely
1
我在 Android N Preview 4 中尝试了这个方法,但它没有起作用。我尝试了 Notification.Builder、Support Library v4 NotificationCompat.Builder 和 Support Library v7 NotificationCompat.Builder。通知没有出现在通知抽屉或状态栏中,但当我使用 getRunningServices()dumpsys 进行检查时,服务并未以前台模式运行。 - Sam
1
这对我不起作用。当发送id!=0时,该进程作为前台启动,但当id=0时,它不会作为前台启动。请使用“adb shell dumpsys activity services”进行验证。 - beetree
我刚刚测试了从Android 4.0.2到7.1的所有版本,但在任何一个版本中都无法正常工作。通知没有出现,但服务也没有在前台运行。 - Sam
@AnggrayudiH 的回答非常棒。这一定是被接受的答案。 - Hiren Patel
显示剩余4条评论

15
您可以使用这个(正如@Kristopher Micinski所建议的):
Notification note = new Notification( 0, null, System.currentTimeMillis() );
note.flags |= Notification.FLAG_NO_CLEAR;
startForeground( 42, note );

更新:

请注意,这在Android KitKat+版本中不再允许。并且请记住,这更或多或少违反了Android的设计原则,即将后台操作对用户可见,如@Kristopher Micinski所述。


2
请注意,使用SDK 17时似乎不再接受此操作。如果传递给通知的可绘制对象为0,则服务将无法前台运行。 - Snicolas
4
在Android 18中,通知不再是隐形的,而是会显示出来,上面写着“应用正在运行,点击获取更多信息或停止应用”。 - Emanuel Moecklin
1
我客户的一张截图:http://1gravity.com/k10/Screenshot_2013-07-25-12-35-49.jpg。我也有一台4.3设备,可以证实这个发现。 - Emanuel Moecklin
@EmanuelMoecklin 谢谢。我喜欢那个新功能,它真的很简洁。 - Snicolas
2
警告:我从安卓4.1.2(SDK 16)的Sony Ericsson LT26i(Xperia S)接收到崩溃报告:android.app.RemoteServiceException: Bad notification posted from package ...: Couldn't create icon: StatusBarIcon(pkg=... id=0x7f020052 level=0 visible=true num=0 )在SDK 17之前,我将图标ID设置为0。从SDK 18开始,我将其设置为有效的可绘制资源。也许,您需要从SDK 16获取有效的图标ID! - almisoft
显示剩余2条评论

12

您可以使用布局高度为"0dp"的自定义布局,在Android 9+上隐藏通知。

NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.custom_notif);
builder.setContent(remoteViews);
builder.setPriority(NotificationCompat.PRIORITY_LOW);
builder.setVisibility(Notification.VISIBILITY_SECRET);

自定义通知布局文件 custom_notif.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="0dp">
</LinearLayout>

测试过Pixel 1,Android 9。这个解决方案在Android 8或更早版本上无效。


1
不错的解决方法。我认为在Android 10上需要图标。使用透明占位符图像作为图标可以解决这个问题。 - shyos
这还能用吗? - tunafish24
似乎这在较新版本(Android 13)中不再适用。会显示一个非常小的通知。 - jaga

8
更新:此方法不再适用于Android 4.3及以上版本。
有一个解决方法。 尝试创建没有设置图标的通知,那么通知将不会显示。不知道为什么,但它确实有效 :)
    Notification notification = new NotificationCompat.Builder(this)
            .setContentTitle("Title")
            .setTicker("Title")
            .setContentText("App running")
            //.setSmallIcon(R.drawable.picture)
            .build();
    startForeground(101,  notification);

3
在Android N预览版4中,这种做法行不通。如果您没有指定图标,则Android会显示一个“(YourServiceName)正在运行”的通知,而不是您自己的通知。根据CommonsWare的说法,自Android 4.3以来就一直是这样的情况:https://commonsware.com/blog/2013/07/30/notifications-foreground-services-android-4p3.html - Sam
1
我今天进行了一些测试,并确认在Android 4.3及以上版本中无法正常工作。 - Sam

8

屏蔽前台服务通知

这里的大部分答案要么无效,会破坏前台服务,或者违反了Google Play政策

唯一可靠且安全地隐藏通知的方法是让用户屏蔽它。

Android 4.1-7.1

唯一的方法是屏蔽您应用程序的所有通知:

  1. Send user to app's details screen:

    Uri uri = Uri.fromParts("package", getPackageName(), null);
    Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(uri);
    startActivity(intent);
    
  2. Have user block app's notifications

注意,这也会阻止您的应用程序弹出通知。 Android 8.0-8.1: 在Android O上阻止通知是没有意义的,因为操作系统将使用“在后台运行”或“正在使用电池”的通知来替换它。
Android 9+: 使用“通知渠道”,可以阻止服务通知而不影响其他通知。请参考:Notification Channel
  1. Assign service notification to notification channel
  2. Send user to notification channel's settings

    Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
        .putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName())
        .putExtra(Settings.EXTRA_CHANNEL_ID, myNotificationChannel.getId());
    startActivity(intent);
    
  3. Have user block channel's notifications


6
更新:此方法在安卓4.3及以上版本不再适用。
我将Notification的构造函数中的icon参数设为零,然后将得到的通知传递给startForeground()。日志中没有错误,也没有通知显示出来。虽然我不知道服务是否成功转为前台状态,但有没有办法进行检查呢?
编辑:通过使用dumpsys进行检查,在我2.3版本的系统上,确实将服务转为了前台状态。还没有在其他操作系统版本中进行过检查。

9
使用命令“adb shell dumpsys activity services”来检查“isForeground”是否设置为true。 - black
1
或者只需调用空的 Notification() 构造函数。 - johsin18
这对我有用。第一次我成功让我的服务在夜间运行,所以它明确是在前台(并且dumpsys也显示了),但没有通知。 - JamieB
请注意,这在Android 4.3(API 18)中不再适用。操作系统会在通知抽屉中放置一个占位符通知。 - Sam

4

版本4.3(18)及以上版本无法隐藏服务通知,但您可以关闭图标;版本4.3(18)及以下版本可以隐藏通知。

Notification noti = new Notification();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
    noti.priority = Notification.PRIORITY_MIN;
}
startForeground(R.string.app_name, noti);

虽然这个回答可能是正确和有用的,但最好还是附上一些解释,以解释它如何帮助解决问题。如果将来出现变化(可能不相关),导致其停止工作,用户需要了解它曾经如何工作,这尤其有用。 - Kevin Brown-Silva
我认为Android 4.3实际上是API 18。此外,这种方法仅会将图标从状态栏隐藏;该图标仍然存在于通知中。 - Sam
要隐藏通知图标,您可以添加一个空图像mBuilder.setSmallIcon(R.drawable.blank)。 - vlad sol
1
我正在查看KitKat源代码,零ID方法似乎无法工作。如果ID为零,则服务记录将更新为非前台。 - Jeffrey Blattman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接