Android Foreground Service和Service封装

1,970 阅读3分钟

一、Foreground Service

从android O版本开始,google为了控制资源使用,对service的后台运行做了限制。service启动后,应用退到后台,如果没有任务执行,静默一段时间(几分钟)后service就会停止。Android 8.0引入了一个新方法Context.startForegroundService()来直接启动一个前台Service,但是当系统创建这个前台Service后,应用需要在5秒内调用Service.startForeground()来显示一个前台通知,否则系统会停止这个前台Service,并弹出ANR。 从Android 9.0开始(Android P, API 28),如果要创建前台Service,还要在AndroidManifest.xml中声明android.permission.FOREGROUND_SERVICE权限,这是一个普通的权限,系统会自动授予app。如果不这样做,会抛出异常。

二、封装的要点和思路

思路:

1、判断版本如果大于Android.O就开启前台服务,否则只是普通的混合开启service
2、Service负责实现具体代码逻辑,Service中的方法由管理类统一调用。
3、前台通知可点击跳转到具体的Activity,也可以直接移除不显示。

注意的点:

1、提前创建好Notification,如果Notifacationnull不能开启前台服务。
2、开启前台服务时将Notification对象通过Intent传递到ServiceonStartCommand方法中。
3、在ServiceonStartCommand方法中开启前台通知startForeground
4、前台通知是不能通过点击或者设置setAutoCancel(true)让它消失的,它会一直存在,除非调用stopForeground(true),这也是开启前台服务后直接移除通知的办法。

三、封装

模拟文件上传下载的存储管理服务

1、设置权限

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

2、Service对外暴露方法调用的管理类

/**
* StoreControlService的客户端管理类,可以调用Service所有的方法。不能直接使用Service对象。
*/
class StoreClient {
  private var controlService: StoreControlService? = null   //service对象,用于调用service的方法
  private val serviceConnection = object: ServiceConnection {
      override fun onServiceConnected(name: ComponentName, binder: IBinder) {
          if (StoreServiceBinder::class.java.isAssignableFrom(binder.javaClass)) {
              //获取Service对象
              controlService = (binder as StoreServiceBinder).getService()?.get
              serviceBound = true
          }
      }

      override fun onServiceDisconnected(name: ComponentName) {
      }
  }
  private lateinit var mServiceStartIntent: Intent

  // notification for Foreground Service
  private var foregroundServiceNotification: Notification? = null  //前台服务的通知
  private var foregroundServiceNotificationId = -1   //前台服务的通知的Id

  @Volatile
  private var serviceBound = false   //service是否绑定

  /**
   * 单例模式
   */
  companion object {
      private val SERVICE_NAME = StoreControlService::class.java.name
      private var instance: StoreClient? = null

      @Synchronized
      fun getInstance(): StoreClient {    //单例
          if (instance == null) {
              instance = StoreClient()
          }
          return instance!!
      }
  }

  /**
   * 如果是>= Build.VERSION_CODES.O ,则需要设置前台服务
   * 通知Notification的内容需要自定义
   */
  fun setForegroundService(notification: Notification, id: Int) {
      foregroundServiceNotification = notification
      foregroundServiceNotificationId = id
  }

