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

Commit 37c423a4 authored by Hawkwood Glazier's avatar Hawkwood Glazier
Browse files

Provide mechanism for on demand loading/unloading of connected plugins

Bug: 266466757
Bug: 270860591
Test: Extended automated test to detect unfreed objects
Test: Manually checked on device running memory
Change-Id: I074601632890776a86a1117c3e305aca4d68503d
parent bf97b682
Loading
Loading
Loading
Loading
+189 −49
Original line number Diff line number Diff line
@@ -29,14 +29,15 @@ import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProvider
import com.android.systemui.plugins.ClockProviderPlugin
import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.PluginLifecycleManager
import com.android.systemui.plugins.PluginListener
import com.android.systemui.plugins.PluginManager
import com.android.systemui.util.Assert
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

private val TAG = ClockRegistry::class.simpleName!!
private const val DEBUG = true
private val KEY_TIMESTAMP = "appliedTimestamp"

@@ -51,7 +52,10 @@ open class ClockRegistry(
    val handleAllUsers: Boolean,
    defaultClockProvider: ClockProvider,
    val fallbackClockId: ClockId = DEFAULT_CLOCK_ID,
    val keepAllLoaded: Boolean,
    val subTag: String,
) {
    private val TAG = "${ClockRegistry::class.simpleName} ($subTag)"
    interface ClockChangeListener {
        // Called when the active clock changes
        fun onCurrentClockChanged() {}
@@ -76,11 +80,85 @@ open class ClockRegistry(

    private val pluginListener =
        object : PluginListener<ClockProviderPlugin> {
            override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
                connectClocks(plugin)
            override fun onPluginAttached(manager: PluginLifecycleManager<ClockProviderPlugin>) {
                manager.loadPlugin()
            }

            override fun onPluginLoaded(
                plugin: ClockProviderPlugin,
                pluginContext: Context,
                manager: PluginLifecycleManager<ClockProviderPlugin>
            ) {
                var isClockListChanged = false
                for (clock in plugin.getClocks()) {
                    val id = clock.clockId
                    var isNew = false
                    val info =
                        availableClocks.getOrPut(id) {
                            isNew = true
                            ClockInfo(clock, plugin, manager)
                        }

            override fun onPluginDisconnected(plugin: ClockProviderPlugin) =
                disconnectClocks(plugin)
                    if (isNew) {
                        isClockListChanged = true
                        onConnected(id)
                    }

                    if (manager != info.manager) {
                        Log.e(
                            TAG,
                            "Clock Id conflict on load: $id is registered to another provider"
                        )
                        continue
                    }

                    info.provider = plugin
                    onLoaded(id)
                }

                if (isClockListChanged) {
                    triggerOnAvailableClocksChanged()
                }
                verifyLoadedProviders()
            }

            override fun onPluginUnloaded(
                plugin: ClockProviderPlugin,
                manager: PluginLifecycleManager<ClockProviderPlugin>
            ) {
                for (clock in plugin.getClocks()) {
                    val id = clock.clockId
                    val info = availableClocks[id]
                    if (info?.manager != manager) {
                        Log.e(
                            TAG,
                            "Clock Id conflict on unload: $id is registered to another provider"
                        )
                        continue
                    }
                    info.provider = null
                    onUnloaded(id)
                }

                verifyLoadedProviders()
            }

            override fun onPluginDetached(manager: PluginLifecycleManager<ClockProviderPlugin>) {
                val removed = mutableListOf<ClockId>()
                availableClocks.entries.removeAll {
                    if (it.value.manager != manager) {
                        return@removeAll false
                    }

                    removed.add(it.key)
                    return@removeAll true
                }

                removed.forEach(::onDisconnected)
                if (removed.size > 0) {
                    triggerOnAvailableClocksChanged()
                }
            }
        }

    private val userSwitchObserver =
@@ -96,7 +174,8 @@ open class ClockRegistry(
        protected set(value) {
            if (field != value) {
                field = value
                scope.launch(mainDispatcher) { onClockChanged { it.onCurrentClockChanged() } }
                verifyLoadedProviders()
                triggerOnCurrentClockChanged()
            }
        }

@@ -168,9 +247,36 @@ open class ClockRegistry(
        Assert.isNotMainThread()
    }

    private fun onClockChanged(func: (ClockChangeListener) -> Unit) {
    private var isClockChanged = AtomicBoolean(false)
    private fun triggerOnCurrentClockChanged() {
        val shouldSchedule = isClockChanged.compareAndSet(false, true)
        if (!shouldSchedule) {
            return
        }

        android.util.Log.e("HAWK", "triggerOnCurrentClockChanged")
        scope.launch(mainDispatcher) {
            assertMainThread()
        clockChangeListeners.forEach(func)
            android.util.Log.e("HAWK", "isClockChanged")
            isClockChanged.set(false)
            clockChangeListeners.forEach { it.onCurrentClockChanged() }
        }
    }

    private var isClockListChanged = AtomicBoolean(false)
    private fun triggerOnAvailableClocksChanged() {
        val shouldSchedule = isClockListChanged.compareAndSet(false, true)
        if (!shouldSchedule) {
            return
        }

        android.util.Log.e("HAWK", "triggerOnAvailableClocksChanged")
        scope.launch(mainDispatcher) {
            assertMainThread()
            android.util.Log.e("HAWK", "isClockListChanged")
            isClockListChanged.set(false)
            clockChangeListeners.forEach { it.onAvailableClocksChanged() }
        }
    }

    public fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) {
@@ -190,7 +296,12 @@ open class ClockRegistry(
        }

    init {
        connectClocks(defaultClockProvider)
        // Register default clock designs
        for (clock in defaultClockProvider.getClocks()) {
            availableClocks[clock.clockId] = ClockInfo(clock, defaultClockProvider, null)
        }

        // Something has gone terribly wrong if the default clock isn't present
        if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) {
            throw IllegalArgumentException(
                "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID"
@@ -244,59 +355,87 @@ open class ClockRegistry(
        }
    }

    private fun connectClocks(provider: ClockProvider) {
        var isAvailableChanged = false
        val currentId = currentClockId
        for (clock in provider.getClocks()) {
            val id = clock.clockId
            val current = availableClocks[id]
            if (current != null) {
                Log.e(
                    TAG,
                    "Clock Id conflict: $id is registered by both " +
                        "${provider::class.simpleName} and ${current.provider::class.simpleName}"
                )
                continue
    private var isVerifying = AtomicBoolean(false)
    private fun verifyLoadedProviders() {
        val shouldSchedule = isVerifying.compareAndSet(false, true)
        if (!shouldSchedule) {
            return
        }

        scope.launch(bgDispatcher) {
            if (keepAllLoaded) {
                // Enforce that all plugins are loaded if requested
                for ((_, info) in availableClocks) {
                    info.manager?.loadPlugin()
                }
                isVerifying.set(false)
                return@launch
            }

            val currentClock = availableClocks[currentClockId]
            if (currentClock == null) {
                // Current Clock missing, load no plugins and use default
                for ((_, info) in availableClocks) {
                    info.manager?.unloadPlugin()
                }
                isVerifying.set(false)
                return@launch
            }

            val currentManager = currentClock.manager
            currentManager?.loadPlugin()

            for ((_, info) in availableClocks) {
                val manager = info.manager
                if (manager != null && manager.isLoaded && currentManager != manager) {
                    manager.unloadPlugin()
                }
            }
            isVerifying.set(false)
        }
    }

            availableClocks[id] = ClockInfo(clock, provider)
            isAvailableChanged = true
    private fun onConnected(clockId: ClockId) {
        if (DEBUG) {
                Log.i(TAG, "Added ${clock.clockId}")
            Log.i(TAG, "Connected $clockId")
        }

            if (currentId == id) {
        if (currentClockId == clockId) {
            if (DEBUG) {
                    Log.i(TAG, "Current clock ($currentId) was connected")
                Log.i(TAG, "Current clock ($clockId) was connected")
            }
                onClockChanged { it.onCurrentClockChanged() }
        }
    }

        if (isAvailableChanged) {
            onClockChanged { it.onAvailableClocksChanged() }
        }
    private fun onLoaded(clockId: ClockId) {
        if (DEBUG) {
            Log.i(TAG, "Loaded $clockId")
        }

    private fun disconnectClocks(provider: ClockProvider) {
        var isAvailableChanged = false
        val currentId = currentClockId
        for (clock in provider.getClocks()) {
            availableClocks.remove(clock.clockId)
            isAvailableChanged = true
        if (currentClockId == clockId) {
            Log.i(TAG, "Current clock ($clockId) was loaded")
            triggerOnCurrentClockChanged()
        }
    }

    private fun onUnloaded(clockId: ClockId) {
        if (DEBUG) {
                Log.i(TAG, "Removed ${clock.clockId}")
            Log.i(TAG, "Unloaded $clockId")
        }

            if (currentId == clock.clockId) {
                Log.w(TAG, "Current clock ($currentId) was disconnected")
                onClockChanged { it.onCurrentClockChanged() }
        if (currentClockId == clockId) {
            Log.w(TAG, "Current clock ($clockId) was unloaded")
            triggerOnCurrentClockChanged()
        }
    }

    private fun onDisconnected(clockId: ClockId) {
        if (DEBUG) {
            Log.i(TAG, "Disconnected $clockId")
        }

        if (isAvailableChanged) {
            onClockChanged { it.onAvailableClocksChanged() }
        if (currentClockId == clockId) {
            Log.w(TAG, "Current clock ($clockId) was disconnected")
        }
    }

@@ -345,6 +484,7 @@ open class ClockRegistry(

    private data class ClockInfo(
        val metadata: ClockMetadata,
        val provider: ClockProvider,
        var provider: ClockProvider?,
        val manager: PluginLifecycleManager<ClockProviderPlugin>?,
    )
}
+48 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.plugins;

/**
 * Provides the ability for consumers to control plugin lifecycle.
 *
 * @param <T> is the target plugin type
 */
public interface PluginLifecycleManager<T extends Plugin> {
    /** Returns the currently loaded plugin instance (if plugin is loaded) */
    T getPlugin();

    /** returns true if the plugin is currently loaded */
    default boolean isLoaded() {
        return getPlugin() != null;
    }

    /**
     * Loads and creates the plugin instance if it does not exist.
     *
     * This will trigger {@link PluginListener#onPluginLoaded} with the new instance if it did not
     * already exist.
     */
    void loadPlugin();

    /**
     * Unloads and destroys the plugin instance if it exists.
     *
     * This will trigger {@link PluginListener#onPluginUnloaded} if a concrete plugin instance
     * existed when this call was made.
     */
    void unloadPlugin();
}
+83 −3
Original line number Diff line number Diff line
@@ -17,7 +17,32 @@ package com.android.systemui.plugins;
import android.content.Context;

/**
 * Interface for listening to plugins being connected.
 * Interface for listening to plugins being connected and disconnected.
 *
 * The call order for a plugin is
 *  1) {@link #onPluginAttached}
 *          Called when a new plugin is added to the device, or an existing plugin was replaced by
 *          the package manager. Will only be called once per package manager event. If multiple
 *          non-conflicting packages which have the same plugin interface are installed on the
 *          device, then this method can be called multiple times with different instances of
 *          {@link PluginLifecycleManager} (as long as `allowMultiple` was set to true when the
 *          listener was registered with {@link PluginManager#addPluginListener}).
 *  2) {@link #onPluginLoaded}
 *          Called whenever a new instance of the plugin object is created and ready for use. Can be
 *          called multiple times per {@link PluginLifecycleManager}, but will always pass a newly
 *          created plugin object. {@link #onPluginUnloaded} with the previous plugin object will
 *          be called before another call to {@link #onPluginLoaded} is made. This method will be
 *          called once automatically after {@link #onPluginAttached}. Besides the initial call,
 *          {@link #onPluginLoaded} will occur due to {@link PluginLifecycleManager#loadPlugin}.
 *  3) {@link #onPluginUnloaded}
 *          Called when a request to unload the plugin has been received. This can be triggered from
 *          a related call to {@link PluginLifecycleManager#unloadPlugin} or for any reason that
 *          {@link #onPluginDetached} would be triggered.
 *  4) {@link #onPluginDetached}
 *          Called when the package is removed from the device, disabled, or replaced due to an
 *          external trigger. These are events from the android package manager.
 *
 * @param <T> is the target plugin type
 */
public interface PluginListener<T extends Plugin> {
    /**
@@ -25,14 +50,69 @@ public interface PluginListener<T extends Plugin> {
     * This may be called multiple times if multiple plugins are allowed.
     * It may also be called in the future if the plugin package changes
     * and needs to be reloaded.
     *
     * @deprecated Migrate to {@link #onPluginLoaded} or {@link #onPluginAttached}
     */
    void onPluginConnected(T plugin, Context pluginContext);
    @Deprecated
    default void onPluginConnected(T plugin, Context pluginContext) {
        // Optional
    }

    /**
     * Called when the plugin is first attached to the host application. {@link #onPluginLoaded}
     * will be automatically called as well when first attached. This may be called multiple times
     * if multiple plugins are allowed. It may also be called in the future if the plugin package
     * changes and needs to be reloaded. Each call to {@link #onPluginAttached} will provide a new
     * or different {@link PluginLifecycleManager}.
     */
    default void onPluginAttached(PluginLifecycleManager<T> manager) {
        // Optional
    }

    /**
     * Called when a plugin has been uninstalled/updated and should be removed
     * from use.
     *
     * @deprecated Migrate to {@link #onPluginDetached} or {@link #onPluginUnloaded}
     */
    @Deprecated
    default void onPluginDisconnected(T plugin) {
        // Optional.
    }

    /**
     * Called when the plugin has been detached from the host application. Implementers should no
     * longer attempt to reload it via this {@link PluginLifecycleManager}. If the package was
     * updated and not removed, then {@link #onPluginAttached} will be called again when the updated
     * package is available.
     */
    default void onPluginDetached(PluginLifecycleManager<T> manager) {
        // Optional.
    }

    /**
     * Called when the plugin is loaded into the host's process and is available for use. This can
     * happen several times if clients are using {@link PluginLifecycleManager} to manipulate a
     * plugin's load state. Each call to {@link #onPluginLoaded} will have a matched call to
     * {@link #onPluginUnloaded} when that plugin object should no longer be used.
     */
    default void onPluginLoaded(
            T plugin,
            Context pluginContext,
            PluginLifecycleManager<T> manager
    ) {
        // Optional, default to deprecated version
        onPluginConnected(plugin, pluginContext);
    }

    /**
     * Called when the plugin should no longer be used. Listeners should clean up all references to
     * the relevant plugin so that it can be garbage collected. If the plugin object is required in
     * the future a call can be made to {@link PluginLifecycleManager#loadPlugin} to create a new
     * plugin object and trigger {@link #onPluginLoaded}.
     */
    default void onPluginUnloaded(T plugin, PluginLifecycleManager<T> manager) {
        // Optional, default to deprecated version
        onPluginDisconnected(plugin);
    }
}
 No newline at end of file
+3 −3
Original line number Diff line number Diff line
@@ -210,12 +210,12 @@ public class PluginActionManager<T extends Plugin> {
    private void onPluginConnected(PluginInstance<T> pluginInstance) {
        if (DEBUG) Log.d(TAG, "onPluginConnected");
        PluginPrefs.setHasPlugins(mContext);
        pluginInstance.onCreate(mContext, mListener);
        pluginInstance.onCreate();
    }

    private void onPluginDisconnected(PluginInstance<T> pluginInstance) {
        if (DEBUG) Log.d(TAG, "onPluginDisconnected");
        pluginInstance.onDestroy(mListener);
        pluginInstance.onDestroy();
    }

    private void queryAll() {
@@ -312,7 +312,7 @@ public class PluginActionManager<T extends Plugin> {
            try {
                return mPluginInstanceFactory.create(
                        mContext, appInfo, component,
                        mPluginClass);
                        mPluginClass, mListener);
            } catch (InvalidVersionException e) {
                reportInvalidVersion(component, component.getClassName(), e);
            }
+171 −35

File changed.

Preview size limit exceeded, changes collapsed.

Loading