Sürekli çalışan Android hizmeti geliştirmek
Bu günlerde Android’de sonsuz bir hizmeti çalıştırmanın bir yolunu bulmaya çalışıyorum . Bu, aynı amacı güden hepiniz için bir rehberdir. Umarım yardımcı olur!
Sorun
Android 8.0’da (API seviyesi 26) tanıtılan Android pil optimizasyonları nedeniyle, arka plan hizmetlerinin artık bazı önemli sınırlamaları vardır . Esasen, uygulama bir süre arka planda kaldığında öldürülürler ve sürekli çalışan bir hizmeti yürütme amacımız için onları değersiz hale getirir.
Android önerilerine göre, oldukça iyi çalışıyor gibi görünen ve bizim için uyandırma kilitlerini kaldıracak , işler devam ederken telefonu uyanık tutacak olan JobScheduler’ı kullanmalıyız .
Maalesef bu da işe yaramayacak. JobScheduler , Android’in takdirine bağlı olarak işleri çalıştıracak ve bunun da ötesinde , telefon Doze Moduna girdiğinde, çalıştırılan bu işlerin sıklığı sürekli olarak azalacaktır. Ve daha da kötüsü, ağa erişmek istersen, yani sunucunuza veri göndermeniz gerektiğini söylerseniz, bunu yapamazsınız. Doze Modunun uyguladığı kısıtlamalar listesine göz atın .
JobScheduler , ağa erişiminizin olmaması ve periyodikliği kontrol etmemeyi umursamıyorsanız iyi çalışır. Bizim durumumuzda, hizmetimizin çok belirli bir frekansta çalışmasını ve asla durdurulmamasını istiyoruz, bu yüzden başka bir şeye ihtiyacımız olacak.
Foreground Hizmetleri
Bu soruna bir çözüm bulmak için internete bakıyorsanız, büyük olasılıkla sonunda bu sayfaya Android belgelerinden gelmişsinizdir .
Orada, Android’in sağladığı farklı hizmet türleriyle tanışıyoruz. Foreground Service
açıklamaya bir göz atın :
Bir ön plan hizmeti, kullanıcının fark edebileceği bazı işlemleri gerçekleştirir. Örneğin, bir ses uygulaması, bir ses parçasını çalmak için bir ön plan hizmeti kullanır. Ön plan hizmetleri bir Bildirim görüntülemelidir. Ön plan hizmetleri, kullanıcı uygulamayla etkileşimde bulunmadığında bile çalışmaya devam eder.
Görünüşe göre tam da aradığımız şey… ve gerçekten de öyle!
Kodları görelim 🙂
foreground service
oluşturmak gerçekten basit bir süreçtir, bu yüzden asla durmayan bir ön plan hizmeti oluşturmak için gereken tüm adımları ziyaret edip açıklayacağım.
Her zamanki gibi, ona bir göz atmak ve yazının geri kalanını atlamak istemeniz durumunda , tüm kodun bulunduğu bir depo oluşturdum .
Bazı bağımlılıklar eklemek
Bu örnek için Kotlin kullanıyorum , bu nedenle HTTP istekleri için eş yordamlardan ve Yakıt kitaplığından yararlanacağız .
Bu bağımlılıkları eklemek için bunları build.gradle
dosyamıza eklemeliyiz :
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.jaredrummler:android-device-names:1.1.8'
implementation 'com.github.kittinunf.fuel:fuel:2.1.0'
implementation 'com.github.kittinunf.fuel:fuel-android:2.1.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
Hizmetimizi Yazmak
Foreground Services
kullanıcının uygulamanın hala çalıştığından haberdar olması için gösterilmesi gereken bir bildirim gerekir. Düşünürseniz mantıklı.
Hizmet yaşam döngüsünün temel yönlerini ele alan bazı Hizmet geri arama yöntemlerini geçersiz kılmamız gerekeceğini unutmayın .
Ayrıca kısmi uyandırma kilidi kullanmamız da çok önemlidir, böylece hizmetimiz Doze Modundan asla etkilenmez . Bunun telefonumuzun pil ömrünü etkileyeceğini unutmayın; bu nedenle, arka planda işlemleri çalıştırmak için kullanım durumumuzun Android’in sunduğu diğer alternatiflerden herhangi biri tarafından ele alınıp alınamayacağını değerlendirmemiz gerekir.
Kodda bazı yardımcı program işlev çağrıları ( log
, setServiceState
) ve bazı özel numaralandırmalar ( ServiceState.STARTED
) vardır, ancak çok fazla endişelenmeyin. Nereden geldiklerini görmek istiyorsanız, örnek depoya bir göz atın .
class EndlessService : Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false
override fun onBind(intent: Intent): IBinder? {
log("Some component want to bind with the service")
// We don't provide binding, so return null
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
log("onStartCommand executed with startId: $startId")
if (intent != null) {
val action = intent.action
log("using an intent with action $action")
when (action) {
Actions.START.name -> startService()
Actions.STOP.name -> stopService()
else -> log("This should never happen. No action in the received intent")
}
} else {
log(
"with a null intent. It has been probably restarted by the system."
)
}
// by returning this we make sure the service is restarted if the system kills the service
return START_STICKY
}
override fun onCreate() {
super.onCreate()
log("The service has been created".toUpperCase())
var notification = createNotification()
startForeground(1, notification)
}
override fun onDestroy() {
super.onDestroy()
log("The service has been destroyed".toUpperCase())
Toast.makeText(this, "Service destroyed", Toast.LENGTH_SHORT).show()
}
private fun startService() {
if (isServiceStarted) return
log("Starting the foreground service task")
Toast.makeText(this, "Service starting its task", Toast.LENGTH_SHORT).show()
isServiceStarted = true
setServiceState(this, ServiceState.STARTED)
// we need this lock so our service gets not affected by Doze Mode
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
acquire()
}
}
// we're starting a loop in a coroutine
GlobalScope.launch(Dispatchers.IO) {
while (isServiceStarted) {
launch(Dispatchers.IO) {
pingFakeServer()
}
delay(1 * 60 * 1000)
}
log("End of the loop for the service")
}
}
private fun stopService() {
log("Stopping the foreground service")
Toast.makeText(this, "Service stopping", Toast.LENGTH_SHORT).show()
try {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
stopForeground(true)
stopSelf()
} catch (e: Exception) {
log("Service stopped without being started: ${e.message}")
}
isServiceStarted = false
setServiceState(this, ServiceState.STOPPED)
}
private fun pingFakeServer() {
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.mmmZ")
val gmtTime = df.format(Date())
val deviceId = Settings.Secure.getString(applicationContext.contentResolver, Settings.Secure.ANDROID_ID)
val json =
"""
{
"deviceId": "$deviceId",
"createdAt": "$gmtTime"
}
"""
try {
Fuel.post("https://jsonplaceholder.typicode.com/posts")
.jsonBody(json)
.response { _, _, result ->
val (bytes, error) = result
if (bytes != null) {
log("[response bytes] ${String(bytes)}")
} else {
log("[response error] ${error?.message}")
}
}
} catch (e: Exception) {
log("Error making the request: ${e.message}")
}
}
private fun createNotification(): Notification {
val notificationChannelId = "ENDLESS SERVICE CHANNEL"
// depending on the Android API that we're dealing with we will have
// to use a specific method to create the notification
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
val channel = NotificationChannel(
notificationChannelId,
"Endless Service notifications channel",
NotificationManager.IMPORTANCE_HIGH
).let {
it.description = "Endless Service channel"
it.enableLights(true)
it.lightColor = Color.RED
it.enableVibration(true)
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
it
}
notificationManager.createNotificationChannel(channel)
}
val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, 0)
}
val builder: Notification.Builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.Builder(
this,
notificationChannelId
) else Notification.Builder(this)
return builder
.setContentTitle("Endless Service")
.setContentText("This is your favorite endless service working")
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setTicker("Ticker text")
.setPriority(Notification.PRIORITY_HIGH) // for under android 26 compatibility
.build()
}
}
Android Manifest ile başa çıkma zamanı
Biz için fazladan izinleri gerekir FOREGROUND_SERVICE
, INTERNET
ve WAKE_LOCK
. Bunları eklemeyi unutmadığınızdan emin olun çünkü aksi halde işe yaramayacaktır.
Bunları yerine koyduktan sonra hizmetimizi beyan etmemiz gerekecek.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.robertohuertas.endless">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service
android:name=".EndlessService"
android:enabled="true"
android:exported="false">
</service>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Hizmete nasıl başladığını bana söyleyebilir misin?
Evet haklısın. Gördüğünüz gibi, Android sürümüne bağlı olarak hizmeti belirli bir yöntemle başlatmalıyız.
Android sürümü API 26’nın altındaysa startService kullanmalıyız . Başka bir durumda, bunun yerine startForegroundService kullanmamız gereken şeydir.
Burada , servisi başlatmak ve durdurmak için MainActivity
iki düğmeli bir ekranımızı görebilirsiniz . Sonsuz hizmetimize başlamak için ihtiyacınız olan tek şey bu .
Bu GitHub deposundaki kodun tamamını kontrol edebileceğinizi unutmayın .
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
title = "Endless Service"
findViewById<Button>(R.id.btnStartService).let {
it.setOnClickListener {
log("START THE FOREGROUND SERVICE ON DEMAND")
actionOnService(Actions.START)
}
}
findViewById<Button>(R.id.btnStopService).let {
it.setOnClickListener {
log("STOP THE FOREGROUND SERVICE ON DEMAND")
actionOnService(Actions.STOP)
}
}
}
private fun actionOnService(action: Actions) {
if (getServiceState(this) == ServiceState.STOPPED && action == Actions.STOP) return
Intent(this, EndlessService::class.java).also {
it.action = action.name
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
log("Starting the service in >=26 Mode")
startForegroundService(it)
return
}
log("Starting the service in < 26 Mode")
startService(it)
}
}
}
Bonus: Hizmeti Android açılışında başlatın
Tamam, artık her dakika istediğimiz gibi ağ istekleri yapan sonsuz hizmetimiz var ama sonra kullanıcı telefonu yeniden başlatıyor… ve hizmetimiz tekrar başlamıyor…
Merak etmeyin, buna da bir çözüm bulabiliriz. Biz yaratacak BroadCastReceiver denir StartReceiver
.
class StartReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED && getServiceState(context) == ServiceState.STARTED) {
Intent(context, EndlessService::class.java).also {
it.action = Actions.START.name
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
log("Starting the service in >=26 Mode from a BroadcastReceiver")
context.startForegroundService(it)
return
}
log("Starting the service in < 26 Mode from a BroadcastReceiver")
context.startService(it)
}
}
}
}
Ardından, yeniden değiştireceğiz Android Manifest
ve yeni bir izin ( RECEIVE_BOOT_COMPLETED
) ve yeni BroadCastReceiver’ımızı ekleyeceğiz .
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.robertohuertas.endless">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service
android:name=".EndlessService"
android:enabled="true"
android:exported="false">
</service>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<receiver android:enabled="true" android:name=".StartReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</application>
</manifest>
Zaten çalışmadıkça hizmetin yeniden başlatılmayacağını unutmayın. Biz onu böyle programladık, öyle olması gerekmiyor.
Bunu test etmek istiyorsanız Neyse, sadece bir kez emülatörü yukarı dönmeye Google Services
içinde ve çalışıyor olması mutlaka adb içinde kök modu.
adb root
# If you get an error then you're not running the proper emulator.
# Be sure to stop the service
# and force a system restart:
adb shell stop
adb shell start
# wait for the service to be restarted!
Bonus 2: Uygulama kapatıldığında(zorla kapatma) hizmeti yeniden başlatın
Michal Materowski bana bu dava ve çözümünü yazdı, bu yüzden onu tebrik ediyoruz !
Teorik olarak, Android
belgelere göre , hizmetin yönteminden RETURN_STICKY döndürmek onStartCommand
, Android’in ön plan hizmetini çalışır durumda tutması için yeterli olmalıdır.
Michal, tüm bunları Android Pie’li bir Xiaomi Note 5 ile test ediyordu ve son uygulamalardan bir uygulamayı her kaydırdığında kusursuz bir şekilde çalıştı. Ne yazık ki, düğmeye ( MIUI’ye özgü) her bastığında, hizmet durduruldu ve bildirim gitti. Düğme muhtemelen tüm işlemler ve bunların ilişkili hizmetlerini öldürerek pil ömrü optimizasyon çeşit yapıyordu.
Android
belgelerinde onTaskRemoved
“hizmet şu anda çalışıyorsa ve kullanıcı hizmetin uygulamasından gelen bir görevi kaldırdıysa çağrılır” ifadesi yer almıştır. Bu yüzden plan, hizmeti yeniden başlatmak için bundan yararlanmaktı. Ancak, onTaskRemoved
uygulama başka bir şekilde öldürülürse (örneğin telefon ayarlarından durdurulursa) bunun çağrılmayacağını unutmayın .
Bu satırları hizmetinize ekleyin :
override fun onTaskRemoved(rootIntent: Intent) {
val restartServiceIntent = Intent(applicationContext, EndlessService::class.java).also {
it.setPackage(packageName)
};
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT);
applicationContext.getSystemService(Context.ALARM_SERVICE);
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
}
Orijinal Michal Materowski’nin PR’sini kodun tamamı ile kontrol edebilirsiniz .
ÖNEMLİ : Michal’in Autostart
izni manuel olarak ayarlaması gerekiyordu , aksi takdirde hizmet önyükleme sırasında başlatılmamıştı .
Michal’e göre, bazı insanlar stopWithTask
bayrak koymanın yardımcı olabileceğinden bahsetti , ancak bu onun için bir fark yaratmadı:
<service android:name=".EndlessService"" android:stopWithTask="false" />
Şeref sürü Michal Materowski bu durumda onun yardım için.
kaynak: https://robertohuertas.com/2019/06/29/android_foreground_services/
Son yorumlar