توسعه سرویس اندرویدی که هرگز متوقف نمیشود – راهنمای کاربردی


در این مقاله در مورد شیوه ساخت یک سرویس اندروید صحبت میکنیم که به صورت نامحدود اجرا شود. اگر به این بحث علاقهمند هستید با ما تا انتهای این راهنما همراه باشید.
بیان مسئله
به دلیل معرفی بحث بهینهسازی باتری که در نسخه 8.0 اندروید (API 28) معرفی شده است، سرویسهای پسزمینه اینک با محدودیت مهمی مواجه هستند. در واقع این سرویسها زمانی که اپلیکیشن برای مدتی در حالت پسزمینه بماند قطع میشوند و به این ترتیب ارزش خود را به عنوان سرویسی که باید به طور مداوم در حال اجرا باشند از دست میدهند.
بر اساس توصیههای اندروید ما باید از JobScheduler استفاده کنیم که به نظر میرسد کارکرد خوبی دارد و wakelock-ها را برای ما مدیریت میکند و زمانی که وظیفهای در حال اجرا است گوشی را بیدار حفظ میکند.
متأسفانه این راهحل نیز کار نمیکند. JobScheduler کارها را به نیابت از اندروید اجرا میکنند و زمانی که گوشی وارد حالت Doze میشود، بسامد این کارها نیز به تدریج کاهش مییابد. موضوع بدتر این است که اگر بخواهید به شبکه دسترسی پیدا کنید، برای مثال اگر میخواهید دادههایی را به سرور ارسال کنید، قادر به انجام این کار نخواهید بود. برای مطالعه فهرست محدودیتهای حالت DOZE به این صفحه (+) مراجعه کنید.
JobScheduler در صورتی که به دسترسی نداشتن به شبکه اهمیتی ندهید و کنترل کردن توالی کارها نیز چندان مهم نباشد به خوبی کار میکند. در این مقاله ما میخواهیم سرویسی بسازیم که با بسامد کاملاً خاصی کار میکند و هرگز متوقف نمیشود و از این رو این روش برای ما کار نمیکند.
سرویسهای پیشزمینه
اگر تاکنون در اینترنت به دنبال راهحلی برای این مشکل گشته باشید، به احتمال زیاد به این صفحه (+) از مستندات اندروید رسیدهاید. در این صفحه انواع مختلفی از سرویسهایی که اندروید ارائه میکند معرفی شدهاند. توضیح «سرویسهای پیشزمینه» (Foreground Service) به صورت زیر است:
سرویس پیشزمینه نوعی عملیات را اجرا میکند که در دید کاربر قرار دارد. برای نمونه یک اپلیکیشن صوتی میتواند از سرویس پیشزمینه برای پخش یک قطعه صوتی استفاده کند. سرویسهای پیشزمینه باید یک نوتیفیکیشن نمایش دهند. سرویسهای پیشزمینه حتی زمانی که کاربر با اپلیکیشن در تعامل نیست همچنان به اجرای خود ادامه میدهند. به نظر میرسد استفاده از سرویسهای پیشزمینه چاره حل مشکل ما است.
کدنویسی
ایجاد یک پردازش foreground service کار نسبتاً سرراستی است و از این رو در ادامه همه مراحل مورد نیاز برای ساخت یک سرویس پیشزمینه را توضیح میدهیم که هرگز متوقف نخواهد شد.
در صورتی که میخواهید مستقیم به کد دسترسی داشته باشید، میتوانند به این ریپازیتوری گیتهاب (+) مراجعه کنید.
افزودن برخی وابستگیها
ما در این مثال از کاتلین استفاده کنیم و بنابراین از کتابخانههای coroutines و Fuel برای درخواستهای HTTP کمک میگیریم. برای افزودن این وابستگیها باید آنها را به فایل build.gradle اضافه کنیم:
1dependencies {
2 implementation fileTree(dir: 'libs', include: ['*.jar'])
3 implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
4 implementation 'com.android.support:appcompat-v7:28.0.0'
5 implementation 'com.android.support.constraint:constraint-layout:1.1.3'
6 implementation 'com.jaredrummler:android-device-names:1.1.8'
7
8 implementation 'com.github.kittinunf.fuel:fuel:2.1.0'
9 implementation 'com.github.kittinunf.fuel:fuel-android:2.1.0'
10 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M1'
11
12 testImplementation 'junit:junit:4.12'
13 androidTestImplementation 'com.android.support.test:runner:1.0.2'
14 androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
15}
نوشتن سرویس
سرویسهای پیشزمینه نیاز به یک نوتیفیکیشن دارند که نمایش یابد تا کاربر از این که اپلیکیشن در حال اجرا است آگاهی یابد. توجه کنید که ما برخی از متدهای callback سرویس را که جنبههای کلیدی چرخه عمر سرویس را کنترل میکنند override کردهایم.
همچنین حتماً باید توجه کنید که ما از یک partial wakelock استفاده کردهایم و از این رو سرویس ما هرگز تحت تأثیر حالت Doze قرار نمیگیرد. به خاطر بسپارید که این وضعیت بر عمر باتری گوشی ما تأثیرگذار است و از این رو باید کاملاً بررسی کنیم که آیا این کاربرد از سوی هر گونه روش جایگزین دیگر اندروید برای اجرای پردازشها در پسزمینه قابل اجرا است یا نه.
برخی فراخوانیهای تابعهای کاربردی (log ،setServiceState) و برخی enum-های سفارشی در کد وجود دارند، اما جای نگرانی زیادی نیست. اگر میخواهید بدانید این موارد از کجا ناشی میشوند کافی است به ریپازیتوری مثال (+) نگاه کنید.
1class EndlessService : Service() {
2
3 private var wakeLock: PowerManager.WakeLock? = null
4 private var isServiceStarted = false
5
6 override fun onBind(intent: Intent): IBinder? {
7 log("Some component want to bind with the service")
8 // We don't provide binding, so return null
9 return null
10 }
11
12 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
13 log("onStartCommand executed with startId: $startId")
14 if (intent != null) {
15 val action = intent.action
16 log("using an intent with action $action")
17 when (action) {
18 Actions.START.name -> startService()
19 Actions.STOP.name -> stopService()
20 else -> log("This should never happen. No action in the received intent")
21 }
22 } else {
23 log(
24 "with a null intent. It has been probably restarted by the system."
25 )
26 }
27 // by returning this we make sure the service is restarted if the system kills the service
28 return START_STICKY
29 }
30
31 override fun onCreate() {
32 super.onCreate()
33 log("The service has been created".toUpperCase())
34 var notification = createNotification()
35 startForeground(1, notification)
36 }
37
38 override fun onDestroy() {
39 super.onDestroy()
40 log("The service has been destroyed".toUpperCase())
41 Toast.makeText(this, "Service destroyed", Toast.LENGTH_SHORT).show()
42 }
43
44 private fun startService() {
45 if (isServiceStarted) return
46 log("Starting the foreground service task")
47 Toast.makeText(this, "Service starting its task", Toast.LENGTH_SHORT).show()
48 isServiceStarted = true
49 setServiceState(this, ServiceState.STARTED)
50
51 // we need this lock so our service gets not affected by Doze Mode
52 wakeLock =
53 (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
54 newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
55 acquire()
56 }
57 }
58
59 // we're starting a loop in a coroutine
60 GlobalScope.launch(Dispatchers.IO) {
61 while (isServiceStarted) {
62 launch(Dispatchers.IO) {
63 pingFakeServer()
64 }
65 delay(1 * 60 * 1000)
66 }
67 log("End of the loop for the service")
68 }
69 }
70
71 private fun stopService() {
72 log("Stopping the foreground service")
73 Toast.makeText(this, "Service stopping", Toast.LENGTH_SHORT).show()
74 try {
75 wakeLock?.let {
76 if (it.isHeld) {
77 it.release()
78 }
79 }
80 stopForeground(true)
81 stopSelf()
82 } catch (e: Exception) {
83 log("Service stopped without being started: ${e.message}")
84 }
85 isServiceStarted = false
86 setServiceState(this, ServiceState.STOPPED)
87 }
88
89 private fun pingFakeServer() {
90 val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.mmmZ")
91 val gmtTime = df.format(Date())
92
93 val deviceId = Settings.Secure.getString(applicationContext.contentResolver, Settings.Secure.ANDROID_ID)
94
95 val json =
96 """
97 {
98 "deviceId": "$deviceId",
99 "createdAt": "$gmtTime"
100 }
101 """
102 try {
103 Fuel.post("https://jsonplaceholder.typicode.com/posts")
104 .jsonBody(json)
105 .response { _, _, result ->
106 val (bytes, error) = result
107 if (bytes != null) {
108 log("[response bytes] ${String(bytes)}")
109 } else {
110 log("[response error] ${error?.message}")
111 }
112 }
113 } catch (e: Exception) {
114 log("Error making the request: ${e.message}")
115 }
116 }
117
118 private fun createNotification(): Notification {
119 val notificationChannelId = "ENDLESS SERVICE CHANNEL"
120
121 // depending on the Android API that we're dealing with we will have
122 // to use a specific method to create the notification
123 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
124 val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
125 val channel = NotificationChannel(
126 notificationChannelId,
127 "Endless Service notifications channel",
128 NotificationManager.IMPORTANCE_HIGH
129 ).let {
130 it.description = "Endless Service channel"
131 it.enableLights(true)
132 it.lightColor = Color.RED
133 it.enableVibration(true)
134 it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
135 it
136 }
137 notificationManager.createNotificationChannel(channel)
138 }
139
140 val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent ->
141 PendingIntent.getActivity(this, 0, notificationIntent, 0)
142 }
143
144 val builder: Notification.Builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.Builder(
145 this,
146 notificationChannelId
147 ) else Notification.Builder(this)
148
149 return builder
150 .setContentTitle("Endless Service")
151 .setContentText("This is your favorite endless service working")
152 .setContentIntent(pendingIntent)
153 .setSmallIcon(R.mipmap.ic_launcher)
154 .setTicker("Ticker text")
155 .setPriority(Notification.PRIORITY_HIGH) // for under android 26 compatibility
156 .build()
157 }
158}
بررسی مانیفست اندروید
ما برای FOREGROUND_SERVICE ،INTERNET و WAKE_LOCK به برخی مجوزهای بیشتر نیاز داریم. مطمئن شوید که آنها را حتماً درج کردهاید، چون در غیر این صورت کار نمیکند. زمانی که آنها را در جای خود قرار دادید، باید سرویس خود را اعلان کنیم:
1<?xml version="1.0" encoding="utf-8"?>
2<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3 package="com.robertohuertas.endless">
4
5 <uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
6 <uses-permission android:name="android.permission.INTERNET"></uses-permission>
7 <uses-permission android:name="android.permission.WAKE_LOCK" />
8
9 <application
10 android:allowBackup="true"
11 android:icon="@mipmap/ic_launcher"
12 android:label="@string/app_name"
13 android:roundIcon="@mipmap/ic_launcher_round"
14 android:supportsRtl="true"
15 android:theme="@style/AppTheme">
16
17 <service
18 android:name=".EndlessService"
19 android:enabled="true"
20 android:exported="false">
21 </service>
22
23 <activity android:name=".MainActivity">
24 <intent-filter>
25 <action android:name="android.intent.action.MAIN"/>
26 <category android:name="android.intent.category.LAUNCHER"/>
27 </intent-filter>
28 </activity>
29 </application>
30</manifest>
روش آغاز کردن سرویس
باید بسته به نسخه اندروید، سرویس خود را با متدهای مختلفی آغاز کنیم. در نسخههای اندروید زیر API 26 باید از startService استفاده کنیم و در موارد دیگر میبایست از startForegroundService استفاده شود.
در ادامه MainActivity ما را میبینید که صرفاً صفحهای با دو دکمه آغاز و توقف سرویس است. اینها تنها مواردی هستند که برای اجرای سرویس بیپایان نیاز داریم.
1class MainActivity : AppCompatActivity() {
2
3 override fun onCreate(savedInstanceState: Bundle?) {
4 super.onCreate(savedInstanceState)
5 setContentView(R.layout.activity_main)
6
7 title = "Endless Service"
8
9 findViewById<Button>(R.id.btnStartService).let {
10 it.setOnClickListener {
11 log("START THE FOREGROUND SERVICE ON DEMAND")
12 actionOnService(Actions.START)
13 }
14 }
15
16 findViewById<Button>(R.id.btnStopService).let {
17 it.setOnClickListener {
18 log("STOP THE FOREGROUND SERVICE ON DEMAND")
19 actionOnService(Actions.STOP)
20 }
21 }
22 }
23
24 private fun actionOnService(action: Actions) {
25 if (getServiceState(this) == ServiceState.STOPPED && action == Actions.STOP) return
26 Intent(this, EndlessService::class.java).also {
27 it.action = action.name
28 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
29 log("Starting the service in >=26 Mode")
30 startForegroundService(it)
31 return
32 }
33 log("Starting the service in < 26 Mode")
34 startService(it)
35 }
36 }
37}
نکته مهم: آغاز سرویس در زمان بوت شدن اندروید
ما اینک سرویس بیپایانی داریم که بنا به خواست ما هر دقیقه یک بار یک درخواست شبکه ارسال میکند اما اگر کاربر گوشی را ریاستارت کند چطور؟ در این حالت سرویس ما در زمان بوت شدن اندروید به صورت خودکار اجرا نمیشود و این یک مشکل محسوب میشود. اما جای نگرانی نیست ما راهحلی برای این مسئله نیز یافتهایم. به این منظور یک BroadCastReceiver به نام StartReceiver میسازیم.
1class StartReceiver : BroadcastReceiver() {
2
3 override fun onReceive(context: Context, intent: Intent) {
4 if (intent.action == Intent.ACTION_BOOT_COMPLETED && getServiceState(context) == ServiceState.STARTED) {
5 Intent(context, EndlessService::class.java).also {
6 it.action = Actions.START.name
7 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
8 log("Starting the service in >=26 Mode from a BroadcastReceiver")
9 context.startForegroundService(it)
10 return
11 }
12 log("Starting the service in < 26 Mode from a BroadcastReceiver")
13 context.startService(it)
14 }
15 }
16 }
17}
سپس Android Manifest را بار دیگر تغییر میدهیم و یک مجوز جدید به نام RECEIVE_BOOT_COMPLETED و کلاس BroadCastReceiver را اضافه میکنیم.
1<?xml version="1.0" encoding="utf-8"?>
2<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3 package="com.robertohuertas.endless">
4 <uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
5 <uses-permission android:name="android.permission.INTERNET"></uses-permission>
6 <uses-permission android:name="android.permission.WAKE_LOCK" />
7 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
8
9 <application
10 android:allowBackup="true"
11 android:icon="@mipmap/ic_launcher"
12 android:label="@string/app_name"
13 android:roundIcon="@mipmap/ic_launcher_round"
14 android:supportsRtl="true"
15 android:theme="@style/AppTheme">
16
17 <service
18 android:name=".EndlessService"
19 android:enabled="true"
20 android:exported="false">
21 </service>
22
23 <activity android:name=".MainActivity">
24 <intent-filter>
25 <action android:name="android.intent.action.MAIN"/>
26 <category android:name="android.intent.category.LAUNCHER"/>
27 </intent-filter>
28 </activity>
29
30 <receiver android:enabled="true" android:name=".StartReceiver">
31 <intent-filter>
32 <action android:name="android.intent.action.BOOT_COMPLETED"/>
33 </intent-filter>
34 </receiver>
35
36 </application>
37</manifest>
بدین ترتیب به پایان این راهنما میرسیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی اندروید
- گنجینه برنامه نویسی اندروید (Android)
- مجموعه آموزشهای برنامهنویسی
- ساخت اپلیکیشن لانچر اندروید — به زبان ساده
- نشت حافظه در اپلیکیشن های اندروید — از صفر تا صد
==
مقاله خوبیه مرسی از زحماتتون ولی یه سوال برام پیش اومد شما میگید که از partial wake lock استفاده کنیم ولی توی این لینک اومده که داز مود wake lock رو ایگنور میکنه
https://developer.android.com/training/monitoring-device-state/doze-standby