  /**
   * 绑定服务
   */
  fun bindService(context: Context) {
      mServiceStartIntent = Intent().apply {
          setClassName(context, SERVICE_NAME)
      }
      var service: Any? = null
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && foregroundServiceNotification != null) {
          Log.d("TAG", "startForegroundService")
          //将Notification放入Intent,在Service的onStartCommand方法中获取
          mServiceStartIntent.putExtra(
              StoreControlService.FOREGROUND_SERVICE_NOTIFICATION,
              foregroundServiceNotification
          )
          //将NotificationId放入Intent,在Service的onStartCommand方法中获取
          mServiceStartIntent.putExtra(
              StoreControlService.FOREGROUND_SERVICE_NOTIFICATION_ID,
              foregroundServiceNotificationId
          )
          //开启前台服务并传递Intent
          service = context.startForegroundService(mServiceStartIntent)
      } else {
          Log.d("TAG", "startService")
          try {
              //开启普通服务并传递Intent
              service = context.startService(mServiceStartIntent)
          } catch (ex: IllegalStateException) {

          }
      }
      if (service == null) {
          throw RuntimeException("cannot start service $SERVICE_NAME")
      }
      Log.d("TAG", "bindService")
      //绑定服务,采用混合开启的方式
      context.bindService(mServiceStartIntent, serviceConnection, Context.BIND_AUTO_CREATE)
  }


  /**
   * 解绑服务
   */
  fun unbindService(context: Context) {
      if (serviceBound) {  //如果有绑定
          try {
              context.unbindService(serviceConnection)   //解绑
              context.stopService(mServiceStartIntent)   //停止服务
              serviceBound = false
          } catch (e: IllegalArgumentException) {
          }
      }
  }

  /**
   * 上传文件的方法供外部调用,具体实现逻辑在Service中
   */
  fun uploadFile() {
      controlService?.uploadFile()
  }

  /**
   * 下载文件的方法供外部调用,具体实现逻辑在Service中
   */
  fun downloadFile() {
      controlService?.downloadFile()
  }

}

2、Service的代码

class StoreControlService : LifecycleService() {

    private var storeServiceBinder: StoreServiceBinder? = null

    companion object {
        val FOREGROUND_SERVICE_NOTIFICATION_ID =
            StoreControlService::class.java.simpleName + ".FOREGROUND_SERVICE_NOTIFICATION_ID"
        val FOREGROUND_SERVICE_NOTIFICATION =
            StoreControlService::class.java.simpleName + ".FOREGROUND_SERVICE_NOTIFICATION"
    }

    override fun onCreate() {
        super.onCreate()
        storeServiceBinder = StoreServiceBinder(this)
    }

    override fun onBind(intent: Intent): IBinder? {
        super.onBind(intent)
        storeServiceBinder?.binderToken =
            intent.getStringExtra(StoreConstants.CALLBACK_ACTIVITY_TOKEN)
        return storeServiceBinder
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        //通过Intent获取创建好的Notification
            val foregroundServiceNotification =
                intent?.getParcelableExtra<Notification>(FOREGROUND_SERVICE_NOTIFICATION)
            if (foregroundServiceNotification != null) {
            //开启前台通知
                startForeground(
                    intent.getIntExtra(FOREGROUND_SERVICE_NOTIFICATION_ID, 1),
                    foregroundServiceNotification
                )
                //直接移除前台通知,也可以给Notification设置PendingIntent点击实现跳转
                stopForeground(true)
            }
        }
        return START_STICKY
    }

    /**
     * 上传文件
     */
    fun uploadFile() {
        //具体实现逻辑代码略...
    }

    /**
     * 下载文件
     */
    fun downloadFile() {
        //具体实现逻辑代码略...
    }

    override fun onDestroy() {
        if (storeServiceBinder != null) {
            storeServiceBinder = null
        }
        super.onDestroy()
    }

}

3、通知

//通知工具类
internal object Notify {

    private var MessageID = 100
    private const val channelId = "chn-01"
    private const val channelFireBaseMsg = "Chn Store"

