diff --git a/firebase-annotations/firebase-annotations.gradle b/firebase-annotations/firebase-annotations.gradle index e77114c5afb..b1762d0a129 100644 --- a/firebase-annotations/firebase-annotations.gradle +++ b/firebase-annotations/firebase-annotations.gradle @@ -29,3 +29,7 @@ java { tasks.withType(JavaCompile) { options.compilerArgs << "-Werror" } + +dependencies { + implementation 'javax.inject:javax.inject:1' +} diff --git a/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Background.java b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Background.java new file mode 100644 index 00000000000..5626ea94a78 --- /dev/null +++ b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Background.java @@ -0,0 +1,30 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.annotations.concurrent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** + * An executor/coroutine dispatcher for long running tasks including disk IO, heavy CPU + * computations. + * + *

For operations that can block for long periods of time, like network requests, use the {@link + * Blocking} executor. + */ +@Qualifier +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) +public @interface Background {} diff --git a/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Blocking.java b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Blocking.java new file mode 100644 index 00000000000..d57513b57f1 --- /dev/null +++ b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Blocking.java @@ -0,0 +1,27 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.annotations.concurrent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** + * An executor/coroutine dispatcher for tasks that can block for long periods of time, e.g network + * IO. + */ +@Qualifier +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) +public @interface Blocking {} diff --git a/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Lightweight.java b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Lightweight.java new file mode 100644 index 00000000000..4d3b0828954 --- /dev/null +++ b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Lightweight.java @@ -0,0 +1,26 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.annotations.concurrent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** + * An executor/coroutine dispatcher for lightweight tasks that never block (on IO or other tasks). + */ +@Qualifier +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) +public @interface Lightweight {} diff --git a/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/UiThread.java b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/UiThread.java new file mode 100644 index 00000000000..bad10c164b8 --- /dev/null +++ b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/UiThread.java @@ -0,0 +1,24 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.annotations.concurrent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** An executor/coroutine dispatcher for work that must run on the UI thread. */ +@Qualifier +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) +public @interface UiThread {} diff --git a/firebase-common/firebase-common.gradle b/firebase-common/firebase-common.gradle index 47726702701..16c38ff1db6 100644 --- a/firebase-common/firebase-common.gradle +++ b/firebase-common/firebase-common.gradle @@ -66,6 +66,7 @@ dependencies { implementation project(':firebase-components') implementation 'com.google.android.gms:play-services-basement:18.1.0' implementation "com.google.android.gms:play-services-tasks:18.0.1" + implementation 'androidx.concurrent:concurrent-futures:1.1.0' // FirebaseApp references storage, so storage needs to be on classpath when dokka runs. javadocClasspath project(path: ':firebase-storage') diff --git a/firebase-common/ktx/ktx.gradle b/firebase-common/ktx/ktx.gradle index 2d15573ab83..f86c14a5e16 100644 --- a/firebase-common/ktx/ktx.gradle +++ b/firebase-common/ktx/ktx.gradle @@ -38,6 +38,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + implementation project(':firebase-annotations') implementation project(':firebase-common') implementation project(':firebase-components') implementation 'androidx.annotation:annotation:1.1.0' diff --git a/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Firebase.kt b/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Firebase.kt index f939f218f0f..64ed1fa31d2 100644 --- a/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Firebase.kt +++ b/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Firebase.kt @@ -17,9 +17,18 @@ import android.content.Context import androidx.annotation.Keep import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions +import com.google.firebase.annotations.concurrent.Background +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.annotations.concurrent.Lightweight +import com.google.firebase.annotations.concurrent.UiThread import com.google.firebase.components.Component import com.google.firebase.components.ComponentRegistrar +import com.google.firebase.components.Dependency +import com.google.firebase.components.Qualified import com.google.firebase.platforminfo.LibraryVersionComponent +import java.util.concurrent.Executor +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.asCoroutineDispatcher /** * Single access point to all firebase SDKs from Kotlin. @@ -40,11 +49,11 @@ fun Firebase.initialize(context: Context): FirebaseApp? = FirebaseApp.initialize /** Initializes and returns a FirebaseApp. */ fun Firebase.initialize(context: Context, options: FirebaseOptions): FirebaseApp = - FirebaseApp.initializeApp(context, options) + FirebaseApp.initializeApp(context, options) /** Initializes and returns a FirebaseApp. */ fun Firebase.initialize(context: Context, options: FirebaseOptions, name: String): FirebaseApp = - FirebaseApp.initializeApp(context, options, name) + FirebaseApp.initializeApp(context, options, name) /** Returns options of default FirebaseApp */ val Firebase.options: FirebaseOptions @@ -57,6 +66,27 @@ internal const val LIBRARY_NAME: String = "fire-core-ktx" class FirebaseCommonKtxRegistrar : ComponentRegistrar { override fun getComponents(): List> { return listOf( - LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), + coroutineDispatcher(), + coroutineDispatcher(), + coroutineDispatcher(), + coroutineDispatcher() + ) } } + +private inline fun coroutineDispatcher(): Component = + Component.builder( + Qualified.qualified(T::class.java, CoroutineDispatcher::class.java) + ).add( + Dependency.required( + Qualified.qualified( + T::class.java, + Executor::class.java + ) + ) + ).factory { c -> + c.get( + Qualified.qualified(T::class.java, Executor::class.java) + ).asCoroutineDispatcher() + }.build() diff --git a/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java b/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java index efc6c01d8ce..8fc7767388e 100644 --- a/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java +++ b/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java @@ -23,8 +23,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Build; -import android.os.Handler; -import android.os.Looper; import android.text.TextUtils; import android.util.Log; import androidx.annotation.GuardedBy; @@ -47,6 +45,7 @@ import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.ComponentRuntime; import com.google.firebase.components.Lazy; +import com.google.firebase.concurrent.ExecutorsRegistrar; import com.google.firebase.events.Publisher; import com.google.firebase.heartbeatinfo.DefaultHeartBeatController; import com.google.firebase.inject.Provider; @@ -59,7 +58,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -97,16 +95,10 @@ public class FirebaseApp { private static final Object LOCK = new Object(); - private static final Executor UI_EXECUTOR = new UiExecutor(); - /** A map of (name, FirebaseApp) instances. */ @GuardedBy("LOCK") static final Map INSTANCES = new ArrayMap<>(); - private static final String FIREBASE_ANDROID = "fire-android"; - private static final String FIREBASE_COMMON = "fire-core"; - private static final String KOTLIN = "kotlin"; - private final Context applicationContext; private final String name; private final FirebaseOptions options; @@ -427,9 +419,10 @@ protected FirebaseApp(Context applicationContext, String name, FirebaseOptions o FirebaseTrace.pushTrace("Runtime"); componentRuntime = - ComponentRuntime.builder(UI_EXECUTOR) + ComponentRuntime.builder(com.google.firebase.concurrent.UiExecutor.INSTANCE) .addLazyComponentRegistrars(registrars) .addComponentRegistrar(new FirebaseCommonRegistrar()) + .addComponentRegistrar(new ExecutorsRegistrar()) .addComponent(Component.of(applicationContext, Context.class)) .addComponent(Component.of(this, FirebaseApp.class)) .addComponent(Component.of(options, FirebaseOptions.class)) @@ -712,14 +705,4 @@ public void onBackgroundStateChanged(boolean background) { } } } - - private static class UiExecutor implements Executor { - - private static final Handler HANDLER = new Handler(Looper.getMainLooper()); - - @Override - public void execute(@NonNull Runnable command) { - HANDLER.post(command); - } - } } diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/CustomThreadFactory.java b/firebase-common/src/main/java/com/google/firebase/concurrent/CustomThreadFactory.java new file mode 100644 index 00000000000..b3079db1d20 --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/CustomThreadFactory.java @@ -0,0 +1,41 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.concurrent; + +import java.util.Locale; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; + +class CustomThreadFactory implements ThreadFactory { + private static final ThreadFactory DEFAULT = Executors.defaultThreadFactory(); + private final AtomicLong threadCount = new AtomicLong(); + private final String namePrefix; + private final int priority; + + CustomThreadFactory(String namePrefix, int priority) { + this.namePrefix = namePrefix; + this.priority = priority; + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = DEFAULT.newThread(r); + thread.setPriority(priority); + thread.setName( + String.format(Locale.ROOT, "%s Thread #%d", namePrefix, threadCount.getAndIncrement())); + return thread; + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledExecutorService.java b/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledExecutorService.java new file mode 100644 index 00000000000..23b9385c284 --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledExecutorService.java @@ -0,0 +1,185 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.concurrent; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class DelegatingScheduledExecutorService implements ScheduledExecutorService { + private final ExecutorService delegate; + private final ScheduledExecutorService scheduler; + + DelegatingScheduledExecutorService(ExecutorService delegate, ScheduledExecutorService scheduler) { + this.delegate = delegate; + this.scheduler = scheduler; + } + + @Override + public void shutdown() { + throw new UnsupportedOperationException("Shutting down is not allowed."); + } + + @Override + public List shutdownNow() { + throw new UnsupportedOperationException("Shutting down is not allowed."); + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + + @Override + public Future submit(Callable task) { + return delegate.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return delegate.submit(task, result); + } + + @Override + public Future submit(Runnable task) { + return delegate.submit(task); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return delegate.invokeAll(tasks); + } + + @Override + public List> invokeAll( + Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + return delegate.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) + throws ExecutionException, InterruptedException { + return delegate.invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws ExecutionException, InterruptedException, TimeoutException { + return delegate.invokeAny(tasks, timeout, unit); + } + + @Override + public void execute(Runnable command) { + delegate.execute(command); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + return new DelegatingScheduledFuture( + completer -> + scheduler.schedule( + () -> + delegate.execute( + () -> { + try { + command.run(); + completer.set(null); + } catch (Exception ex) { + completer.setException(ex); + } + }), + delay, + unit)); + } + + @Override + public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + return new DelegatingScheduledFuture<>( + completer -> + scheduler.schedule( + () -> + delegate.submit( + () -> { + try { + V result = callable.call(); + completer.set(result); + } catch (Exception ex) { + completer.setException(ex); + } + }), + delay, + unit)); + } + + @Override + public ScheduledFuture scheduleAtFixedRate( + Runnable command, long initialDelay, long period, TimeUnit unit) { + return new DelegatingScheduledFuture<>( + completer -> + scheduler.scheduleAtFixedRate( + () -> + delegate.execute( + () -> { + try { + command.run(); + } catch (Exception ex) { + completer.setException(ex); + throw ex; + } + }), + initialDelay, + period, + unit)); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay( + Runnable command, long initialDelay, long delay, TimeUnit unit) { + return new DelegatingScheduledFuture<>( + completer -> + scheduler.scheduleWithFixedDelay( + () -> + delegate.execute( + () -> { + try { + command.run(); + } catch (Exception ex) { + completer.setException(ex); + } + }), + initialDelay, + delay, + unit)); + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledFuture.java b/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledFuture.java new file mode 100644 index 00000000000..26ef258136a --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledFuture.java @@ -0,0 +1,72 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.concurrent; + +import android.annotation.SuppressLint; +import androidx.concurrent.futures.AbstractResolvableFuture; +import java.util.concurrent.Delayed; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +// While direct use of AbstractResolvableFuture is not encouraged, it's stable for use and is not +// going to be removed. In this case it's required since we need to implement a ScheduledFuture so +// we can't use CallbackToFutureAdapter. +@SuppressLint("RestrictedApi") +class DelegatingScheduledFuture extends AbstractResolvableFuture + implements ScheduledFuture { + + interface Completer { + void set(T value); + + void setException(Throwable ex); + } + + interface Resolver { + ScheduledFuture addCompleter(Completer completer); + } + + DelegatingScheduledFuture(Resolver resolver) { + upstreamFuture = + resolver.addCompleter( + new Completer() { + @Override + public void set(V value) { + DelegatingScheduledFuture.this.set(value); + } + + @Override + public void setException(Throwable ex) { + DelegatingScheduledFuture.this.setException(ex); + } + }); + } + + private final ScheduledFuture upstreamFuture; + + @Override + protected void afterDone() { + upstreamFuture.cancel(wasInterrupted()); + } + + @Override + public long getDelay(TimeUnit unit) { + return upstreamFuture.getDelay(unit); + } + + @Override + public int compareTo(Delayed o) { + return upstreamFuture.compareTo(o); + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/ExecutorsRegistrar.java b/firebase-common/src/main/java/com/google/firebase/concurrent/ExecutorsRegistrar.java new file mode 100644 index 00000000000..1b28eb8aec9 --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/ExecutorsRegistrar.java @@ -0,0 +1,102 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.concurrent; + +import android.annotation.SuppressLint; +import android.os.Process; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Lazy; +import com.google.firebase.components.Qualified; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; + +/** @hide */ +@SuppressLint("ThreadPoolCreation") +public class ExecutorsRegistrar implements ComponentRegistrar { + private static final Lazy BG_EXECUTOR = + new Lazy<>( + () -> + scheduled( + Executors.newFixedThreadPool( + 4, factory("Firebase Background", Process.THREAD_PRIORITY_BACKGROUND)))); + + private static final Lazy LITE_EXECUTOR = + new Lazy<>( + () -> + scheduled( + Executors.newFixedThreadPool( + Math.max(2, Runtime.getRuntime().availableProcessors()), + factory("Firebase Lite", Process.THREAD_PRIORITY_DEFAULT)))); + + private static final Lazy BLOCKING_EXECUTOR = + new Lazy<>( + () -> + scheduled( + Executors.newCachedThreadPool( + factory( + "Firebase Blocking", + Process.THREAD_PRIORITY_BACKGROUND + + Process.THREAD_PRIORITY_LESS_FAVORABLE)))); + + private static final Lazy SCHEDULER = + new Lazy<>( + () -> + Executors.newSingleThreadScheduledExecutor( + factory("Firebase Scheduler", Process.THREAD_PRIORITY_DEFAULT))); + + @Override + public List> getComponents() { + return Arrays.asList( + Component.builder( + Qualified.qualified(Background.class, ScheduledExecutorService.class), + Qualified.qualified(Background.class, ExecutorService.class), + Qualified.qualified(Background.class, Executor.class)) + .factory(c -> BG_EXECUTOR.get()) + .build(), + Component.builder( + Qualified.qualified(Blocking.class, ScheduledExecutorService.class), + Qualified.qualified(Blocking.class, ExecutorService.class), + Qualified.qualified(Blocking.class, Executor.class)) + .factory(c -> BLOCKING_EXECUTOR.get()) + .build(), + Component.builder( + Qualified.qualified(Lightweight.class, ScheduledExecutorService.class), + Qualified.qualified(Lightweight.class, ExecutorService.class), + Qualified.qualified(Lightweight.class, Executor.class)) + .factory(c -> LITE_EXECUTOR.get()) + .build(), + Component.builder(Qualified.qualified(UiThread.class, Executor.class)) + .factory(c -> UiExecutor.INSTANCE) + .build()); + } + + private static ScheduledExecutorService scheduled(ExecutorService delegate) { + return new DelegatingScheduledExecutorService(delegate, SCHEDULER.get()); + } + + private static ThreadFactory factory(String threadPrefix, int priority) { + return new CustomThreadFactory(threadPrefix, priority); + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/UiExecutor.java b/firebase-common/src/main/java/com/google/firebase/concurrent/UiExecutor.java new file mode 100644 index 00000000000..e06311c8a3a --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/UiExecutor.java @@ -0,0 +1,31 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.concurrent; + +import android.os.Handler; +import android.os.Looper; +import java.util.concurrent.Executor; + +/** @hide */ +public enum UiExecutor implements Executor { + INSTANCE; + + private static final Handler HANDLER = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable command) { + HANDLER.post(command); + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java b/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java index cb7722c37bd..ff9ba5123fc 100644 --- a/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java +++ b/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java @@ -23,18 +23,16 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; import com.google.firebase.components.Component; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.inject.Provider; import com.google.firebase.platforminfo.UserAgentPublisher; import java.io.ByteArrayOutputStream; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.zip.GZIPOutputStream; import org.json.JSONArray; import org.json.JSONObject; @@ -52,9 +50,6 @@ public class DefaultHeartBeatController implements HeartBeatController, HeartBea private final Executor backgroundExecutor; - private static final ThreadFactory THREAD_FACTORY = - r -> new Thread(r, "heartbeat-information-executor"); - public Task registerHeartBeat() { if (consumers.size() <= 0) { return Tasks.forResult(null); @@ -118,12 +113,12 @@ private DefaultHeartBeatController( Context context, String persistenceKey, Set consumers, - Provider userAgentProvider) { + Provider userAgentProvider, + Executor backgroundExecutor) { this( () -> new HeartBeatInfoStorage(context, persistenceKey), consumers, - new ThreadPoolExecutor( - 0, 1, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), THREAD_FACTORY), + backgroundExecutor, userAgentProvider, context); } @@ -143,19 +138,22 @@ private DefaultHeartBeatController( } public static @NonNull Component component() { + Qualified backgroundExecutor = Qualified.qualified(Background.class, Executor.class); return Component.builder( DefaultHeartBeatController.class, HeartBeatController.class, HeartBeatInfo.class) .add(Dependency.required(Context.class)) .add(Dependency.required(FirebaseApp.class)) .add(Dependency.setOf(HeartBeatConsumer.class)) .add(Dependency.requiredProvider(UserAgentPublisher.class)) + .add(Dependency.required(backgroundExecutor)) .factory( c -> new DefaultHeartBeatController( c.get(Context.class), c.get(FirebaseApp.class).getPersistenceKey(), c.setOf(HeartBeatConsumer.class), - c.getProvider(UserAgentPublisher.class))) + c.getProvider(UserAgentPublisher.class), + c.get(backgroundExecutor))) .build(); } diff --git a/firebase-common/src/test/AndroidManifest.xml b/firebase-common/src/test/AndroidManifest.xml new file mode 100644 index 00000000000..b0baac40e37 --- /dev/null +++ b/firebase-common/src/test/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase-common/src/test/java/com/google/firebase/concurrent/DelegatingScheduledExecutorServiceTest.java b/firebase-common/src/test/java/com/google/firebase/concurrent/DelegatingScheduledExecutorServiceTest.java new file mode 100644 index 00000000000..0717ddf4ee8 --- /dev/null +++ b/firebase-common/src/test/java/com/google/firebase/concurrent/DelegatingScheduledExecutorServiceTest.java @@ -0,0 +1,82 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.concurrent; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class DelegatingScheduledExecutorServiceTest { + private final DelegatingScheduledExecutorService service = + new DelegatingScheduledExecutorService( + Executors.newCachedThreadPool(), Executors.newSingleThreadScheduledExecutor()); + + @Test + public void schedule_whenCancelled_shouldCancelUnderlyingFuture() { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean ran = new AtomicBoolean(); + ScheduledFuture future = + service.schedule( + () -> { + latch.await(); + ran.set(true); + return null; + }, + 10, + TimeUnit.SECONDS); + future.cancel(true); + latch.countDown(); + assertThat(ran.get()).isFalse(); + } + + @Test + public void scheduleAtFixedRate_whenRunnableThrows_shouldCancelSchedule() + throws InterruptedException { + Semaphore semaphore = new Semaphore(0); + AtomicLong ran = new AtomicLong(); + + ScheduledFuture future = + service.scheduleAtFixedRate( + () -> { + ran.incrementAndGet(); + throw new RuntimeException("fail"); + }, + 1, + 1, + TimeUnit.SECONDS); + + semaphore.release(); + try { + future.get(); + fail("Expected exception not thrown"); + } catch (ExecutionException ex) { + assertThat(ex.getCause()).isInstanceOf(RuntimeException.class); + assertThat(ex.getCause().getMessage()).isEqualTo("fail"); + } + assertThat(ran.get()).isEqualTo(1); + } +} diff --git a/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponent.java b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponent.java new file mode 100644 index 00000000000..0da8650a80f --- /dev/null +++ b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponent.java @@ -0,0 +1,58 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.concurrent; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; + +class ExecutorComponent { + final ScheduledExecutorService bgScheduledService; + final ExecutorService bgService; + final Executor bgExecutor; + + final ScheduledExecutorService liteScheduledService; + final ExecutorService liteService; + final Executor liteExecutor; + + final ScheduledExecutorService blockingScheduledService; + final ExecutorService blockingService; + final Executor blockingExecutor; + + final Executor uiExecutor; + + public ExecutorComponent( + ScheduledExecutorService bgScheduledService, + ExecutorService bgService, + Executor bgExecutor, + ScheduledExecutorService liteScheduledService, + ExecutorService liteService, + Executor liteExecutor, + ScheduledExecutorService blockingScheduledService, + ExecutorService blockingService, + Executor blockingExecutor, + Executor uiExecutor) { + this.bgScheduledService = bgScheduledService; + this.bgService = bgService; + this.bgExecutor = bgExecutor; + this.liteScheduledService = liteScheduledService; + this.liteService = liteService; + this.liteExecutor = liteExecutor; + this.blockingScheduledService = blockingScheduledService; + this.blockingService = blockingService; + this.blockingExecutor = blockingExecutor; + this.uiExecutor = uiExecutor; + } +} diff --git a/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponentTest.java b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponentTest.java new file mode 100644 index 00000000000..bf560c88e95 --- /dev/null +++ b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponentTest.java @@ -0,0 +1,46 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.concurrent; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.FirebaseAppTestUtil.withApp; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.firebase.FirebaseOptions; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ExecutorComponentTest { + private static final FirebaseOptions OPTIONS = + new FirebaseOptions.Builder() + .setApiKey("myKey") + .setApplicationId("123") + .setProjectId("456") + .build(); + + @Test + public void testThatAllExecutorsAreRegisteredByCommon() { + withApp( + "test", + OPTIONS, + app -> { + ExecutorComponent executorComponent = app.get(ExecutorComponent.class); + // If the component is not null, it means it was able to get all of its required + // dependencies, otherwise get() would throw. + assertThat(executorComponent).isNotNull(); + }); + } +} diff --git a/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorTestsRegistrar.java b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorTestsRegistrar.java new file mode 100644 index 00000000000..9fdcf7d428f --- /dev/null +++ b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorTestsRegistrar.java @@ -0,0 +1,81 @@ +// Copyright 2022 Google LLC +// +// 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.google.firebase.concurrent; + +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; + +public class ExecutorTestsRegistrar implements ComponentRegistrar { + @Override + public List> getComponents() { + Qualified bgScheduledService = + Qualified.qualified(Background.class, ScheduledExecutorService.class); + Qualified bgService = + Qualified.qualified(Background.class, ExecutorService.class); + Qualified bgExecutor = Qualified.qualified(Background.class, Executor.class); + + Qualified liteScheduledService = + Qualified.qualified(Lightweight.class, ScheduledExecutorService.class); + Qualified liteService = + Qualified.qualified(Lightweight.class, ExecutorService.class); + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + + Qualified blockingScheduledService = + Qualified.qualified(Blocking.class, ScheduledExecutorService.class); + Qualified blockingService = + Qualified.qualified(Blocking.class, ExecutorService.class); + Qualified blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); + + Qualified uiExecutor = Qualified.qualified(UiThread.class, Executor.class); + + return Collections.singletonList( + Component.builder(ExecutorComponent.class) + .add(Dependency.required(bgScheduledService)) + .add(Dependency.required(bgService)) + .add(Dependency.required(bgExecutor)) + .add(Dependency.required(liteScheduledService)) + .add(Dependency.required(liteService)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(blockingScheduledService)) + .add(Dependency.required(blockingService)) + .add(Dependency.required(blockingExecutor)) + .add(Dependency.required(uiExecutor)) + .factory( + c -> + new ExecutorComponent( + c.get(bgScheduledService), + c.get(bgService), + c.get(bgExecutor), + c.get(liteScheduledService), + c.get(liteService), + c.get(liteExecutor), + c.get(blockingScheduledService), + c.get(blockingService), + c.get(blockingExecutor), + c.get(uiExecutor))) + .build()); + } +} diff --git a/tools/lint/src/main/kotlin/CheckRegistry.kt b/tools/lint/src/main/kotlin/CheckRegistry.kt index ebd68916a9f..4b995e55cb4 100644 --- a/tools/lint/src/main/kotlin/CheckRegistry.kt +++ b/tools/lint/src/main/kotlin/CheckRegistry.kt @@ -29,6 +29,8 @@ class CheckRegistry : IssueRegistry() { NonAndroidxNullabilityDetector.NON_ANDROIDX_NULLABILITY, DeferredApiDetector.INVALID_DEFERRED_API_USE, ProviderAssignmentDetector.INVALID_PROVIDER_ASSIGNMENT + // TODO(vkryachko): enable the check after suppressing current violations. + // ThreadPoolDetector.THREAD_POOL_CREATION ) override val api: Int diff --git a/tools/lint/src/main/kotlin/ThreadPoolDetector.kt b/tools/lint/src/main/kotlin/ThreadPoolDetector.kt new file mode 100644 index 00000000000..46148b5b0f7 --- /dev/null +++ b/tools/lint/src/main/kotlin/ThreadPoolDetector.kt @@ -0,0 +1,64 @@ +package com.google.firebase.lint.checks + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +class ThreadPoolDetector : Detector(), SourceCodeScanner { + override fun getApplicableMethodNames(): List = listOf( + "newCachedThreadPool", + "newFixedThreadPool", + "newScheduledThreadPool", + "newSingleThreadExecutor", + "newSingleThreadScheduledExecutor", + "newWorkStealingPool" + ) + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + if (!isExecutorMethod(method)) { + return + } + + context.report( + THREAD_POOL_CREATION, + context.getCallLocation(node, includeReceiver = false, includeArguments = true), + "Creating thread pools is not allowed.") + } + + private fun isExecutorMethod(method: PsiMethod): Boolean { + (method.parent as? PsiClass)?.let { + return it.qualifiedName == "java.util.concurrent.Executors" + } + return false + } + + companion object { + private val IMPLEMENTATION = Implementation( + ThreadPoolDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + + /** Calling methods on the wrong thread */ + @JvmField + val THREAD_POOL_CREATION = Issue.create( + id = "ThreadPoolCreation", + briefDescription = "Creating thread pools is not allowed.", + + explanation = """ + Please use one of the executors provided by firebase-common + """, + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.ERROR, + implementation = IMPLEMENTATION + ) + } +}