Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 05e51c1d authored by Robert Snoeberger's avatar Robert Snoeberger
Browse files

Adding executor with repeat functionality

The use case I'm looking at is for the seek bar in media controls. While
playing, the elapsed time needs to be continuously checked with a fixed
delay (ie. polling the elapsed time).

Bug: 154352658
Test: Added RepeatableExecutorTest
Change-Id: I142545e9a405c38fda6002a85fa3c47343f2fce4
parent a38de343
Loading
Loading
Loading
Loading
+30 −0
Original line number Diff line number Diff line
@@ -136,6 +136,36 @@ public abstract class ConcurrencyModule {
        return new ExecutorImpl(looper);
    }

    /**
     * Provide a Background-Thread Executor by default.
     */
    @Provides
    @Singleton
    public static RepeatableExecutor provideRepeatableExecutor(@Background DelayableExecutor exec) {
        return new RepeatableExecutorImpl(exec);
    }

    /**
     * Provide a Background-Thread Executor.
     */
    @Provides
    @Singleton
    @Background
    public static RepeatableExecutor provideBackgroundRepeatableExecutor(
            @Background DelayableExecutor exec) {
        return new RepeatableExecutorImpl(exec);
    }

    /**
     * Provide a Main-Thread Executor.
     */
    @Provides
    @Singleton
    @Main
    public static RepeatableExecutor provideMainRepeatableExecutor(@Main DelayableExecutor exec) {
        return new RepeatableExecutorImpl(exec);
    }

    /**
     * Provide an Executor specifically for running UI operations on a separate thread.
     *
+54 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.util.concurrency;

import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

/**
 * A sub-class of {@link Executor} that allows scheduling commands to execute periodically.
 */
public interface RepeatableExecutor extends Executor {

    /**
     * Execute supplied Runnable on the Executors thread after initial delay, and subsequently with
     * the given delay between the termination of one execution and the commencement of the next.
     *
     * Each invocation of the supplied Runnable will be scheduled after the previous invocation
     * completes. For example, if you schedule the Runnable with a 60 second delay, and the Runnable
     * itself takes 1 second, the effective delay will be 61 seconds between each invocation.
     *
     * See {@link java.util.concurrent.ScheduledExecutorService#scheduleRepeatedly(Runnable,
     * long, long)}
     *
     * @return A Runnable that, when run, removes the supplied argument from the Executor queue.
     */
    default Runnable executeRepeatedly(Runnable r, long initialDelayMillis, long delayMillis) {
        return executeRepeatedly(r, initialDelayMillis, delayMillis, TimeUnit.MILLISECONDS);
    }

    /**
     * Execute supplied Runnable on the Executors thread after initial delay, and subsequently with
     * the given delay between the termination of one execution and the commencement of the next..
     *
     * See {@link java.util.concurrent.ScheduledExecutorService#scheduleRepeatedly(Runnable,
     * long, long)}
     *
     * @return A Runnable that, when run, removes the supplied argument from the Executor queue.
     */
    Runnable executeRepeatedly(Runnable r, long initialDelay, long delay, TimeUnit unit);
}
+84 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.util.concurrency;

import java.util.concurrent.TimeUnit;

/**
 * Implementation of {@link RepeatableExecutor} for SystemUI.
 */
class RepeatableExecutorImpl implements RepeatableExecutor {

    private final DelayableExecutor mExecutor;

    RepeatableExecutorImpl(DelayableExecutor executor) {
        mExecutor = executor;
    }

    @Override
    public void execute(Runnable command) {
        mExecutor.execute(command);
    }

    @Override
    public Runnable executeRepeatedly(Runnable r, long initDelay, long delay, TimeUnit unit) {
        ExecutionToken token = new ExecutionToken(r, delay, unit);
        token.start(initDelay, unit);
        return token::cancel;
    }

    private class ExecutionToken implements Runnable {
        private final Runnable mCommand;
        private final long mDelay;
        private final TimeUnit mUnit;
        private final Object mLock = new Object();
        private Runnable mCancel;

        ExecutionToken(Runnable r, long delay, TimeUnit unit) {
            mCommand = r;
            mDelay = delay;
            mUnit = unit;
        }

        @Override
        public void run() {
            mCommand.run();
            synchronized (mLock) {
                if (mCancel != null) {
                    mCancel = mExecutor.executeDelayed(this, mDelay, mUnit);
                }
            }
        }

        /** Starts execution that will repeat the command until {@link cancel}. */
        public void start(long startDelay, TimeUnit unit) {
            synchronized (mLock) {
                mCancel = mExecutor.executeDelayed(this, startDelay, unit);
            }
        }

        /** Cancel repeated execution of command. */
        public void cancel() {
            synchronized (mLock) {
                if (mCancel != null) {
                    mCancel.run();
                    mCancel = null;
                }
            }
        }
    }
}
+162 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.util.concurrency;

