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

Commit 9590d058 authored by András Kurucz's avatar András Kurucz Committed by Android (Google) Code Review
Browse files

Merge "BigPicture lazy loading" into main

parents 1f81948b 51322d6b
Loading
Loading
Loading
Loading
+21 −0
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.view.ViewGroup;
import com.android.internal.util.NotificationMessagingUtil;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -38,6 +39,7 @@ import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.NotificationClicker;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.icon.IconManager;
import com.android.systemui.statusbar.notification.row.BigPictureIconManager;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
import com.android.systemui.statusbar.notification.row.NotifBindPipeline;
@@ -151,6 +153,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
                                component.getExpandableNotificationRowController();
                        rowController.init(entry);
                        entry.setRowController(rowController);
                        maybeSetBigPictureIconManager(row, component);
                        bindRow(entry, row);
                        updateRow(entry, row);
                        inflateContentViews(entry, params, row, callback);
@@ -165,6 +168,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
            return;
        }
        mLogger.logReleasingViews(entry);
        cancelRunningJobs(entry.getRow());
        final RowContentBindParams params = mRowContentBindStage.getStageParams(entry);
        params.markContentViewsFreeable(FLAG_CONTENT_VIEW_CONTRACTED);
        params.markContentViewsFreeable(FLAG_CONTENT_VIEW_EXPANDED);
@@ -172,6 +176,23 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
        mRowContentBindStage.requestRebind(entry, null);
    }

    private void maybeSetBigPictureIconManager(ExpandableNotificationRow row,
            ExpandableNotificationRowComponent component) {
        if (mFeatureFlags.isEnabled(Flags.BIGPICTURE_NOTIFICATION_LAZY_LOADING)) {
            row.setBigPictureIconManager(component.getBigPictureIconManager());
        }
    }

    private void cancelRunningJobs(ExpandableNotificationRow row) {
        if (row == null) {
            return;
        }
        BigPictureIconManager iconManager = row.getBigPictureIconManager();
        if (iconManager != null) {
            iconManager.cancelJobs();
        }
    }

    /**
     * Bind row to various controllers and managers. This is only called when the row is first
     * created.
+298 −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.statusbar.notification.row

import android.annotation.WorkerThread
import android.app.ActivityManager
import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.util.Dumpable
import android.util.Log
import android.util.Size
import com.android.internal.R
import com.android.internal.widget.NotificationDrawableConsumer
import com.android.internal.widget.NotificationIconManager
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.graphics.ImageLoader
import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.Empty
import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.FullImage
import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.PlaceHolder
import java.io.PrintWriter
import javax.inject.Inject
import kotlin.math.min
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

private const val TAG = "BigPicImageLoader"
private const val DEBUG = false
private const val FREE_IMAGE_DELAY_MS = 3000L

/**
 * A helper class for [com.android.internal.widget.BigPictureNotificationImageView] to lazy-load
 * images from SysUI. It replaces the placeholder image with the fully loaded one, and vica versa.
 *
 * TODO(b/283082473) move the logs to a [com.android.systemui.log.LogBuffer]
 */
