| /* |
| * Copyright (C) 2022 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.car.portraitlauncher.homeactivities; |
| |
| import static android.view.InsetsState.ITYPE_BOTTOM_GENERIC_OVERLAY; |
| import static android.view.InsetsState.ITYPE_TOP_GENERIC_OVERLAY; |
| import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; |
| import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; |
| |
| import static com.android.car.caruiportrait.common.service.CarUiPortraitService.MSG_APP_GRID_VISIBILITY_CHANGE; |
| import static com.android.car.caruiportrait.common.service.CarUiPortraitService.MSG_FG_TASK_VIEW_READY; |
| import static com.android.car.caruiportrait.common.service.CarUiPortraitService.MSG_HIDE_SYSTEM_BAR_FOR_IMMERSIVE; |
| import static com.android.car.caruiportrait.common.service.CarUiPortraitService.MSG_IMMERSIVE_MODE_REQUESTED; |
| import static com.android.car.caruiportrait.common.service.CarUiPortraitService.MSG_REGISTER_CLIENT; |
| import static com.android.car.caruiportrait.common.service.CarUiPortraitService.MSG_SUW_IN_PROGRESS; |
| import static com.android.car.caruiportrait.common.service.CarUiPortraitService.MSG_UNREGISTER_CLIENT; |
| |
| import android.annotation.Nullable; |
| import android.app.ActivityManager; |
| import android.app.TaskInfo; |
| import android.app.TaskStackListener; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.content.res.Configuration; |
| import android.graphics.Color; |
| import android.graphics.Insets; |
| import android.graphics.Rect; |
| import android.graphics.Region; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Message; |
| import android.os.Messenger; |
| import android.os.RemoteException; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.WindowManager; |
| import android.widget.FrameLayout; |
| |
| import androidx.annotation.NonNull; |
| import androidx.core.view.WindowCompat; |
| import androidx.fragment.app.FragmentActivity; |
| import androidx.fragment.app.FragmentTransaction; |
| import androidx.lifecycle.ViewModelProvider; |
| |
| import com.android.car.carlauncher.CarLauncher; |
| import com.android.car.carlauncher.CarLauncherUtils; |
| import com.android.car.carlauncher.CarTaskView; |
| import com.android.car.carlauncher.ControlledCarTaskViewCallbacks; |
| import com.android.car.carlauncher.ControlledCarTaskViewConfig; |
| import com.android.car.carlauncher.LaunchRootCarTaskViewCallbacks; |
| import com.android.car.carlauncher.SemiControlledCarTaskViewCallbacks; |
| import com.android.car.carlauncher.TaskViewManager; |
| import com.android.car.carlauncher.homescreen.HomeCardModule; |
| import com.android.car.carlauncher.taskstack.TaskStackChangeListeners; |
| import com.android.car.caruiportrait.common.service.CarUiPortraitService; |
| import com.android.car.portraitlauncher.R; |
| import com.android.car.portraitlauncher.panel.TaskViewPanel; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * This home screen has maps running in the background hosted in a TaskView. At the bottom, there |
| * is a control bar view that will display information regarding media/dialer. Any other |
| * application other than assistant voice activity will be launched in the rootTaskView that is |
| * displayed in the lower half of the screen. |
| * |
| * —-------------------------------------------------------------------- |
| * | Status Bar | |
| * —-------------------------------------------------------------------- |
| * | | |
| * | | |
| * | | |
| * | Navigation app | |
| * | (BackgroundTask View) | |
| * | | |
| * | | |
| * | | |
| * | | |
| * —-------------------------------------------------------------------- |
| * | | |
| * | | |
| * | | |
| * | App Space (Root Task View Panel) | |
| * | (This layer is above maps) | |
| * | | |
| * | | |
| * | | |
| * | | |
| * —-------------------------------------------------------------------- |
| * | Control Bar | |
| * | | |
| * —-------------------------------------------------------------------- |
| * | Nav Bar | |
| * —-------------------------------------------------------------------- |
| * |
| * In total this Activity has 3 TaskViews. |
| * Background Task view: |
| * - It only contains maps app. |
| * - Maps app is manually started in this taskview. |
| * |
| * Root Task View (Part of Root Task View Panel): |
| * - It acts as the default container. Which means all the apps will run inside it by default. |
| * |
| * Note: Root Task View Panel always overlap over the Background TaskView. |
| * |
| * Fullscreen Task View |
| * - Used for voice assist applications |
| */ |
| public final class CarUiPortraitHomeScreen extends FragmentActivity { |
| public static final String TAG = CarUiPortraitHomeScreen.class.getSimpleName(); |
| private static final boolean DBG = Build.IS_DEBUGGABLE; |
| |
| private int mStatusBarHeight; |
| private FrameLayout mContainer; |
| private View mControlBarView; |
| private TaskViewManager mTaskViewManager; |
| // All the TaskViews & corresponding helper instance variables. |
| private CarTaskView mBackgroundTaskView; |
| private CarTaskView mFullScreenTaskView; |
| private boolean mIsBackgroundTaskViewReady; |
| private boolean mIsFullScreenTaskViewReady; |
| private int mNavBarHeight; |
| private boolean mIsSUWInProgress; |
| private TaskCategoryManager mTaskCategoryManager; |
| private TaskInfoCache mTaskInfoCache; |
| private TaskViewPanel mAppGridTaskViewPanel; |
| private TaskViewPanel mRootTaskViewPanel; |
| |
| /** Messenger for communicating with {@link CarUiPortraitService}. */ |
| private Messenger mService = null; |
| /** Flag indicating whether or not {@link CarUiPortraitService} is bounded. */ |
| private boolean mIsBound; |
| |
| /** |
| * All messages from {@link CarUiPortraitService} are received in this handler. |
| */ |
| private final Messenger mMessenger = new Messenger(new IncomingHandler()); |
| |
| /** Holds any messages fired before service connection is establish. */ |
| private final List<Message> mMessageCache = new ArrayList<>(); |
| |
| private CarUiPortraitDriveStateController mCarUiPortraitDriveStateController; |
| |
| /** |
| * Class for interacting with the main interface of the {@link CarUiPortraitService}. |
| */ |
| private final ServiceConnection mConnection = new ServiceConnection() { |
| public void onServiceConnected(ComponentName className, IBinder service) { |
| // Communicating with our service through an IDL interface, so get a client-side |
| // representation of that from the raw service object. |
| mService = new Messenger(service); |
| |
| // Register to the service. |
| try { |
| Message msg = Message.obtain(null, MSG_REGISTER_CLIENT); |
| msg.replyTo = mMessenger; |
| mService.send(msg); |
| } catch (RemoteException e) { |
| // In this case the service has crashed before we could even |
| // do anything with it; we can count on soon being |
| // disconnected (and then reconnected if it can be restarted) |
| // so there is no need to do anything here. |
| Log.w(TAG, "can't connect to CarUiPortraitService: ", e); |
| } |
| for (Message msg : mMessageCache) { |
| notifySystemUI(msg); |
| } |
| mMessageCache.clear(); |
| } |
| |
| public void onServiceDisconnected(ComponentName className) { |
| mService = null; |
| } |
| }; |
| |
| // This listener lets us know when actives are added and removed from any of the display regions |
| // we care about, so we can trigger the opening and closing of the app containers as needed. |
| private final TaskStackListener mTaskStackListener = new TaskStackListener() { |
| @Override |
| public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) |
| throws RemoteException { |
| logIfDebuggable("On task moved to front, task = " + taskInfo); |
| if (!mRootTaskViewPanel.isReady()) { |
| logIfDebuggable("Root Task View is not ready yet."); |
| if (!TaskCategoryManager.isHomeIntent(taskInfo) |
| && !mTaskCategoryManager.isBackgroundApp(taskInfo) |
| && !mTaskCategoryManager.isAppGridActivity(taskInfo)) { |
| |
| cacheTask(taskInfo); |
| } |
| return; |
| } |
| |
| // Close the panel if the top application is a blank activity. |
| // This is to prevent showing a blank panel to the user if an app crashes an reveals |
| // the blank activity underneath. |
| if (mTaskCategoryManager.isBlankActivity(taskInfo)) { |
| mRootTaskViewPanel.closePanel(/* animated = */ true); |
| return; |
| } |
| |
| if (shouldTaskShowOnRootTaskView(taskInfo)) { |
| logIfDebuggable("Opening in root task view: " + taskInfo); |
| |
| if (mAppGridTaskViewPanel.isOpen()) { |
| // Animate the root task view to expand on top of the app grid task view. |
| mRootTaskViewPanel.expandPanel(); |
| } else { |
| // Open the root task view. |
| mRootTaskViewPanel.openPanel(/* animate = */ true); |
| } |
| } else { |
| logIfDebuggable("Not showing task in rootTaskView"); |
| } |
| } |
| |
| /** |
| * Called whenever IActivityManager.startActivity is called on an activity that is already |
| * running, but the task is either brought to the front or a new Intent is delivered to it. |
| * |
| * @param taskInfo information about the task the activity was relaunched into |
| * @param homeTaskVisible whether or not the home task is visible |
| * @param clearedTask whether or not the launch activity also cleared the task as a part of |
| * starting |
| * @param wasVisible whether the activity was visible before the restart attempt |
| */ |
| @Override |
| public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo taskInfo, |
| boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) |
| throws RemoteException { |
| super.onActivityRestartAttempt(taskInfo, homeTaskVisible, clearedTask, wasVisible); |
| |
| logIfDebuggable("On Activity restart attempt, task = " + taskInfo); |
| if (taskInfo.baseIntent == null || taskInfo.baseIntent.getComponent() == null |
| || mRootTaskViewPanel.isAnimating()) { |
| return; |
| } |
| |
| if (!wasVisible) { |
| return; |
| } |
| |
| if (mTaskCategoryManager.isBackgroundApp(taskInfo) |
| || mTaskCategoryManager.isBlankActivity(taskInfo)) { |
| return; |
| } |
| |
| if (mRootTaskViewPanel.isAnimating() || mAppGridTaskViewPanel.isAnimating()) { |
| return; |
| } |
| |
| logIfDebuggable("Update UI state on app restart attempt, task = " + taskInfo); |
| if (mTaskCategoryManager.isAppGridActivity(taskInfo)) { |
| // If the new task is an app grid then toggle the app grid panel: |
| // 1 - Close the app grid panel if it is open. |
| // 2 - Open the app grid panel if it is closed: |
| // a) If the root task view is open on top of the app grid then use a fade |
| // animation to hide the root task view panel and show the app grid panel. |
| // b) Otherwise, simply open the app grid panel. |
| if (mAppGridTaskViewPanel.isOpen()) { |
| mAppGridTaskViewPanel.closePanel(); |
| } else if (mRootTaskViewPanel.isOpen()) { |
| mAppGridTaskViewPanel.fadeInPanel(); |
| mRootTaskViewPanel.fadeOutPanel(); |
| } else { |
| mAppGridTaskViewPanel.openPanel(); |
| } |
| } else if (shouldTaskShowOnRootTaskView(taskInfo)) { |
| // If the new task should be launched in the root task view panel: |
| // 1 - Close the root task view panel if it is open and the task is notification |
| // center. |
| // 2 - Open the root task view panel if it is closed: |
| // a) If the app grid panel is already open then use an expand animation |
| // to open the root task view on top of the app grid task view. |
| // b) Otherwise, simply open the app grid panel. |
| if (mRootTaskViewPanel.isOpen() |
| && mTaskCategoryManager.isNotificationActivity(taskInfo)) { |
| mRootTaskViewPanel.closePanel(); |
| } else if (mAppGridTaskViewPanel.isOpen()) { |
| mRootTaskViewPanel.expandPanel(); |
| } else { |
| mRootTaskViewPanel.openPanel(); |
| } |
| } |
| } |
| }; |
| |
| /** |
| * Only resize the size of rootTaskView when SUW is in progress. This is to resize the height of |
| * rootTaskView after status bar hide on SUW start. |
| */ |
| private final View.OnLayoutChangeListener mHomeScreenLayoutChangeListener = |
| (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { |
| int newHeight = bottom - top; |
| int oldHeight = oldBottom - oldTop; |
| if (oldHeight == newHeight) { |
| return; |
| } |
| logIfDebuggable("container height change from " + oldHeight + " to " + newHeight); |
| if (mIsSUWInProgress) { |
| mRootTaskViewPanel.openFullScreenPanel(/* animate = */ false); |
| } |
| }; |
| |
| private static void logIfDebuggable(String message) { |
| if (DBG) { |
| Log.d(TAG, message); |
| } |
| } |
| |
| private final View.OnLayoutChangeListener mControlBarOnLayoutChangeListener = |
| (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { |
| if (bottom == oldBottom && top == oldTop) { |
| return; |
| } |
| |
| logIfDebuggable("Control Bar layout changed!"); |
| updateTaskViewInsets(); |
| updateBackgroundTaskViewInsets(); |
| updateObscuredTouchRegion(); |
| }; |
| |
| @Override |
| protected void onCreate(@Nullable Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| if (getApplicationContext().getResources().getConfiguration().orientation |
| == Configuration.ORIENTATION_LANDSCAPE) { |
| Intent launcherIntent = new Intent(this, CarLauncher.class); |
| launcherIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| startActivity(launcherIntent); |
| finish(); |
| return; |
| } |
| |
| setContentView(R.layout.car_ui_portrait_launcher); |
| |
| mTaskCategoryManager = new TaskCategoryManager(getApplicationContext()); |
| mTaskInfoCache = new TaskInfoCache(getApplicationContext()); |
| |
| // Make the window fullscreen as GENERIC_OVERLAYS are supplied to the background task view |
| WindowCompat.setDecorFitsSystemWindows(getWindow(), false); |
| |
| // Activity is running fullscreen to allow background task to bleed behind status bar |
| mNavBarHeight = getResources().getDimensionPixelSize( |
| com.android.internal.R.dimen.navigation_bar_height); |
| logIfDebuggable("Navbar height: " + mNavBarHeight); |
| mContainer = findViewById(R.id.container); |
| mContainer.addOnLayoutChangeListener(mHomeScreenLayoutChangeListener); |
| setHomeScreenBottomMargin(mNavBarHeight); |
| |
| mAppGridTaskViewPanel = findViewById(R.id.app_grid_panel); |
| |
| mRootTaskViewPanel = findViewById(R.id.application_panel); |
| |
| mStatusBarHeight = getResources().getDimensionPixelSize( |
| com.android.internal.R.dimen.status_bar_height); |
| |
| mControlBarView = findViewById(R.id.control_bar_area); |
| mControlBarView.addOnLayoutChangeListener(mControlBarOnLayoutChangeListener); |
| |
| // Setting as trusted overlay to let touches pass through. |
| getWindow().addPrivateFlags(PRIVATE_FLAG_TRUSTED_OVERLAY); |
| // To pass touches to the underneath task. |
| getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL); |
| |
| mContainer.addOnLayoutChangeListener( |
| (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { |
| boolean widthChanged = (right - left) != (oldRight - oldLeft); |
| boolean heightChanged = (bottom - top) != (oldBottom - oldTop); |
| |
| if (widthChanged || heightChanged) { |
| onContainerDimensionsChanged(); |
| } |
| }); |
| |
| // If we happen to be resurfaced into a multi display mode we skip launching content |
| // in the activity view as we will get recreated anyway. |
| if (isInMultiWindowMode() || isInPictureInPictureMode()) { |
| return; |
| } |
| |
| if (mTaskViewManager == null) { |
| mTaskViewManager = new TaskViewManager(this, getMainThreadHandler()); |
| } |
| |
| mCarUiPortraitDriveStateController = new CarUiPortraitDriveStateController( |
| getApplicationContext()); |
| |
| TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); |
| |
| initializeCards(); |
| doBindService(); |
| |
| setUpRootTaskView(); |
| } |
| |
| @Override |
| protected void onNewIntent(Intent intent) { |
| super.onNewIntent(intent); |
| |
| // This is done to handle the case where 'close' is tapped on ActivityBlockingActivity and |
| // it navigates to the home app. It assumes that the currently display task will be |
| // replaced with the home. |
| // In this case, ABA is actually displayed inside launch-root-task. By closing the |
| // root task view panel we make sure the app goes to the background. |
| mRootTaskViewPanel.closePanel(); |
| } |
| |
| private void initializeCards() { |
| Set<HomeCardModule> homeCardModules = new androidx.collection.ArraySet<>(); |
| for (String providerClassName : getResources().getStringArray( |
| R.array.config_homeCardModuleClasses)) { |
| try { |
| long reflectionStartTime = System.currentTimeMillis(); |
| HomeCardModule cardModule = (HomeCardModule) Class.forName( |
| providerClassName).getDeclaredConstructor().newInstance(); |
| cardModule.setViewModelProvider(new ViewModelProvider(/* owner= */ this)); |
| homeCardModules.add(cardModule); |
| if (DBG) { |
| long reflectionTime = System.currentTimeMillis() - reflectionStartTime; |
| logIfDebuggable( |
| "Initialization of HomeCardModule class " + providerClassName |
| + " took " + reflectionTime + " ms"); |
| } |
| } catch (IllegalAccessException | InstantiationException |
| | ClassNotFoundException | InvocationTargetException |
| | NoSuchMethodException e) { |
| Log.w(TAG, "Unable to create HomeCardProvider class " + providerClassName, e); |
| } |
| } |
| FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); |
| for (HomeCardModule cardModule : homeCardModules) { |
| transaction.replace(cardModule.getCardResId(), cardModule.getCardView()); |
| } |
| transaction.commitNow(); |
| } |
| |
| @Override |
| public void onConfigurationChanged(@NonNull Configuration newConfig) { |
| super.onConfigurationChanged(newConfig); |
| initializeCards(); |
| |
| mRootTaskViewPanel.post(() -> mRootTaskViewPanel.refresh(getTheme())); |
| mAppGridTaskViewPanel.post(() -> mAppGridTaskViewPanel.refresh(getTheme())); |
| } |
| |
| @Override |
| protected void onDestroy() { |
| mTaskViewManager = null; |
| mRootTaskViewPanel.onDestroy(); |
| mBackgroundTaskView = null; |
| mFullScreenTaskView = null; |
| TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); |
| doUnbindService(); |
| super.onDestroy(); |
| } |
| |
| private boolean shouldTaskShowOnRootTaskView(TaskInfo taskInfo) { |
| if (taskInfo.baseIntent == null || taskInfo.baseIntent.getComponent() == null |
| || mRootTaskViewPanel.isAnimating()) { |
| logIfDebuggable("Should not show on root task view since task is null"); |
| return false; |
| } |
| |
| // fullscreen activities will open in a separate task view which will show on top most |
| // z-layer, that should not change the state of the root task view. |
| if (mTaskCategoryManager.isFullScreenActivity(taskInfo)) { |
| logIfDebuggable("Should not show on root task view since task is full screen activity"); |
| return false; |
| } |
| |
| if (mTaskCategoryManager.isAppGridActivity(taskInfo)) { |
| logIfDebuggable("Don't open app grid activity on the root task view"); |
| return false; |
| } |
| |
| if (mTaskCategoryManager.isBackgroundApp(taskInfo)) { |
| logIfDebuggable("Should not show on root task view since task is background activity"); |
| // we don't want to change the state of the root task view when background |
| // task are launched or brought to front. |
| return false; |
| } |
| |
| // Any task that does NOT meet all the below criteria should be ignored. |
| // 1. displayAreaFeatureId should be FEATURE_DEFAULT_TASK_CONTAINER |
| // 2. should be visible |
| // 3. for the current user ONLY. System user launches some tasks on cluster that should |
| // not affect the state of the foreground DA |
| // 4. any task that is manually defined to be ignored |
| return taskInfo.displayAreaFeatureId == FEATURE_DEFAULT_TASK_CONTAINER |
| && taskInfo.userId == ActivityManager.getCurrentUser() |
| && !mTaskCategoryManager.shouldIgnoreOpeningForegroundDA(taskInfo); |
| } |
| |
| private void cacheTask(ActivityManager.RunningTaskInfo taskInfo) { |
| logIfDebuggable("Caching the task: " + taskInfo); |
| if (mTaskInfoCache.cancelTask(taskInfo)) { |
| boolean cached = mTaskInfoCache.cacheTask(taskInfo); |
| logIfDebuggable("Task " + taskInfo + " is cached = " + cached); |
| } |
| } |
| |
| private void setHomeScreenBottomMargin(int bottomMargin) { |
| FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mContainer.getLayoutParams(); |
| lp.bottomMargin = bottomMargin; |
| mContainer.setLayoutParams(lp); |
| } |
| |
| private void updateObscuredTouchRegion() { |
| if (mBackgroundTaskView == null) { |
| return; |
| } |
| Rect controlBarRect = new Rect(); |
| mControlBarView.getBoundsOnScreen(controlBarRect); |
| |
| Rect appPanelGripBarRect = new Rect(); |
| mAppGridTaskViewPanel.getGripBarBounds(appPanelGripBarRect); |
| |
| Rect rootPanelGripBarRect = new Rect(); |
| mRootTaskViewPanel.getGripBarBounds(rootPanelGripBarRect); |
| |
| Rect navigationBarRect = new Rect(controlBarRect.right, controlBarRect.bottom, |
| controlBarRect.left, mContainer.getMeasuredHeight()); |
| |
| Region obscuredTouchRegion = new Region(); |
| obscuredTouchRegion.union(controlBarRect); |
| obscuredTouchRegion.union(navigationBarRect); |
| |
| mRootTaskViewPanel.setObscuredTouchRegion(obscuredTouchRegion); |
| mAppGridTaskViewPanel.setObscuredTouchRegion(obscuredTouchRegion); |
| obscuredTouchRegion.union(appPanelGripBarRect); |
| obscuredTouchRegion.union(rootPanelGripBarRect); |
| mBackgroundTaskView.setObscuredTouchRegion(obscuredTouchRegion); |
| mFullScreenTaskView.setObscuredTouchRegion(obscuredTouchRegion); |
| } |
| |
| private void updateBackgroundTaskViewInsets() { |
| if (mBackgroundTaskView == null) { |
| return; |
| } |
| |
| int bottomOverlap = Math.min(mControlBarView.getTop(), |
| mRootTaskViewPanel.getTop()); |
| bottomOverlap = Math.min(bottomOverlap, mAppGridTaskViewPanel.getTop()); |
| |
| Rect appAreaBounds = new Rect(); |
| mBackgroundTaskView.getBoundsOnScreen(appAreaBounds); |
| |
| Rect bottomInsets = new Rect(appAreaBounds.left, bottomOverlap, |
| appAreaBounds.right, appAreaBounds.bottom); |
| Rect topInsets = new Rect(appAreaBounds.left, appAreaBounds.top, appAreaBounds.right, |
| mStatusBarHeight); |
| |
| logIfDebuggable( |
| "Applying bottom insets: " + bottomInsets + " top insets: " + topInsets); |
| mBackgroundTaskView.setInsets(new SparseArray<>() { |
| { |
| append(ITYPE_BOTTOM_GENERIC_OVERLAY, bottomInsets); |
| append(ITYPE_TOP_GENERIC_OVERLAY, topInsets); |
| } |
| }); |
| } |
| |
| private void updateTaskViewInsets() { |
| Insets insets = |
| Insets.of(/* left= */ 0, /* top= */ 0, /* right= */ 0, mControlBarView.getHeight()); |
| if (mRootTaskViewPanel != null) { |
| mRootTaskViewPanel.setInsets(insets); |
| } |
| if (mAppGridTaskViewPanel != null) { |
| mAppGridTaskViewPanel.setInsets(insets); |
| } |
| } |
| |
| private void onContainerDimensionsChanged() { |
| if (mRootTaskViewPanel != null) { |
| mRootTaskViewPanel.onParentDimensionChanged(); |
| } |
| |
| if (mAppGridTaskViewPanel != null) { |
| mAppGridTaskViewPanel.onParentDimensionChanged(); |
| } |
| |
| updateTaskViewInsets(); |
| |
| if (mBackgroundTaskView != null) { |
| mBackgroundTaskView.post(() -> mBackgroundTaskView.onLocationChanged()); |
| } |
| } |
| |
| private void setUpBackgroundTaskView() { |
| ViewGroup parent = findViewById(R.id.background_app_area); |
| mTaskViewManager.createControlledCarTaskView(getMainExecutor(), |
| ControlledCarTaskViewConfig.builder() |
| .setActivityIntent(CarLauncherUtils.getMapsIntent(getApplicationContext())) |
| .setAutoRestartOnCrash(/* autoRestartOnCrash- */ true) |
| .build(), |
| new ControlledCarTaskViewCallbacks() { |
| @Override |
| public void onTaskViewCreated(CarTaskView taskView) { |
| logIfDebuggable("Background Task View is created"); |
| taskView.setZOrderOnTop(false); |
| mBackgroundTaskView = taskView; |
| parent.addView(mBackgroundTaskView); |
| } |
| |
| @Override |
| public void onTaskViewReady() { |
| logIfDebuggable("Background Task View is ready"); |
| mIsBackgroundTaskViewReady = true; |
| onTaskViewReadinessUpdated(); |
| updateBackgroundTaskViewInsets(); |
| } |
| } |
| ); |
| } |
| |
| private void setControlBarVisibility(boolean isVisible, boolean animate) { |
| float translationY = isVisible ? 0 : mContainer.getHeight() - mControlBarView.getTop(); |
| if (animate) { |
| mControlBarView.animate().translationY(translationY); |
| } else { |
| mControlBarView.setTranslationY(translationY); |
| } |
| |
| } |
| |
| private void setUpAppGridTaskView() { |
| mAppGridTaskViewPanel.setTag("AppGridPanel"); |
| mTaskViewManager.createControlledCarTaskView(getMainExecutor(), |
| ControlledCarTaskViewConfig.builder() |
| .setActivityIntent(CarLauncherUtils.getAppsGridIntent()) |
| .setAutoRestartOnCrash(/* autoRestartOnCrash= */ true) |
| .build(), |
| new ControlledCarTaskViewCallbacks() { |
| @Override |
| public void onTaskViewCreated(CarTaskView taskView) { |
| taskView.setZOrderMediaOverlay(true); |
| mAppGridTaskViewPanel.setTaskView(taskView); |
| } |
| |
| @Override |
| public void onTaskViewReady() { |
| logIfDebuggable("App grid Task View is ready"); |
| mAppGridTaskViewPanel.setReady(true); |
| onTaskViewReadinessUpdated(); |
| } |
| } |
| ); |
| |
| mAppGridTaskViewPanel.setOnStateChangeListener(new TaskViewPanel.OnStateChangeListener() { |
| @Override |
| public void onStateChangeStart(TaskViewPanel.State oldState, |
| TaskViewPanel.State newState, boolean animated) { |
| boolean isVisible = newState.isVisible(); |
| if (isVisible) { |
| notifySystemUI(MSG_APP_GRID_VISIBILITY_CHANGE, boolToInt(isVisible)); |
| } |
| } |
| |
| @Override |
| public void onStateChangeEnd(TaskViewPanel.State oldState, |
| TaskViewPanel.State newState, boolean animated) { |
| updateObscuredTouchRegion(); |
| updateBackgroundTaskViewInsets(); |
| |
| boolean isVisible = newState.isVisible(); |
| if (!isVisible) { |
| notifySystemUI(MSG_APP_GRID_VISIBILITY_CHANGE, boolToInt(isVisible)); |
| } |
| } |
| }); |
| } |
| |
| private boolean isAllTaskViewsReady() { |
| return mRootTaskViewPanel.isReady() && mAppGridTaskViewPanel.isReady() |
| && mIsBackgroundTaskViewReady && mIsFullScreenTaskViewReady; |
| } |
| |
| private void onTaskViewReadinessUpdated() { |
| if (!isAllTaskViewsReady()) { |
| return; |
| } |
| logIfDebuggable("All task views are ready"); |
| updateObscuredTouchRegion(); |
| updateBackgroundTaskViewInsets(); |
| notifySystemUI(MSG_FG_TASK_VIEW_READY, boolToInt(true)); |
| Rect controlBarBounds = new Rect(); |
| mControlBarView.getBoundsOnScreen(controlBarBounds); |
| boolean isControlBarVisible = mControlBarView.getVisibility() == View.VISIBLE; |
| logIfDebuggable("Control bar:" |
| + "( visible: " + isControlBarVisible |
| + ", bounds:" + controlBarBounds |
| + ")"); |
| } |
| |
| private void setUpRootTaskView() { |
| mRootTaskViewPanel.setTag("RootPanel"); |
| mRootTaskViewPanel.setTaskViewBackgroundColor(Color.BLACK); |
| mRootTaskViewPanel.setOnStateChangeListener(new TaskViewPanel.OnStateChangeListener() { |
| @Override |
| public void onStateChangeStart(TaskViewPanel.State oldState, |
| TaskViewPanel.State newState, boolean animated) { |
| boolean isFullScreen = newState.isFullScreen(); |
| if (isFullScreen) { |
| setHomeScreenBottomMargin(mIsSUWInProgress ? 0 : mNavBarHeight); |
| if (!mIsSUWInProgress) { |
| notifySystemUI(MSG_HIDE_SYSTEM_BAR_FOR_IMMERSIVE, boolToInt(isFullScreen)); |
| } |
| } else { |
| setControlBarVisibility(/* isVisible= */ true, animated); |
| } |
| } |
| |
| @Override |
| public void onStateChangeEnd(TaskViewPanel.State oldState, |
| TaskViewPanel.State newState, boolean animated) { |
| updateObscuredTouchRegion(); |
| updateBackgroundTaskViewInsets(); |
| |
| // Hide the control bar after the animation if in full screen. |
| if (newState.isFullScreen()) { |
| setControlBarVisibility(/* isVisible= */ false, animated); |
| } else { |
| // Adjust the bottom margin to count for the nav bar. |
| setHomeScreenBottomMargin(mNavBarHeight); |
| // Show the nav bar if not showing Setup Wizard |
| if (!mIsSUWInProgress) { |
| notifySystemUI(MSG_HIDE_SYSTEM_BAR_FOR_IMMERSIVE, |
| boolToInt(newState.isFullScreen())); |
| } |
| } |
| |
| // Hide the app grid task view behind the root task view. |
| if (newState.isVisible()) { |
| mAppGridTaskViewPanel.closePanel(/* animated = */ false); |
| } else { |
| // Launch a blank activity to move the top activity to background. |
| startActivity(BlankActivity.createIntent(getApplicationContext())); |
| } |
| } |
| }); |
| |
| mTaskViewManager.createLaunchRootTaskView(getMainExecutor(), |
| new LaunchRootCarTaskViewCallbacks() { |
| @Override |
| public void onTaskViewCreated(CarTaskView taskView) { |
| logIfDebuggable("Root Task View is created"); |
| taskView.setZOrderMediaOverlay(true); |
| mRootTaskViewPanel.setTaskView(taskView); |
| } |
| |
| @Override |
| public void onTaskViewReady() { |
| logIfDebuggable("Root Task View is ready"); |
| mRootTaskViewPanel.setReady(true); |
| mTaskInfoCache.startCachedTasks(); |
| onTaskViewReadinessUpdated(); |
| |
| setUpBackgroundTaskView(); |
| setUpAppGridTaskView(); |
| setUpFullScreenTaskView(); |
| } |
| }); |
| } |
| |
| private void setUpFullScreenTaskView() { |
| ViewGroup parent = findViewById(R.id.fullscreen_container); |
| mTaskViewManager.createSemiControlledTaskView(getMainExecutor(), |
| new SemiControlledCarTaskViewCallbacks() { |
| @Override |
| public boolean shouldStartInTaskView(TaskInfo taskInfo) { |
| if (taskInfo.baseActivity == null) { |
| return false; |
| } |
| return mTaskCategoryManager.isFullScreenActivity(taskInfo); |
| } |
| |
| @Override |
| public void onTaskViewCreated(CarTaskView taskView) { |
| logIfDebuggable("FullScreen Task View is created"); |
| mFullScreenTaskView = taskView; |
| mFullScreenTaskView.setZOrderOnTop(true); |
| parent.addView(mFullScreenTaskView); |
| } |
| |
| @Override |
| public void onTaskViewReady() { |
| logIfDebuggable("FullScreen Task View is ready"); |
| mIsFullScreenTaskViewReady = true; |
| onTaskViewReadinessUpdated(); |
| } |
| }); |
| } |
| |
| private void onImmersiveModeRequested(boolean requested, boolean animate) { |
| logIfDebuggable("onImmersiveModeRequested = " + requested); |
| if (requested && (!mCarUiPortraitDriveStateController.isDrivingStateMoving() |
| || mIsSUWInProgress)) { |
| mRootTaskViewPanel.openFullScreenPanel(animate); |
| } else { |
| if (mTaskViewManager.getRootTaskCount() > 0) { |
| mRootTaskViewPanel.openPanel(animate); |
| } else { |
| // Don't animate if there is no task in the panel. |
| mRootTaskViewPanel.closePanel(/* animated = */ false); |
| } |
| } |
| } |
| |
| /** |
| * Handler of incoming messages from service. |
| */ |
| class IncomingHandler extends Handler { |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_IMMERSIVE_MODE_REQUESTED: |
| onImmersiveModeRequested(intToBool(msg.arg1), /* animate = */ true); |
| break; |
| case MSG_SUW_IN_PROGRESS: |
| mIsSUWInProgress = intToBool(msg.arg1); |
| logIfDebuggable("Get intent about the SUW is " + mIsSUWInProgress); |
| onImmersiveModeRequested(mIsSUWInProgress, /* animate = */ false); |
| break; |
| default: |
| super.handleMessage(msg); |
| } |
| } |
| } |
| |
| void doBindService() { |
| // Establish a connection with {@link CarUiPortraitService}. We use an explicit class |
| // name because there is no reason to be able to let other applications replace our |
| // component. |
| bindService(new Intent(this, CarUiPortraitService.class), mConnection, |
| Context.BIND_AUTO_CREATE); |
| mIsBound = true; |
| } |
| |
| void doUnbindService() { |
| if (mIsBound) { |
| if (mService != null) { |
| try { |
| Message msg = Message.obtain(null, MSG_UNREGISTER_CLIENT); |
| msg.replyTo = mMessenger; |
| mService.send(msg); |
| } catch (RemoteException e) { |
| Log.w(TAG, "can't unregister to CarUiPortraitService: ", e); |
| } |
| } |
| |
| // Detach our existing connection. |
| unbindService(mConnection); |
| mIsBound = false; |
| } |
| } |
| |
| private void notifySystemUI(int key, int value) { |
| Message msg = Message.obtain(null, key, value, 0); |
| notifySystemUI(msg); |
| } |
| |
| private void notifySystemUI(Message msg) { |
| try { |
| if (mService != null) { |
| mService.send(msg); |
| } else { |
| logIfDebuggable("Service is not connected yet! Caching the message:" + msg); |
| mMessageCache.add(msg); |
| } |
| } catch (RemoteException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private static int boolToInt(Boolean b) { |
| return b ? 1 : 0; |
| } |
| |
| private static boolean intToBool(int val) { |
| return val == 1; |
| } |
| } |