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

Commit 4f6344ba authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

Do not retain album art in NotificationMediaManager if unused

Album artwork is only needed here for lockscreen wallpaper, which is no longer
used by default.

- Update CentralSurfces SHOW_LOCKSCREEN_MEDIA_ARTWORK to be accurate
- If device does not use media lockscreen artwork: instead of saving the
  metadata as is, save a copy without artwork info, so we don't retain
  potentially large bitmaps in memory.

Bug: 278115559
Test: compare hprof - NotificationMediaManager memory reduced significantly
Test: atest NotificationMediaManagerTest
Test: manual - normal usage of media player unchanged
Change-Id: I2e203f581ad594a60bc3eec6f0782215c55b2ea5
parent cf2fc89d
Loading
Loading
Loading
Loading
+43 −5
Original line number Original line Diff line number Diff line
@@ -71,6 +71,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.Utils;
import com.android.systemui.util.Utils;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.concurrency.DelayableExecutor;


import dagger.Lazy;

import java.io.PrintWriter;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.ArrayList;
@@ -80,8 +82,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Optional;
import java.util.Set;
import java.util.Set;


import dagger.Lazy;

/**
/**
 * Handles tasks and state related to media notifications. For example, there is a 'current' media
 * Handles tasks and state related to media notifications. For example, there is a 'current' media
 * notification, which this class keeps track of.
 * notification, which this class keeps track of.
@@ -161,11 +161,48 @@ public class NotificationMediaManager implements Dumpable {
                Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata);
                Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata);
            }
            }
            mMediaArtworkProcessor.clearCache();
            mMediaArtworkProcessor.clearCache();
            mMediaMetadata = metadata;
            mMediaMetadata = cleanMetadata(metadata);
            dispatchUpdateMediaMetaData(true /* changed */, true /* allowAnimation */);
            dispatchUpdateMediaMetaData(true /* changed */, true /* allowAnimation */);
        }
        }
    };
    };


    /**
     * If this build is not configured for lockscreen artwork, clear artwork references from the
     * metadata to avoid excess memory usage. Otherwise, return as is.
     * @param data Original metadata
     * @return a copy without artwork data, or original
     */
    private MediaMetadata cleanMetadata(MediaMetadata data) {
        if (SHOW_LOCKSCREEN_MEDIA_ARTWORK) {
            return data;
        }
        if (data == null) {
            return null;
        }
        if (DEBUG_MEDIA) {
            String[] artKeys = new String[] {
                MediaMetadata.METADATA_KEY_ART,
                MediaMetadata.METADATA_KEY_ALBUM_ART,
                MediaMetadata.METADATA_KEY_DISPLAY_ICON,
                MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
                MediaMetadata.METADATA_KEY_ART_URI,
                MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
            };
            Log.v(TAG, "DEBUG_MEDIA: removing artwork from metadata");
            for (String key: artKeys) {
                Log.v(TAG, "  " + key + ": " + data.containsKey(key));
            }
        }
        return new MediaMetadata.Builder(data)
                .putBitmap(MediaMetadata.METADATA_KEY_ART, null)
                .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null)
                .putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, null)
                .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, null)
                .putString(MediaMetadata.METADATA_KEY_ART_URI, null)
                .putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, null)
                .build();
    }

    /**
    /**
     * Injected constructor. See {@link CentralSurfacesModule}.
     * Injected constructor. See {@link CentralSurfacesModule}.
     */
     */
@@ -313,6 +350,7 @@ public class NotificationMediaManager implements Dumpable {
        return mMediaNotificationKey;
        return mMediaNotificationKey;
    }
    }


    @VisibleForTesting
    public MediaMetadata getMediaMetadata() {
    public MediaMetadata getMediaMetadata() {
        return mMediaMetadata;
        return mMediaMetadata;
    }
    }