@SuppressWarnings("DumpableNotRegistered")
class BigPictureIconManager
@Inject
constructor(
    private val context: Context,
    private val imageLoader: ImageLoader,
    @Application private val scope: CoroutineScope,
    @Main private val mainDispatcher: CoroutineDispatcher,
    @Background private val bgDispatcher: CoroutineDispatcher
) : NotificationIconManager, Dumpable {

    private var lastLoadingJob: Job? = null
    private var drawableConsumer: NotificationDrawableConsumer? = null
    private var displayedState: DrawableState = Empty(null)
    private var viewShown = false

    private var maxWidth = getMaxWidth()
    private var maxHeight = getMaxHeight()

    /**
     * Called when the displayed state changes of the view.
     *
     * @param shown true if the view is shown, and the image needs to be displayed.
     */
    fun onViewShown(shown: Boolean) {
        log("onViewShown:$shown")

        if (this.viewShown != shown) {
            this.viewShown = shown

            val state = displayedState

            this.lastLoadingJob?.cancel()
            this.lastLoadingJob =
                when {
                    state is Empty && shown -> state.icon?.let(::startLoadingJob)
                    state is PlaceHolder && shown -> startLoadingJob(state.icon)
                    state is FullImage && !shown ->
                        startFreeImageJob(state.icon, state.drawableSize)
                    else -> null
                }
        }
    }

    /**
     * Update the maximum width and height allowed for bitmaps, ex. after a configuration change.
     */
    fun updateMaxImageSizes() {
        log("updateMaxImageSizes")
        maxWidth = getMaxWidth()
        maxHeight = getMaxHeight()
    }

    /** Cancels all currently running jobs. */
    fun cancelJobs() {
        lastLoadingJob?.cancel()
    }

    @WorkerThread
    override fun updateIcon(drawableConsumer: NotificationDrawableConsumer, icon: Icon?): Runnable {
        if (this.drawableConsumer != null && this.drawableConsumer != drawableConsumer) {
            Log.wtf(TAG, "A consumer is already set for this iconManager.")
            return Runnable {}
        }

        if (displayedState.iconSameAs(icon)) {
            // We're already handling this icon, nothing to do here.
            log("skipping updateIcon for consumer:$drawableConsumer with icon:$icon")
            return Runnable {}
        }

        this.drawableConsumer = drawableConsumer
        this.displayedState = Empty(icon)
        this.lastLoadingJob?.cancel()

        val drawable = loadImageOrPlaceHolderSync(icon)

        log("icon updated")

        return Runnable { drawableConsumer.setImageDrawable(drawable) }
    }

    override fun dump(pw: PrintWriter, args: Array<out String>?) {
        pw.println("BigPictureIconManager ${getDebugString()}")
    }

    @WorkerThread
    private fun loadImageOrPlaceHolderSync(icon: Icon?): Drawable? {
        icon ?: return null

        if (viewShown) {
            return loadImageSync(icon)
        }

        return loadPlaceHolderSync(icon) ?: loadImageSync(icon)
    }

    @WorkerThread
    private fun loadImageSync(icon: Icon): Drawable? {
        return imageLoader.loadDrawableSync(icon, context, maxWidth, maxHeight)?.also { drawable ->
            checkPlaceHolderSizeForDrawable(this.displayedState, drawable)
            this.displayedState = FullImage(icon, drawable.intrinsicSize)
        }
    }

    private fun checkPlaceHolderSizeForDrawable(
        displayedState: DrawableState,
        newDrawable: Drawable
    ) {
        if (displayedState is PlaceHolder) {
            val (oldWidth, oldHeight) = displayedState.drawableSize
            val (newWidth, newHeight) = newDrawable.intrinsicSize

            if (oldWidth != newWidth || oldHeight != newHeight) {
                Log.e(
                    TAG,
                    "Mismatch in dimensions, when replacing PlaceHolder " +
                        "$oldWidth X $oldHeight with Drawable $newWidth X $newHeight."
                )
            }
        }
    }

    @WorkerThread
    private fun loadPlaceHolderSync(icon: Icon): Drawable? {
        return imageLoader
            .loadSizeSync(icon, context)
            ?.resizeToMax(maxWidth, maxHeight) // match the dimensions of the fully loaded drawable
            ?.let { size -> createPlaceHolder(size) }
            ?.also { drawable -> this.displayedState = PlaceHolder(icon, drawable.intrinsicSize) }
    }

    private fun startLoadingJob(icon: Icon): Job =
        scope.launch {
            val drawable = withContext(bgDispatcher) { loadImageSync(icon) }
            withContext(mainDispatcher) { drawableConsumer?.setImageDrawable(drawable) }
            log("image loaded")
        }

    private fun startFreeImageJob(icon: Icon, drawableSize: Size): Job =
        scope.launch {
            delay(FREE_IMAGE_DELAY_MS)
            val drawable = createPlaceHolder(drawableSize)
            displayedState = PlaceHolder(icon, drawable.intrinsicSize)
            withContext(mainDispatcher) { drawableConsumer?.setImageDrawable(drawable) }
            log("placeholder loaded")
        }

    private fun createPlaceHolder(size: Size): Drawable {
        return PlaceHolderDrawable(width = size.width, height = size.height)
    }

    private fun isLowRam(): Boolean {
        return ActivityManager.isLowRamDeviceStatic()
    }

    private fun getMaxWidth() =
        context.resources.getDimensionPixelSize(
            if (isLowRam()) {
                R.dimen.notification_big_picture_max_width_low_ram
            } else {
                R.dimen.notification_big_picture_max_width
            }
        )

    private fun getMaxHeight() =
        context.resources.getDimensionPixelSize(
            if (isLowRam()) {
                R.dimen.notification_big_picture_max_height_low_ram
            } else {
                R.dimen.notification_big_picture_max_height
            }
        )

    private fun log(msg: String) {
        if (DEBUG) {
            Log.d(TAG, "$msg state=${getDebugString()}")
        }
    }

    private fun getDebugString() =
        "{ state:$displayedState, hasConsumer:${drawableConsumer != null}, viewShown:$viewShown}"

    private sealed class DrawableState(open val icon: Icon?) {
        data class Empty(override val icon: Icon?) : DrawableState(icon)
        data class PlaceHolder(override val icon: Icon, val drawableSize: Size) :
            DrawableState(icon)
        data class FullImage(override val icon: Icon, val drawableSize: Size) : DrawableState(icon)

        fun iconSameAs(other: Icon?): Boolean {
            val displayedIcon = icon
            return when {
                displayedIcon == null && other == null -> true
                displayedIcon != null && other != null -> displayedIcon.sameAs(other)
                else -> false
            }
        }
    }
}

