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

Commit 33788ae6 authored by Andre Le's avatar Andre Le
Browse files

QSDetailedView: Add getDetailsViewModel to ScreenRecordTile

Create a new details view model called ScreenRecordDetailsViewModel and
add it to ScreenRecordTile so that the screen record details view is
shown when clicking on the tile (if the flag is enabled). This model
class will be further developed in later CLs.

The functionality to dismiss the dialog should be in the DialogDelegate,
rather than in the view binder, since the detailed view will not dismiss
the dialog when start recording button is clicked. Thus, this CL extract
the part to dismiss dialog out and move it back to the DialogDelegate.

This CL follows the design mentioned in
go/al-screen-record-detailed-view.

Bug: b/378514312
Flag: com.android.systemui.qs_tile_detailed_view
Test: ScreenRecordTileTest
Test: Click on screen record tile in the QS -> verify that detailed
view is shown.

Change-Id: Iaeed00e3ab2678489926d5555b7152e84bae3063
parent 0fd6b40d
Loading
Loading
Loading
Loading
+31 −3
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import static org.mockito.Mockito.when;
import android.app.Dialog;
import android.media.projection.StopReason;
import android.os.Handler;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.FlagsParameterization;
import android.service.quicksettings.Tile;
import android.testing.TestableLooper;
@@ -52,6 +53,7 @@ import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.flags.QsDetailedView;
import com.android.systemui.qs.flags.QsInCompose;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
@@ -63,6 +65,7 @@ import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.statusbar.policy.KeyguardStateController;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -70,11 +73,11 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.List;

import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;

import java.util.List;

@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
@@ -82,7 +85,8 @@ public class ScreenRecordTileTest extends SysuiTestCase {

    @Parameters(name = "{0}")
    public static List<FlagsParameterization> getParams() {
        return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX);
        return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX,
                QsDetailedView.FLAG_NAME);
    }

    @Mock