    private val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
    } else {
        PendingIntent.FLAG_UPDATE_CURRENT
    }

    //发通知
    fun notification(
        context: Context,
        messageString: String,
        intent: Intent?,
        notificationTitle: Int
    ) {

        //Get the notification manage which we will use to display the notification
        val ns = Context.NOTIFICATION_SERVICE
        val notificationManager = context.getSystemService(ns) as NotificationManager

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(
                channelId,
                channelFireBaseMsg,
                NotificationManager.IMPORTANCE_LOW
            )
            notificationChannel.enableLights(true)
            notificationChannel.lightColor = Color.RED
            notificationChannel.enableVibration(true)
            notificationChannel.vibrationPattern =
                longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)

            notificationManager.createNotificationChannel(notificationChannel)
        }

        val `when` = System.currentTimeMillis()

        //get the notification title from the application's strings.xml file
        val contentTitle: CharSequence = context.getString(notificationTitle)

        //the message that will be displayed as the ticker
        val ticker = "$contentTitle $messageString"

        //build the pending intent that will start the appropriate activity
        val pendingIntent = PendingIntent.getActivity(context, 0, intent, pendingIntentFlags)

        //build the notification
        val notificationCompat = NotificationCompat.Builder(context, channelId)
        notificationCompat.setAutoCancel(true)
            .setContentTitle(contentTitle)
            .setContentIntent(pendingIntent)
            .setContentText(messageString)
            .setTicker(ticker)
            .setWhen(`when`)
            .setSmallIcon(R.mipmap.app_icon_round)
        val notification = notificationCompat.build()

        notificationManager.notify(MessageID, notification)
        MessageID++
    }

    //创建前台通知对象
    fun foregroundNotification(
        context: Context,
        connectionName: String,
        intent: Intent?,
        notificationTitle: Int
    ): Notification {
        //Get the notification manage which we will use to display the notification
        val ns = Context.NOTIFICATION_SERVICE
        val notificationManager = context.getSystemService(ns) as NotificationManager

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(
                channelId,
                channelFireBaseMsg,
                NotificationManager.IMPORTANCE_LOW
            )
            notificationChannel.enableLights(true)
            notificationChannel.lightColor = Color.RED
            notificationChannel.enableVibration(true)
            notificationChannel.vibrationPattern =
                longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)

            notificationManager.createNotificationChannel(notificationChannel)
        }

        val `when` = System.currentTimeMillis()

        //get the notification title from the application's strings.xml file
        val contentTitle: CharSequence = context.getString(notificationTitle)

        //the message that will be displayed as the ticker
        val ticker = "$contentTitle $connectionName"

        //build the pending intent that will start the appropriate activity
        val pendingIntent =
            PendingIntent.getActivity(context, 0, intent ?: Intent(), pendingIntentFlags)

        //build the notification
        val notificationCompat = NotificationCompat.Builder(context, channelId)
        notificationCompat
            .setAutoCancel(true)
            .setContentTitle(contentTitle)
            .setContentIntent(pendingIntent)
            .setContentText(connectionName)
            .setTicker(ticker)
            .setWhen(`when`)
            .setSmallIcon(R.mipmap.app_icon_round)
        return notificationCompat.build()
    }
}

4、其他

internal interface StoreConstants {
    companion object {
        const val CALLBACK_ACTIVITY_TOKEN = "activityToken"
    }
}
//弱引用,不然会有内存泄漏
class StoreServiceBinder() : Binder() {
    var binderToken: String? = null
    private var weakReferenceService: WeakReference<StoreControlService>? = null

    constructor(service: StoreControlService) : this() {
        weakReferenceService = WeakReference(service)
    }

    fun getService(): WeakReference<StoreControlService>? = weakReferenceService
}

完整的封装就在上面了

5、使用

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        //如果大于8.0需要设计好Notification的显示内容
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
        //这是Notification点击跳转使用的PendingIntent,如果想直接移除前台通知,下面的pendingIntent参数可给null
            val pendingIntent = Intent(this, MainActivity::class.java).apply {   
                flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
            }
            //创建一个前台通知的Notification对象
            val foregroundNotification = Notify.foregroundNotification(
                this,
                "我是通知的内容",
                pendingIntent,
                R.string.notification_title  //通知的标题
            )
            //先设置好Notification和NotificationId
            StoreClient.getInstance().setForegroundService(foregroundNotification, 100)
        }
        //再开启服务
        StoreClient.getInstance().bindService(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        //解绑服务
        StoreClient.getInstance().unbindService(this)
    }
}

参考了以下博客,表示感谢:

Android O对后台Service限制

个人学习笔记