import static com.google.common.truth.Truth.assertThat;

import android.testing.AndroidTestingRunner;

import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.time.FakeSystemClock;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@SmallTest
@RunWith(AndroidTestingRunner.class)
public class RepeatableExecutorTest extends SysuiTestCase {

    private static final int DELAY = 100;

    private FakeSystemClock mFakeClock;
    private FakeExecutor mFakeExecutor;
    private RepeatableExecutor mExecutor;
    private CountingTask mCountingTask;

    @Before
    public void setUp() throws Exception {
        mFakeClock = new FakeSystemClock();
        mFakeExecutor = new FakeExecutor(mFakeClock);
        mCountingTask = new CountingTask();
        mExecutor = new RepeatableExecutorImpl(mFakeExecutor);
    }

    /**
     * Test FakeExecutor that receives non-delayed items to execute.
     */
    @Test
    public void testExecute() {
        mExecutor.execute(mCountingTask);
        mFakeExecutor.runAllReady();
        assertThat(mCountingTask.getCount()).isEqualTo(1);
    }

    @Test
    public void testRepeats() {
        // GIVEN that a command is queued to repeat
        mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY);
        // WHEN The clock advances and the task is run
        mFakeExecutor.advanceClockToNext();
        mFakeExecutor.runAllReady();
        // THEN another task is queued
        assertThat(mCountingTask.getCount()).isEqualTo(1);
        assertThat(mFakeExecutor.numPending()).isEqualTo(1);
    }

    @Test
    public void testNoExecutionBeforeStartDelay() {
        // WHEN a command is queued with a start delay
        mExecutor.executeRepeatedly(mCountingTask, 2 * DELAY, DELAY);
        mFakeExecutor.runAllReady();
        // THEN then it doesn't run immediately
        assertThat(mCountingTask.getCount()).isEqualTo(0);
        assertThat(mFakeExecutor.numPending()).isEqualTo(1);
    }

    @Test
    public void testExecuteAfterStartDelay() {
        // GIVEN that a command is queued to repeat with a longer start delay
        mExecutor.executeRepeatedly(mCountingTask, 2 * DELAY, DELAY);
        // WHEN the clock advances the start delay
        mFakeClock.advanceTime(2 * DELAY);
        mFakeExecutor.runAllReady();
        // THEN the command has run and another task is queued
        assertThat(mCountingTask.getCount()).isEqualTo(1);
        assertThat(mFakeExecutor.numPending()).isEqualTo(1);
    }

    @Test
    public void testExecuteWithZeroStartDelay() {
        // WHEN a command is queued with no start delay
        mExecutor.executeRepeatedly(mCountingTask, 0L, DELAY);
        mFakeExecutor.runAllReady();
        // THEN the command has run and another task is queued
        assertThat(mCountingTask.getCount()).isEqualTo(1);
        assertThat(mFakeExecutor.numPending()).isEqualTo(1);
    }

    @Test
    public void testAdvanceTimeTwice() {
        // GIVEN that a command is queued to repeat
        mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY);
        // WHEN the clock advances the time DELAY twice
        mFakeClock.advanceTime(DELAY);
        mFakeExecutor.runAllReady();
        mFakeClock.advanceTime(DELAY);
        mFakeExecutor.runAllReady();
        // THEN the command has run twice and another task is queued
        assertThat(mCountingTask.getCount()).isEqualTo(2);
        assertThat(mFakeExecutor.numPending()).isEqualTo(1);
    }

    @Test
    public void testCancel() {
        // GIVEN that a scheduled command has been cancelled
        Runnable cancel = mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY);
        cancel.run();
        // WHEN the clock advances the time DELAY
        mFakeClock.advanceTime(DELAY);
        mFakeExecutor.runAllReady();
        // THEN the comamnd has not run and no further tasks are queued
        assertThat(mCountingTask.getCount()).isEqualTo(0);
        assertThat(mFakeExecutor.numPending()).isEqualTo(0);
    }

    @Test
    public void testCancelAfterStart() {
        // GIVEN that a command has reapeated a few times
        Runnable cancel = mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY);
        mFakeClock.advanceTime(DELAY);
        mFakeExecutor.runAllReady();
        // WHEN cancelled and time advances
        cancel.run();
        // THEN the command has only run the first time
        assertThat(mCountingTask.getCount()).isEqualTo(1);
        assertThat(mFakeExecutor.numPending()).isEqualTo(0);
    }

    /**
     * Runnable used for testing that counts the number of times run() is invoked.
     */
    private static class CountingTask implements Runnable {

        private int mRunCount;

        @Override
        public void run() {
            mRunCount++;
        }

        /** Gets the run count. */
        public int getCount() {
            return mRunCount;
        }
    }
}