@@ -336,6 +340,30 @@ public class ScreenRecordTileTest extends SysuiTestCase {
                .notifyPermissionRequestDisplayed(mContext.getUserId());
    }

    @Test
    @EnableFlags(QsDetailedView.FLAG_NAME)
    public void testNotStartingAndRecording_returnDetailsViewModel() {
        when(mController.isStarting()).thenReturn(false);
        when(mController.isRecording()).thenReturn(false);
        mTile.getDetailsViewModel(Assert::assertNotNull);
    }

    @Test
    @EnableFlags(QsDetailedView.FLAG_NAME)
    public void testStarting_notReturnDetailsViewModel() {
        when(mController.isStarting()).thenReturn(true);
        when(mController.isRecording()).thenReturn(false);
        mTile.getDetailsViewModel(Assert::assertNull);
    }

    @Test
    @EnableFlags(QsDetailedView.FLAG_NAME)
    public void testRecording_notReturnDetailsViewModel() {
        when(mController.isStarting()).thenReturn(false);
        when(mController.isRecording()).thenReturn(true);
        mTile.getDetailsViewModel(Assert::assertNull);
    }

    private QSTile.Icon createExpectedIcon(int resId) {
        if (QsInCompose.isEnabled()) {
            return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId);
+66 −44
Original line number Diff line number Diff line
@@ -41,12 +41,14 @@ import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.TileDetailsViewModel;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.QsEventLogger;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsViewModel;
import com.android.systemui.res.R;
import com.android.systemui.screenrecord.RecordingController;
import com.android.systemui.screenrecord.data.model.ScreenRecordModel;
@@ -54,6 +56,8 @@ import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.statusbar.policy.KeyguardStateController;

import java.util.function.Consumer;

import javax.inject.Inject;

/**
@@ -122,16 +126,77 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>

    @Override
    protected void handleClick(@Nullable Expandable expandable) {
        handleClick(() -> showDialog(expandable));
    }

    private void showDialog(@Nullable Expandable expandable) {
        final Dialog dialog = mController.createScreenRecordDialog(
                this::onStartRecordingClicked);

        executeWhenUnlockedKeyguard(() -> {
            // We animate from the touched view only if we are not on the keyguard, given that if we
            // are we will dismiss it which will also collapse the shade.
            boolean shouldAnimateFromExpandable =
                    expandable != null && !mKeyguardStateController.isShowing();

            if (shouldAnimateFromExpandable) {
                DialogTransitionAnimator.Controller controller =
                        expandable.dialogTransitionController(new DialogCuj(
                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
                                INTERACTION_JANK_TAG));
                if (controller != null) {
                    mDialogTransitionAnimator.show(dialog,
                            controller, /* animateBackgroundBoundsChange= */ true);
                } else {
                    dialog.show();
                }
            } else {
                dialog.show();
            }
        });
    }

    private void onStartRecordingClicked() {
        // We dismiss the shade. Since starting the recording will also dismiss the dialog (if
        // there is one showing), we disable the exit animation which looks weird when it happens
        // at the same time as the shade collapsing.
        mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations();
        mPanelInteractor.collapsePanels();
    }

    private void executeWhenUnlockedKeyguard(Runnable dismissActionCallback) {
        ActivityStarter.OnDismissAction dismissAction = () -> {
            dismissActionCallback.run();

            int uid = mUserContextProvider.getUserContext().getUserId();
            mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid);

            return false;
        };

        mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */,
                true /* afterKeyguardDone */);
    }

    private void handleClick(Runnable showPromptCallback) {
        if (mController.isStarting()) {
            cancelCountdown();
        } else if (mController.isRecording()) {
            stopRecording();
        } else {
            mUiHandler.post(() -> showPrompt(expandable));
            mUiHandler.post(showPromptCallback);
        }
        refreshState();
    }

    @Override
    public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) {
        handleClick(() ->
                callback.accept(new ScreenRecordDetailsViewModel())
        );
        return true;
    }

    @Override
    protected void handleUpdateState(BooleanState state, Object arg) {
        boolean isStarting = mController.isStarting();
@@ -178,49 +243,6 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
        return mContext.getString(R.string.quick_settings_screen_record_label);
    }

    private void showPrompt(@Nullable Expandable expandable) {
        // We animate from the touched view only if we are not on the keyguard, given that if we
        // are we will dismiss it which will also collapse the shade.
        boolean shouldAnimateFromExpandable =
                expandable != null && !mKeyguardStateController.isShowing();

        // Create the recording dialog that will collapse the shade only if we start the recording.
        Runnable onStartRecordingClicked = () -> {
            // We dismiss the shade. Since starting the recording will also dismiss the dialog, we
            // disable the exit animation which looks weird when it happens at the same time as the
            // shade collapsing.
            mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations();
            mPanelInteractor.collapsePanels();
        };

        final Dialog dialog = mController.createScreenRecordDialog(onStartRecordingClicked);

        ActivityStarter.OnDismissAction dismissAction = () -> {
            if (shouldAnimateFromExpandable) {
                DialogTransitionAnimator.Controller controller =
                        expandable.dialogTransitionController(new DialogCuj(
                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
                                INTERACTION_JANK_TAG));
                if (controller != null) {
                    mDialogTransitionAnimator.show(dialog,
                            controller, /* animateBackgroundBoundsChange= */ true);
                } else {
                    dialog.show();
                }
            } else {
                dialog.show();
            }

            int uid = mUserContextProvider.getUserContext().getUserId();
            mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid);

            return false;
        };

        mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */,
                true /* afterKeyguardDone */);
    }

    private void cancelCountdown() {
        Log.d(TAG, "Cancelling countdown");
        mController.cancelCountdown();
+61 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.qs.tiles.dialog

import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.res.R

/** The view model used for the screen record details view in the Quick Settings */
class ScreenRecordDetailsViewModel() : TileDetailsViewModel() {
    @Composable
    override fun GetContentView() {
        // TODO(b/378514312): Finish implementing this function.
        AndroidView(
            modifier = Modifier.fillMaxWidth().heightIn(max = VIEW_MAX_HEIGHT),
            factory = { context ->
                // Inflate with the existing dialog xml layout
                LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null)
            },
        )
    }

    override fun clickOnSettingsButton() {
        // No settings button in this tile.
    }

    override fun getTitle(): String {
        // TODO(b/388321032): Replace this string with a string in a translatable xml file,
        return "Screen recording"
    }

    override fun getSubTitle(): String {
        // No sub-title in this tile.
        return ""
    }

    companion object {
        private val VIEW_MAX_HEIGHT: Dp = 320.dp
    }
}