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

Commit bdbeef9e authored by Andre Le's avatar Andre Le Committed by Android (Google) Code Review
Browse files

Merge "QSDetailedView: Add getDetailsViewModel to ScreenRecordTile" into main

parents 84aa0ce4 33788ae6
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
    }
}