Sürekli çalışan Android hizmeti geliştirmek

Çalışmayı asla durdurmayan bir 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!:sırıtış:

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! : göz kırpma:

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.gradledosyamı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ı ( logsetServiceState) 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_SERVICEINTERNETve 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 MainActivityiki 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…:hayal kırıklığına uğramış:

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, Androidbelgelere 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ı .

Otomatik başlatma

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/

Bunlar da hoşunuza gidebilir...

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Bu site, istenmeyenleri azaltmak için Akismet kullanıyor. Yorum verilerinizin nasıl işlendiği hakkında daha fazla bilgi edinin.