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

Commit ac9b36dc authored by Jainam Shah's avatar Jainam Shah
Browse files

Add WindowDecorations for Car form factor

Bug: 370104463
Test: manual
Flag: com.android.systemui.car.display_compatibility_caption_bar
Change-Id: I5e0c78f88da9dede5949d59d4cb73fbf2b75f720
parent 4de0b1b8
Loading
Loading
Loading
Loading
+263 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.wm.shell.windowdecor;

import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.SurfaceControl;
import android.view.View;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;

import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
import com.android.wm.shell.shared.FocusTransitionListener;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;

/**
 * Works with decorations that extend {@link CarWindowDecoration}.
 */
public abstract class CarWindowDecorViewModel
        implements WindowDecorViewModel, FocusTransitionListener {
    private static final String TAG = "CarWindowDecorViewModel";

    private final ShellTaskOrganizer mTaskOrganizer;
    private final Context mContext;
    private final @ShellBackgroundThread ShellExecutor mBgExecutor;
    private final ShellExecutor mMainExecutor;
    private final DisplayController mDisplayController;
    private final FocusTransitionObserver mFocusTransitionObserver;
    private final SyncTransactionQueue mSyncQueue;
    private final SparseArray<CarWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
    private final WindowDecorViewHostSupplier<WindowDecorViewHost> mWindowDecorViewHostSupplier;

    public CarWindowDecorViewModel(
            Context context,
            @ShellBackgroundThread ShellExecutor bgExecutor,
            @ShellMainThread ShellExecutor shellExecutor,
            ShellInit shellInit,
            ShellTaskOrganizer taskOrganizer,
            DisplayController displayController,
            SyncTransactionQueue syncQueue,
            FocusTransitionObserver focusTransitionObserver,
            WindowDecorViewHostSupplier<WindowDecorViewHost> windowDecorViewHostSupplier) {
        mContext = context;
        mMainExecutor = shellExecutor;
        mBgExecutor = bgExecutor;
        mTaskOrganizer = taskOrganizer;
        mDisplayController = displayController;
        mFocusTransitionObserver = focusTransitionObserver;
        mSyncQueue = syncQueue;
        mWindowDecorViewHostSupplier = windowDecorViewHostSupplier;

        shellInit.addInitCallback(this::onInit, this);
    }

    private void onInit() {
        mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor);
    }

    @Override
    public void onFocusedTaskChanged(int taskId, boolean isFocusedOnDisplay,
            boolean isFocusedGlobally) {
        // no-op
    }

    @Override
    public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
        // no-op
    }

    @Override
    public void setSplitScreenController(SplitScreenController splitScreenController) {
        // no-op
    }

    @Override
    public boolean onTaskOpening(
            RunningTaskInfo taskInfo,
            SurfaceControl taskSurface,
            SurfaceControl.Transaction startT,
            SurfaceControl.Transaction finishT) {
        if (!shouldShowWindowDecor(taskInfo)) {
            return false;
        }
        createWindowDecoration(taskInfo, taskSurface, startT, finishT);
        return true;
    }

    @Override
    public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
        final CarWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);

        if (decoration == null) {
            return;
        }

        if (!shouldShowWindowDecor(taskInfo)) {
            destroyWindowDecoration(taskInfo);
            return;
        }

        decoration.relayout(taskInfo, decoration.mHasGlobalFocus, decoration.mExclusionRegion);
    }

    @Override
    public void onTaskVanished(RunningTaskInfo taskInfo) {
        // A task vanishing doesn't necessarily mean the task was closed, it could also mean its
        // windowing mode changed. We're only interested in closing tasks so checking whether
        // its info still exists in the task organizer is one way to disambiguate.
        final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null;
        if (closed) {
            // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition
            // changes happen, but there are certain cases in which closing tasks aren't included
            // in transitions, such as when a non-visible task is closed. See b/296921167.
            // Destroy the decoration here in case the lack of transition missed it.
            destroyWindowDecoration(taskInfo);
        }
    }

    @Override
    public void onTaskChanging(
            RunningTaskInfo taskInfo,
            SurfaceControl taskSurface,
            SurfaceControl.Transaction startT,
            SurfaceControl.Transaction finishT) {
        final CarWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);

        if (!shouldShowWindowDecor(taskInfo)) {
            if (decoration != null) {
                destroyWindowDecoration(taskInfo);
            }
            return;
        }

        if (decoration == null) {
            createWindowDecoration(taskInfo, taskSurface, startT, finishT);
        } else {
            decoration.relayout(taskInfo, startT, finishT);
        }
    }

    @Override
    public void onTaskClosing(
            RunningTaskInfo taskInfo,
            SurfaceControl.Transaction startT,
            SurfaceControl.Transaction finishT) {
        final CarWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
        if (decoration == null) {
            return;
        }
        decoration.relayout(taskInfo, startT, finishT);
    }

    @Override
    public void destroyWindowDecoration(RunningTaskInfo taskInfo) {
        final CarWindowDecoration decoration =
                mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId);
        if (decoration == null) {
            return;
        }

        decoration.close();
    }

    /**
     * @return {@code true} if the task/activity associated with {@code taskInfo} should show
     * window decoration.
     */
    protected abstract boolean shouldShowWindowDecor(RunningTaskInfo taskInfo);

    private void createWindowDecoration(
            RunningTaskInfo taskInfo,
            SurfaceControl taskSurface,
            SurfaceControl.Transaction startT,
            SurfaceControl.Transaction finishT) {
        final CarWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId);
        if (oldDecoration != null) {
            // close the old decoration if it exists to avoid two window decorations being added
            oldDecoration.close();
        }
        final CarWindowDecoration windowDecoration =
                new CarWindowDecoration(
                        mContext,
                        mContext.createContextAsUser(UserHandle.of(taskInfo.userId), 0 /* flags */),
                        mDisplayController,
                        mTaskOrganizer,
                        taskInfo,
                        taskSurface,
                        mBgExecutor,
                        mWindowDecorViewHostSupplier,
                        new ButtonClickListener(taskInfo));
        mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
        windowDecoration.relayout(taskInfo, startT, finishT);
    }

    private class ButtonClickListener implements View.OnClickListener {
        private final WindowContainerToken mTaskToken;
        private final int mDisplayId;

        private ButtonClickListener(RunningTaskInfo taskInfo) {
            mTaskToken = taskInfo.token;
            mDisplayId = taskInfo.displayId;
        }

        @Override
        public void onClick(View v) {
            final int id = v.getId();
            if (id == R.id.close_window) {
                WindowContainerTransaction wct = new WindowContainerTransaction();
                wct.removeTask(mTaskToken);
                mSyncQueue.queue(wct);
            } else if (id == R.id.back_button) {
                sendBackEvent(KeyEvent.ACTION_DOWN, mDisplayId);
                sendBackEvent(KeyEvent.ACTION_UP, mDisplayId);
            }
        }

        private void sendBackEvent(int action, int displayId) {
            final long when = SystemClock.uptimeMillis();
            final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK,
                    0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD,
                    0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
                    InputDevice.SOURCE_KEYBOARD);

            ev.setDisplayId(displayId);
            if (!mContext.getSystemService(InputManager.class)
                    .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
                Log.e(TAG, "Inject input event fail");
            }
        }
    }
}
+137 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.wm.shell.windowdecor;

