1 /*
<lambda>null2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.work.impl
18 
19 import android.app.job.JobParameters.STOP_REASON_CONSTRAINT_CONNECTIVITY
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.ContextWrapper
23 import android.content.Intent
24 import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
25 import androidx.core.app.NotificationChannelCompat
26 import androidx.core.app.NotificationCompat
27 import androidx.core.app.NotificationManagerCompat
28 import androidx.test.core.app.ApplicationProvider
29 import androidx.test.ext.junit.runners.AndroidJUnit4
30 import androidx.test.filters.MediumTest
31 import androidx.work.Configuration
32 import androidx.work.DatabaseTest
33 import androidx.work.ForegroundInfo
34 import androidx.work.OneTimeWorkRequest
35 import androidx.work.PeriodicWorkRequest
36 import androidx.work.impl.foreground.SystemForegroundDispatcher.createStartForegroundIntent
37 import androidx.work.impl.foreground.SystemForegroundDispatcher.createStopForegroundIntent
38 import androidx.work.impl.model.WorkGenerationalId
39 import androidx.work.impl.model.generationalId
40 import androidx.work.impl.testutils.TrackingWorkerFactory
41 import androidx.work.impl.utils.SerialExecutorImpl
42 import androidx.work.impl.utils.taskexecutor.TaskExecutor
43 import androidx.work.worker.LatchWorker
44 import androidx.work.worker.StopAwareWorker
45 import androidx.work.worker.StopLatchWorker
46 import java.util.concurrent.CountDownLatch
47 import java.util.concurrent.Executor
48 import java.util.concurrent.ExecutorService
49 import java.util.concurrent.Executors
50 import java.util.concurrent.TimeUnit
51 import org.junit.After
52 import org.junit.Assert.assertEquals
53 import org.junit.Assert.assertFalse
54 import org.junit.Assert.assertTrue
55 import org.junit.Before
56 import org.junit.Test
57 import org.junit.runner.RunWith
58 
59 @RunWith(AndroidJUnit4::class)
60 class ProcessorTests : DatabaseTest() {
61     val factory = TrackingWorkerFactory()
62     lateinit var processor: Processor
63     lateinit var defaultExecutor: ExecutorService
64     lateinit var backgroundExecutor: ExecutorService
65     lateinit var serialExecutor: SerialExecutorImpl
66     private val context =
67         TrackingContext(ApplicationProvider.getApplicationContext<Context>().applicationContext)
68 
69     private val foregroundInfo: ForegroundInfo
70         get() {
71             val channel =
72                 NotificationChannelCompat.Builder(
73                         "test",
74                         NotificationManagerCompat.IMPORTANCE_DEFAULT
75                     )
76                     .setName("hello")
77                     .build()
78             NotificationManagerCompat.from(context).createNotificationChannel(channel)
79             val notification =
80                 NotificationCompat.Builder(context, "test")
81                     .setOngoing(true)
82                     .setTicker("ticker")
83                     .setContentText("content text")
84                     .setSmallIcon(androidx.core.R.drawable.notification_bg)
85                     .build()
86             return ForegroundInfo(1, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
87         }
88 
89     @Before
90     fun setUp() {
91         // first worker will take over the first thread with its doWork
92         // second worker will execute on the second thread
93         defaultExecutor = Executors.newFixedThreadPool(2)
94         backgroundExecutor = Executors.newSingleThreadExecutor()
95         serialExecutor = SerialExecutorImpl(backgroundExecutor)
96         val taskExecutor =
97             object : TaskExecutor {
98                 val mainExecutor = Executor { runnable -> runnable.run() }
99 
100                 override fun getMainThreadExecutor(): Executor {
101                     return mainExecutor
102                 }
103 
104                 override fun getSerialTaskExecutor(): SerialExecutorImpl {
105                     return serialExecutor
106                 }
107             }
108         val configuration =
109             Configuration.Builder().setWorkerFactory(factory).setExecutor(defaultExecutor).build()
110         processor = Processor(context, configuration, taskExecutor, mDatabase)
111     }
112 
113     @Test
114     @MediumTest
115     fun testInterruptNotInCriticalSection() {
116         val request1 = OneTimeWorkRequest.Builder(StopLatchWorker::class.java).build()
117         val request2 = OneTimeWorkRequest.Builder(StopLatchWorker::class.java).build()
118         insertWork(request1)
119         insertWork(request2)
120         class CountDownListener(val expectedId: String) : ExecutionListener {
121             val latch = CountDownLatch(1)
122 
123             override fun onExecuted(id: WorkGenerationalId, needsReschedule: Boolean) {
124                 if (id.workSpecId == expectedId) {
125                     latch.countDown()
126                 }
127             }
128         }
129         val firstListener = CountDownListener(request1.workSpec.id)
130         processor.addExecutionListener(firstListener)
131         val startStopToken = StartStopToken(WorkGenerationalId(request1.workSpec.id, 0))
132         processor.startWork(startStopToken)
133 
134         val firstWorker = factory.awaitWorker(request1.id)
135         Executors.newSingleThreadExecutor().execute {
136             // wil result in long running onStop call, but it will block task thread
137             processor.stopWork(startStopToken, 0)
138         }
139         assertTrue((firstWorker as StopLatchWorker).awaitOnStopCall())
140 
141         val secondListener = CountDownListener(request2.workSpec.id)
142         processor.addExecutionListener(secondListener)
143         // This would have previously failed trying to acquire a lock
144         processor.startWork(StartStopToken(WorkGenerationalId(request2.workSpec.id, 0)))
145         firstWorker.countDown()
146         val secondWorker = factory.awaitWorker(request2.id)
147         assertTrue(firstListener.latch.await(3, TimeUnit.SECONDS))
148         (secondWorker as StopLatchWorker).countDown()
149         assertTrue(secondListener.latch.await(3, TimeUnit.SECONDS))
150         firstWorker.countDown()
151         assertTrue(context.intents.isEmpty())
152     }
153 
154     @Test
155     @MediumTest
156     fun testStartForegroundStopWork() {
157         val request = OneTimeWorkRequest.Builder(LatchWorker::class.java).build()
158         insertWork(request)
159         val startStopToken = StartStopToken(request.workSpec.generationalId())
160         val executionFinished = CountDownLatch(1)
161         processor.addExecutionListener { _, _ -> executionFinished.countDown() }
162         processor.startWork(startStopToken)
163 
164         processor.startForeground(startStopToken.id.workSpecId, foregroundInfo)
165         // won't actually stopWork, because stopForeground should be used
166         processor.stopWork(startStopToken, 0)
167         // follow-up startWork shouldn't fail
168         processor.startWork(StartStopToken(request.workSpec.generationalId()))
169         assertTrue(processor.isEnqueued(startStopToken.id.workSpecId))
170         val firstWorker = factory.awaitWorker(request.id)
171         (firstWorker as LatchWorker).mLatch.countDown()
172         assertTrue(executionFinished.await(3, TimeUnit.SECONDS))
173     }
174 
175     @Test
176     @MediumTest
177     fun testInterruptStopsService() {
178         val request = OneTimeWorkRequest.Builder(StopAwareWorker::class.java).build()
179         insertWork(request)
180         val id = request.workSpec.generationalId()
181         val startStopToken = StartStopToken(id)
182         val executionFinished = CountDownLatch(1)
183         processor.addExecutionListener { _, _ -> executionFinished.countDown() }
184         processor.startWork(startStopToken)
185         processor.startForeground(startStopToken.id.workSpecId, foregroundInfo)
186         val expected = createStartForegroundIntent(context, id, foregroundInfo)
187         assertTrue(context.intents[0].filterEquals(expected))
188         // won't actually stopWork, because stopForeground should be used
189         processor.stopForegroundWork(startStopToken, STOP_REASON_CONSTRAINT_CONNECTIVITY)
190         assertFalse(processor.isEnqueued(startStopToken.id.workSpecId))
191         assertTrue(executionFinished.await(3, TimeUnit.SECONDS))
192         val stopIntentExpected = createStopForegroundIntent(context)
193 
194         val intent = context.intents.getOrNull(1) ?: throw AssertionError("Stop Intent wasn't sent")
195         assertTrue(intent.filterEquals(stopIntentExpected))
196     }
197 
198     @Test
199     @MediumTest
200     fun testStartOldGenerationDoesntStopCurrentWorker() {
201         val request = OneTimeWorkRequest.Builder(LatchWorker::class.java).build()
202         insertWork(request)
203         mDatabase.workSpecDao().incrementGeneration(request.stringId)
204         val token = StartStopToken(WorkGenerationalId(request.workSpec.id, 1))
205         processor.startWork(token)
206         val firstWorker = factory.awaitWorker(request.id)
207         var called = false
208         val oldGenerationListener = ExecutionListener { id, needsReschedule ->
209             called = true
210             assertEquals(WorkGenerationalId(request.workSpec.id, 0), id)
211             assertFalse(needsReschedule)
212         }
213         processor.addExecutionListener(oldGenerationListener)
214         processor.startWork(StartStopToken(WorkGenerationalId(request.workSpec.id, 0)))
215         assertTrue(called)
216         processor.removeExecutionListener(oldGenerationListener)
217         val executionFinished = CountDownLatch(1)
218         processor.addExecutionListener { _, _ -> executionFinished.countDown() }
219         (firstWorker as LatchWorker).mLatch.countDown()
220         assertTrue(executionFinished.await(3, TimeUnit.SECONDS))
221     }
222 
223     @Test
224     @MediumTest
225     fun testStartNewGenerationDoesntStopCurrentWorker() {
226         val request =
227             PeriodicWorkRequest.Builder(LatchWorker::class.java, 10, TimeUnit.DAYS).build()
228         insertWork(request)
229         val token = StartStopToken(WorkGenerationalId(request.workSpec.id, 0))
230         processor.startWork(token)
231         val firstWorker = factory.awaitWorker(request.id)
232         var called = false
233         val oldGenerationListener = ExecutionListener { id, needsReschedule ->
234             called = true
235             assertEquals(WorkGenerationalId(request.workSpec.id, 1), id)
236             assertFalse(needsReschedule)
237         }
238         processor.addExecutionListener(oldGenerationListener)
239         mDatabase.workSpecDao().incrementGeneration(request.stringId)
240         processor.startWork(StartStopToken(WorkGenerationalId(request.workSpec.id, 1)))
241         assertTrue(called)
242         processor.removeExecutionListener(oldGenerationListener)
243         val executionFinished = CountDownLatch(1)
244         processor.addExecutionListener { _, _ -> executionFinished.countDown() }
245         (firstWorker as LatchWorker).mLatch.countDown()
246         assertTrue(executionFinished.await(3, TimeUnit.SECONDS))
247     }
248 
249     @Test
250     @MediumTest
251     fun testOldGenerationDoesntStart() {
252         val request = OneTimeWorkRequest.Builder(LatchWorker::class.java).build()
253         insertWork(request)
254         mDatabase.workSpecDao().incrementGeneration(request.stringId)
255         val oldToken = StartStopToken(WorkGenerationalId(request.workSpec.id, 0))
256         var called = false
257         val oldGenerationListener = ExecutionListener { id, needsReschedule ->
258             called = true
259             // worker shouldn't have been created
260             assertEquals(0, factory.createdWorkers.value.size)
261             assertEquals(WorkGenerationalId(request.workSpec.id, 0), id)
262             assertFalse(needsReschedule)
263         }
264         processor.addExecutionListener(oldGenerationListener)
265         assertFalse(processor.startWork(oldToken))
266         assertTrue(called)
267     }
268 
269     @After
270     fun tearDown() {
271         defaultExecutor.shutdownNow()
272         backgroundExecutor.shutdownNow()
273         assertTrue(defaultExecutor.awaitTermination(3, TimeUnit.SECONDS))
274         assertTrue(backgroundExecutor.awaitTermination(3, TimeUnit.SECONDS))
275     }
276 
277     private class TrackingContext(base: Context) : ContextWrapper(base) {
278         val intents = mutableListOf<Intent>()
279 
280         override fun startService(service: Intent): ComponentName? {
281             // don't start anything, simply track requests
282             intents.add(service)
283             // result isn't used so simply return null
284             return null
285         }
286 
287         override fun startForegroundService(service: Intent): ComponentName? {
288             // simply track it
289             return startService(service)
290         }
291     }
292 }
293