/**
 * @return an image size that conforms to the maxWidth / maxHeight parameters. It can be the same
 *   instance, if the provided size was already small enough.
 */
private fun Size.resizeToMax(maxWidth: Int, maxHeight: Int): Size {
    if (width <= maxWidth && height <= maxHeight) {
        return this
    }

    // Calculate the scale factor for both dimensions
    val wScale =
        if (maxWidth <= 0) {
            1.0f
        } else {
            maxWidth.toFloat() / width.toFloat()
        }

    val hScale =
        if (maxHeight <= 0) {
            1.0f
        } else {
            maxHeight.toFloat() / height.toFloat()
        }

    // Scale down to the smaller scale factor
    val scale = min(wScale, hScale)
    if (scale < 1.0f) {
        val targetWidth = (width * scale).toInt()
        val targetHeight = (height * scale).toInt()

        return Size(targetWidth, targetHeight)
    }

    return this
}

private val Drawable.intrinsicSize
    get() = Size(/*width=*/ intrinsicWidth, /*height=*/ intrinsicHeight)

private operator fun Size.component1() = width

private operator fun Size.component2() = height
+52 −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.statusbar.notification.row

import android.content.Context
import android.util.AttributeSet
import android.view.View
import com.android.internal.widget.BigPictureNotificationImageView
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
import javax.inject.Inject

class BigPictureLayoutInflaterFactory @Inject constructor() : NotifRemoteViewsFactory {

    override fun instantiate(
        row: ExpandableNotificationRow,
        @InflationFlag layoutType: Int,
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
    ): View? {
        // Currently the [BigPictureIconManager] only handles one view per notification.
        // Exclude other layout types for now, to make sure that we set the same iconManager
        // on only one [BigPictureNotificationImageView].
        if (layoutType != FLAG_CONTENT_VIEW_EXPANDED) {
            return null
        }

        return when (name) {
            BigPictureNotificationImageView::class.java.name ->
                BigPictureNotificationImageView(context, attrs).also { view ->
                    view.setIconManager(row.bigPictureIconManager)
                }
            else -> null
        }
    }
}
+17 −0
Original line number Diff line number Diff line
@@ -376,6 +376,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
    private float mTranslationWhenRemoved;
    private boolean mWasChildInGroupWhenRemoved;
    private NotificationInlineImageResolver mImageResolver;
    private BigPictureIconManager mBigPictureIconManager;
    @Nullable
    private OnExpansionChangedListener mExpansionChangedListener;
    @Nullable
@@ -1355,6 +1356,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        if (mImageResolver != null) {
            mImageResolver.updateMaxImageSizes();
        }
        if (mBigPictureIconManager != null) {
            mBigPictureIconManager.updateMaxImageSizes();
        }
    }

    public void onUiModeChanged() {
@@ -1794,6 +1798,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        return mImageResolver;
    }

    public BigPictureIconManager getBigPictureIconManager() {
        return mBigPictureIconManager;
    }

    public void setBigPictureIconManager(
            BigPictureIconManager bigPictureIconManager) {
        mBigPictureIconManager = bigPictureIconManager;
    }


    /**
     * Resets this view so it can be re-used for an updated notification.
     */
@@ -3687,6 +3701,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
                pw.println("no viewState!!!");
            }
            pw.println(getRoundableState().debugString());
            if (mBigPictureIconManager != null) {
                mBigPictureIconManager.dump(pw, args);
            }
            dumpBackgroundView(pw, args);

            int transientViewCount = mChildrenContainer == null
+5 −1
Original line number Diff line number Diff line
@@ -60,12 +60,16 @@ public abstract class NotificationRowModule {
    @Named(NOTIF_REMOTEVIEWS_FACTORIES)
    static Set<NotifRemoteViewsFactory> provideNotifRemoteViewsFactories(
            FeatureFlags featureFlags,
            PrecomputedTextViewFactory precomputedTextViewFactory
            PrecomputedTextViewFactory precomputedTextViewFactory,
            BigPictureLayoutInflaterFactory bigPictureLayoutInflaterFactory
    ) {
        final Set<NotifRemoteViewsFactory> replacementFactories = new HashSet<>();
        if (featureFlags.isEnabled(Flags.PRECOMPUTED_TEXT)) {
            replacementFactories.add(precomputedTextViewFactory);
        }
        if (featureFlags.isEnabled(Flags.BIGPICTURE_NOTIFICATION_LAZY_LOADING)) {
            replacementFactories.add(bigPictureLayoutInflaterFactory);
        }
        return replacementFactories;
    }
}
Loading