import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;

import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.Region;
import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.View;
import android.window.WindowContainerTransaction;

import androidx.annotation.NonNull;

import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;

/**
 * {@link WindowDecoration} to show app controls for windows on automotive.
 */
public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
    private WindowDecorLinearLayout mRootView;
    private @ShellBackgroundThread final ShellExecutor mBgExecutor;
    private final View.OnClickListener mClickListener;

    CarWindowDecoration(
            Context context,
            @android.annotation.NonNull Context userContext,
            DisplayController displayController,
            ShellTaskOrganizer taskOrganizer,
            ActivityManager.RunningTaskInfo taskInfo,
            SurfaceControl taskSurface,
            @ShellBackgroundThread ShellExecutor bgExecutor,
            WindowDecorViewHostSupplier<WindowDecorViewHost> windowDecorViewHostSupplier,
            View.OnClickListener clickListener) {
        super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
                windowDecorViewHostSupplier);
        mBgExecutor = bgExecutor;
        mClickListener = clickListener;
    }

    @Override
    void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus,
            @NonNull Region displayExclusionRegion) {
        final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
        relayout(taskInfo, t, t);
    }

    @SuppressLint("MissingPermission")
    void relayout(ActivityManager.RunningTaskInfo taskInfo,
            SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
        final WindowContainerTransaction wct = new WindowContainerTransaction();

        RelayoutParams relayoutParams = new RelayoutParams();
        RelayoutResult<WindowDecorLinearLayout> outResult = new RelayoutResult<>();

        updateRelayoutParams(relayoutParams, taskInfo,
                mDisplayController.getInsetsState(taskInfo.displayId));

        relayout(relayoutParams, startT, finishT, wct, mRootView, outResult);
        // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
        mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct));

        if (outResult.mRootView == null) {
            // This means something blocks the window decor from showing, e.g. the task is hidden.
            // Nothing is set up in this case including the decoration surface.
            return;
        }
        if (mRootView != outResult.mRootView) {
            mRootView = outResult.mRootView;
            setupRootView(outResult.mRootView, mClickListener);
        }
    }

    @Override
    @NonNull
    Rect calculateValidDragArea() {
        return new Rect();
    }

    @Override
    int getCaptionViewId() {
        return R.id.caption;
    }

    private void updateRelayoutParams(
            RelayoutParams relayoutParams,
            ActivityManager.RunningTaskInfo taskInfo,
            InsetsState displayInsetsState) {
        relayoutParams.reset();
        relayoutParams.mRunningTaskInfo = taskInfo;
        // todo(b/382071404): update to car specific UI
        relayoutParams.mLayoutResId = R.layout.caption_window_decor;
        relayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
        relayoutParams.mIsCaptionVisible = mIsStatusBarVisible && !mIsKeyguardVisibleAndOccluded;
        relayoutParams.mCaptionTopPadding = 0;
        relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
        relayoutParams.mApplyStartTransactionOnDraw = true;
    }

    /**
     * Sets up listeners when a new root view is created.
     */
    private void setupRootView(View rootView, View.OnClickListener onClickListener) {
        final View caption = rootView.findViewById(R.id.caption);
        final View close = caption.findViewById(R.id.close_window);
        if (close != null) {
            close.setOnClickListener(onClickListener);
        }
        final View back = caption.findViewById(R.id.back_button);
        if (back != null) {
            back.setOnClickListener(onClickListener);
        }
    }
}