@@ -350,7 +388,7 @@ public class NotificationMediaManager implements Dumpable {
     * update this manager's internal state.
     * update this manager's internal state.
     * @return whether the current MediaMetadata changed (and needs to be announced to listeners).
     * @return whether the current MediaMetadata changed (and needs to be announced to listeners).
     */
     */
    boolean findPlayingMediaNotification(
    private boolean findPlayingMediaNotification(
            @NonNull Collection<NotificationEntry> allNotifications) {
            @NonNull Collection<NotificationEntry> allNotifications) {
        boolean metaDataChanged = false;
        boolean metaDataChanged = false;
        // Promote the media notification with a controller in 'playing' state, if any.
        // Promote the media notification with a controller in 'playing' state, if any.
@@ -383,7 +421,7 @@ public class NotificationMediaManager implements Dumpable {
            clearCurrentMediaNotificationSession();
            clearCurrentMediaNotificationSession();
            mMediaController = controller;
            mMediaController = controller;
            mMediaController.registerCallback(mMediaListener);
            mMediaController.registerCallback(mMediaListener);
            mMediaMetadata = mMediaController.getMetadata();
            mMediaMetadata = cleanMetadata(mMediaController.getMetadata());
            if (DEBUG_MEDIA) {
            if (DEBUG_MEDIA) {
                Log.v(TAG, "DEBUG_MEDIA: insert listener, found new controller: "
                Log.v(TAG, "DEBUG_MEDIA: insert listener, found new controller: "
                        + mMediaController + ", receive metadata: " + mMediaMetadata);
                        + mMediaController + ", receive metadata: " + mMediaMetadata);
+1 −1
Original line number Original line Diff line number Diff line
@@ -75,7 +75,7 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn
    boolean DEBUG_WAKEUP_DELAY = Compile.IS_DEBUG;
    boolean DEBUG_WAKEUP_DELAY = Compile.IS_DEBUG;
    // additional instrumentation for testing purposes; intended to be left on during development
    // additional instrumentation for testing purposes; intended to be left on during development
    boolean CHATTY = DEBUG;
    boolean CHATTY = DEBUG;
    boolean SHOW_LOCKSCREEN_MEDIA_ARTWORK = true;
    boolean SHOW_LOCKSCREEN_MEDIA_ARTWORK = false;
    String ACTION_FAKE_ARTWORK = "fake_artwork";
    String ACTION_FAKE_ARTWORK = "fake_artwork";
    int FADE_KEYGUARD_START_DELAY = 100;
    int FADE_KEYGUARD_START_DELAY = 100;
    int FADE_KEYGUARD_DURATION = 300;
    int FADE_KEYGUARD_DURATION = 300;
+122 −17
Original line number Original line Diff line number Diff line
@@ -16,11 +16,34 @@


package com.android.systemui.statusbar
package com.android.systemui.statusbar


import android.app.Notification
import android.app.WallpaperManager
import android.graphics.Bitmap
import android.media.MediaMetadata
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.testing.AndroidTestingRunner
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestCase
import com.android.systemui.colorextraction.SysuiColorExtractor
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.pipeline.MediaDataManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.notification.collection.NotifCollection
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.whenever
import org.junit.After
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import dagger.Lazy
import java.util.Optional
import org.junit.Before
import org.junit.Before
import org.junit.Test
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runner.RunWith
@@ -32,39 +55,121 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.MockitoAnnotations


/**
 * Temporary test for the lock screen live wallpaper project.
 *
 * TODO(b/273443374): remove this test
 */
@RunWith(AndroidTestingRunner::class)
@RunWith(AndroidTestingRunner::class)
@SmallTest
@SmallTest
@RunWithLooper
class NotificationMediaManagerTest : SysuiTestCase() {
class NotificationMediaManagerTest : SysuiTestCase() {


    @Mock private lateinit var notificationMediaManager: NotificationMediaManager
    @Mock private lateinit var centralSurfaces: CentralSurfaces
    @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
    @Mock private lateinit var visibilityProvider: NotificationVisibilityProvider
    @Mock private lateinit var mediaArtworkProcessor: MediaArtworkProcessor
    @Mock private lateinit var keyguardBypassController: KeyguardBypassController
    @Mock private lateinit var notifPipeline: NotifPipeline
    @Mock private lateinit var notifCollection: NotifCollection
    @Mock private lateinit var mediaDataManager: MediaDataManager
    @Mock private lateinit var statusBarStateController: StatusBarStateController
    @Mock private lateinit var colorExtractor: SysuiColorExtractor
    @Mock private lateinit var keyguardStateController: KeyguardStateController
    @Mock private lateinit var dumpManager: DumpManager
    @Mock private lateinit var wallpaperManager: WallpaperManager

    @Mock private lateinit var notificationEntry: NotificationEntry


    lateinit var manager: NotificationMediaManager
    val clock = FakeSystemClock()
    val mainExecutor: DelayableExecutor = FakeExecutor(clock)

    @Mock private lateinit var mockManager: NotificationMediaManager
    @Mock private lateinit var mockBackDropView: BackDropView
    @Mock private lateinit var mockBackDropView: BackDropView


    @Before
    @Before
    fun setUp() {
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        MockitoAnnotations.initMocks(this)
        doCallRealMethod()
        doCallRealMethod().whenever(mockManager).updateMediaMetaData(anyBoolean(), anyBoolean())
            .whenever(notificationMediaManager)
        doReturn(mockBackDropView).whenever(mockManager).backDropView
            .updateMediaMetaData(anyBoolean(), anyBoolean())
        doReturn(mockBackDropView).whenever(notificationMediaManager).backDropView
    }


    @After fun tearDown() {}
        manager =
            NotificationMediaManager(
                context,
                Lazy { Optional.of(centralSurfaces) },
                Lazy { notificationShadeWindowController },
                visibilityProvider,
                mediaArtworkProcessor,
                keyguardBypassController,
                notifPipeline,
                notifCollection,
                mainExecutor,
                mediaDataManager,
                statusBarStateController,
                colorExtractor,
                keyguardStateController,
                dumpManager,
                wallpaperManager,
            )
    }


    /** Check that updateMediaMetaData is a no-op with mIsLockscreenLiveWallpaperEnabled = true */
    /**
     * Check that updateMediaMetaData is a no-op with mIsLockscreenLiveWallpaperEnabled = true
     * Temporary test for the lock screen live wallpaper project.
     *
     * TODO(b/273443374): remove this test
     */
    @Test
    @Test
    fun testUpdateMediaMetaDataDisabled() {
    fun testUpdateMediaMetaDataDisabled() {
        notificationMediaManager.mIsLockscreenLiveWallpaperEnabled = true
        mockManager.mIsLockscreenLiveWallpaperEnabled = true
        for (metaDataChanged in listOf(true, false)) {
        for (metaDataChanged in listOf(true, false)) {
            for (allowEnterAnimation in listOf(true, false)) {
            for (allowEnterAnimation in listOf(true, false)) {
                notificationMediaManager.updateMediaMetaData(metaDataChanged, allowEnterAnimation)
                mockManager.updateMediaMetaData(metaDataChanged, allowEnterAnimation)
                verify(notificationMediaManager, never()).mediaMetadata
                verify(mockManager, never()).mediaMetadata
            }
            }
        }
        }
    }
    }

    @Test
    fun testMetadataUpdated_doesNotRetainArtwork() {
        val artBmp = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
        val artUri = "content://example"
        val inputMetadata =
            MediaMetadata.Builder()
                .putBitmap(MediaMetadata.METADATA_KEY_ART, artBmp)
                .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, artBmp)
                .putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, artBmp)
                .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, artUri)
                .putString(MediaMetadata.METADATA_KEY_ART_URI, artUri)
                .putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, artUri)
                .build()

        // Create a playing media notification
        val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
        val session = MediaSession(context, "NotificationMediaManagerTest")
        session.setMetadata(inputMetadata)
        session.setPlaybackState(state)
        val sbn =
            SbnBuilder().run {
                modifyNotification(context).also {
                    it.setSmallIcon(android.R.drawable.ic_media_play)
                    it.setStyle(
                        Notification.MediaStyle().apply { setMediaSession(session.sessionToken) }
                    )
                }
                build()
            }
        whenever(notificationEntry.sbn).thenReturn(sbn)
        val collection = ArrayList<NotificationEntry>()
        collection.add(notificationEntry)
        whenever(notifPipeline.allNotifs).thenReturn(collection)

        // Trigger update in NotificationMediaManager
        manager.findAndUpdateMediaNotifications()

        // Verify that there is no artwork data retained
        val metadata = manager.mediaMetadata
        assertThat(metadata.getBitmap(MediaMetadata.METADATA_KEY_ART)).isNull()
        assertThat(metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)).isNull()
        assertThat(metadata.getBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON)).isNull()
        assertThat(metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI)).isNull()
        assertThat(metadata.getString(MediaMetadata.METADATA_KEY_ART_URI)).isNull()
        assertThat(metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI)).isNull()
    }
}
}