diff --git a/src/com/android/launcher3/model/data/WorkspaceData.kt b/src/com/android/launcher3/model/data/WorkspaceData.kt index 692d0d36c3..e69de29bb2 100644 --- a/src/com/android/launcher3/model/data/WorkspaceData.kt +++ b/src/com/android/launcher3/model/data/WorkspaceData.kt @@ -1,191 +0,0 @@ -/* - * 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.launcher3.model.data - -import android.content.Context -import android.util.SparseArray -import androidx.annotation.VisibleForTesting -import androidx.core.util.putAll -import androidx.core.util.valueIterator -import app.lawnchair.preferences2.PreferenceManager2 -import com.android.launcher3.BuildConfig -import com.android.launcher3.BuildConfigs -import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP -import com.android.launcher3.Workspace -import com.android.launcher3.model.data.WorkspaceChangeEvent.AddEvent -import com.android.launcher3.model.data.WorkspaceChangeEvent.RemoveEvent -import com.android.launcher3.model.data.WorkspaceChangeEvent.UpdateEvent -import com.android.launcher3.util.IntArray -import com.android.launcher3.util.IntSet -import com.android.launcher3.util.ItemInfoMatcher -import com.patrykmichalik.opto.core.firstBlocking -import java.util.concurrent.atomic.AtomicInteger -import java.util.stream.Stream -import java.util.stream.StreamSupport - -/** - * An immutable representation of all the workspace items (shortcuts, folders, widgets and predicted - * items) - */ -sealed class WorkspaceData : Iterable { - - /** Creates an array of valid workspace screens based on current items in the model. */ - // LC-Note: Add context to replace QSB_ON_FIRST_SCREEN config - fun collectWorkspaceScreens(context: Context): IntArray { - val prefs2 = PreferenceManager2.INSTANCE.get(context) - val smartspaceEnabled = prefs2.enableSmartspace.firstBlocking() - - val screenSet = IntSet() - forEach { if (it.container == CONTAINER_DESKTOP) screenSet.add(it.screenId) } - if (smartspaceEnabled || screenSet.isEmpty) { - screenSet.add(Workspace.FIRST_SCREEN_ID) - } - return screenSet.array - } - - /** Returns the [ItemInfo] associated with the [id] or null */ - abstract operator fun get(id: Int): ItemInfo? - - fun stream(): Stream = StreamSupport.stream(spliterator(), false) - - /** Version determines the uniqueness per model load cycle */ - abstract val version: Int - /** Number of times, this data has been modified */ - internal abstract val modificationId: Int - /** Previous modifications to this data in reverse order: 1st entry is the latest update */ - protected abstract val changeHistory: List - - /** - * Returns the predicted items for the provided [containerId] or an empty list id no such - * container exists - */ - fun getPredictedContents(containerId: Int): List = - get(containerId).let { if (it is PredictedContainerInfo) it.getContents() else null } - ?: emptyList() - - /** Returns an immutable copy of the dataset */ - abstract fun copy(): WorkspaceData - - /** - * Returns a list of [WorkspaceChangeEvent] to apply for reaching from [source] to current - * state. Returns an empty list of the source is same as the current state and null if such a - * transition is not possible. - */ - fun diff(source: WorkspaceData): List? { - if (version != source.version) return null - val requiredHistorySize = modificationId - source.modificationId - - val changes = changeHistory - return when { - requiredHistorySize < 0 -> null - requiredHistorySize > changes.size -> null - requiredHistorySize == 0 -> emptyList() - else -> changes.subList(0, requiredHistorySize).reversed() - } - } - - override fun equals(other: Any?): Boolean { - return other is WorkspaceData && - other.version == version && - other.modificationId == modificationId - } - - /** A mutable implementation of [WorkspaceData] */ - class MutableWorkspaceData : WorkspaceData() { - - private val itemsIdMap = SparseArray() - - override var version: Int = VERSION_COUNTER.incrementAndGet() - - override var modificationId: Int = 0 - - override val changeHistory = mutableListOf() - - override fun iterator() = itemsIdMap.valueIterator() - - override fun get(id: Int): ItemInfo? = itemsIdMap.get(id) - - /** Replaces the existing dataset with [items] */ - fun replaceDataMap(items: SparseArray) { - itemsIdMap.clear() - itemsIdMap.putAll(items) - version = VERSION_COUNTER.incrementAndGet() - modificationId = 0 - changeHistory.clear() - } - - /** Adds the [item] to the dataset */ - fun addItems(items: List, owner: Any?) { - items.forEach { itemsIdMap[it.id] = it } - pushUpdate(AddEvent(items, owner)) - } - - /** Removes existing [items] from the dataset */ - fun removeItems(items: Collection, owner: Any?) { - items.forEach { itemsIdMap.remove(it.id) } - pushUpdate(RemoveEvent(ItemInfoMatcher.ofItems(items), owner)) - } - - /** Replaces an existing [item] from the dataset */ - fun replaceItem(item: ItemInfo, owner: Any?) { - itemsIdMap[item.id] = item - notifyItemsUpdated(listOf(item), owner) - } - - /** - * Notifies this dataset that or updates already performed on existing [items]. Since the - * underlying [ItemInfo]s are mutable objects, its possible to update their properties - * without going though this dataset - */ - fun notifyItemsUpdated(items: List, owner: Any?) { - pushUpdate(UpdateEvent(items, owner)) - } - - private fun pushUpdate(update: WorkspaceChangeEvent) { - modificationId++ - changeHistory.add(0, update) - if (changeHistory.size > MAX_HISTORY_SIZE) changeHistory.removeAt(changeHistory.lastIndex) - } - - override fun copy(): WorkspaceData = - ImmutableWorkspaceData(version, modificationId, changeHistory.toList(), itemsIdMap) - } - - /** An immutable implementation of [WorkspaceData] */ - class ImmutableWorkspaceData( - override val version: Int, - override val modificationId: Int, - override val changeHistory: List, - items: SparseArray, - ) : WorkspaceData() { - - private val itemsIdMap = items.clone() - - override fun iterator() = itemsIdMap.valueIterator() - - override fun get(id: Int): ItemInfo? = itemsIdMap.get(id) - - override fun copy(): WorkspaceData = this - } - - companion object { - // Maximum number of historic change events to keep in memory - @VisibleForTesting const val MAX_HISTORY_SIZE = 4 - - private val VERSION_COUNTER = AtomicInteger() - } -} diff --git a/src/com/android/launcher3/preview/LauncherPreviewRenderer.java b/src/com/android/launcher3/preview/LauncherPreviewRenderer.java index eab5572fae..e69de29bb2 100644 --- a/src/com/android/launcher3/preview/LauncherPreviewRenderer.java +++ b/src/com/android/launcher3/preview/LauncherPreviewRenderer.java @@ -1,421 +0,0 @@ -/* - * 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.launcher3.preview; - -import static android.view.View.MeasureSpec.EXACTLY; -import static android.view.View.MeasureSpec.makeMeasureSpec; -import static android.view.View.VISIBLE; - -import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_PREVIEW_RENDERER; -import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; -import static com.android.launcher3.model.ModelUtils.currentScreenContentFilter; - -import static java.util.Comparator.comparingDouble; - -import android.app.Fragment; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.TypedArray; -import android.graphics.Rect; -import android.os.Handler; -import android.os.Looper; -import android.util.AttributeSet; -import android.util.SparseIntArray; -import android.view.ContextThemeWrapper; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextClock; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; -import androidx.lifecycle.DefaultLifecycleObserver; -import androidx.lifecycle.LifecycleOwner; - -import app.lawnchair.preferences2.PreferenceManager2; -import com.android.launcher3.BuildConfig; -import com.android.launcher3.BuildConfigs; -import com.android.launcher3.CellLayout; -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Hotseat; -import com.android.launcher3.InsettableFrameLayout; -import com.android.launcher3.InvariantDeviceProfile; -import com.android.launcher3.LauncherModel; -import com.android.launcher3.R; -import com.android.launcher3.Workspace; -import com.android.launcher3.WorkspaceLayoutManager; -import com.android.launcher3.celllayout.CellLayoutLayoutParams; -import com.android.launcher3.celllayout.CellPosMapper; -import com.android.launcher3.dagger.LauncherComponentProvider; -import com.android.launcher3.dragndrop.SimpleDragLayer; -import com.android.launcher3.graphics.FragmentWithPreview; -import com.android.launcher3.model.BgDataModel; -import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.WorkspaceData; -import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.util.BaseContext; -import com.android.launcher3.util.DisplayController; -import com.android.launcher3.util.IntSet; -import com.android.launcher3.util.ItemInflater; -import com.android.launcher3.util.LauncherBindableItemsContainer; -import com.android.launcher3.util.window.WindowManagerProxy; -import com.android.launcher3.views.BaseDragLayer; -import com.android.launcher3.widget.LauncherWidgetHolder; - -import com.patrykmichalik.opto.core.PreferenceExtensionsKt; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; - -/** - * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile. - * Steps: - * 1) Create a dummy icon info with just white icon - * 2) Inflate a strip down layout definition for Launcher - * 3) Place appropriate elements like icons and first-page qsb - * 4) Measure and draw the view on a canvas - */ -public class LauncherPreviewRenderer extends BaseContext - implements WorkspaceLayoutManager, LayoutInflater.Factory2, - LauncherBindableItemsContainer, BgDataModel.Callbacks { - - public final CompletableFuture initialRender = new CompletableFuture<>(); - - private final Handler mUiHandler; - private final InvariantDeviceProfile mIdp; - private final DeviceProfile mDp; - private final LayoutInflater mHomeElementInflater; - private final LauncherPreviewLayout mRootView; - private final Hotseat mHotseat; - private final Map mWorkspaceScreens = new HashMap<>(); - private final ItemInflater mItemInflater; - - private final PreferenceManager2 mPreferenceManager2; - - public LauncherPreviewRenderer(Context context, - int workspaceScreenId, - @Nullable SparseIntArray wallpaperColorResources, - LauncherModel model, - int themeRes) { - - super(context, themeRes); - mPreferenceManager2 = PreferenceManager2.getInstance(context); - - mUiHandler = new Handler(Looper.getMainLooper()); - mIdp = InvariantDeviceProfile.INSTANCE.get(context); - mDp = getDeviceProfileForPreview(context).toBuilder(context) - .setViewScaleProvider(new PreviewScaleProvider(this)).build(); - Rect insets = getInsets(context); - mDp.updateInsets(insets); - - mHomeElementInflater = LayoutInflater.from( - new ContextThemeWrapper(this, R.style.HomeScreenElementTheme)); - mHomeElementInflater.setFactory2(this); - - int layoutRes = mDp.getDeviceProperties().isTwoPanels() - ? R.layout.launcher_preview_two_panel_layout - : R.layout.launcher_preview_layout; - mRootView = (LauncherPreviewLayout) mHomeElementInflater.inflate( - layoutRes, null, false); - mRootView.setInsets(insets); - measureAndLayoutRootView(); - - mHotseat = mRootView.findViewById(R.id.hotseat); - mHotseat.resetLayout(false); - - CellLayout firstScreen = mRootView.findViewById(R.id.workspace); - firstScreen.setPadding( - mDp.workspacePadding.left + mDp.cellLayoutPaddingPx.left, - mDp.workspacePadding.top + mDp.cellLayoutPaddingPx.top, - mDp.getDeviceProperties().isTwoPanels() ? (mDp.cellLayoutBorderSpacePx.x / 2) - : (mDp.workspacePadding.right + mDp.cellLayoutPaddingPx.right), - mDp.workspacePadding.bottom + mDp.cellLayoutPaddingPx.bottom - ); - - if (mDp.getDeviceProperties().isTwoPanels()) { - CellLayout rightPanel = mRootView.findViewById(R.id.workspace_right); - rightPanel.setPadding( - mDp.cellLayoutBorderSpacePx.x / 2, - mDp.workspacePadding.top + mDp.cellLayoutPaddingPx.top, - mDp.workspacePadding.right + mDp.cellLayoutPaddingPx.right, - mDp.workspacePadding.bottom + mDp.cellLayoutPaddingPx.bottom - ); - - int closestEvenPageId = workspaceScreenId - (workspaceScreenId % 2); - mWorkspaceScreens.put(closestEvenPageId, firstScreen); - mWorkspaceScreens.put(closestEvenPageId + 1, rightPanel); - } else { - mWorkspaceScreens.put(workspaceScreenId, firstScreen); - } - - LauncherWidgetHolder widgetHolder = LauncherComponentProvider.get(this) - .getWidgetHolderFactory().newInstance(this); - if (wallpaperColorResources != null) { - widgetHolder.setOnViewCreationCallback( - v -> v.setColorResources(wallpaperColorResources)); - } - - mItemInflater = new ItemInflater<>( - this, - widgetHolder, - view -> { }, - (view, b) -> { }, - mHotseat - ); - onViewCreated(); - model.addCallbacksAndLoad(this); - getLifecycle().addObserver(new DefaultLifecycleObserver() { - @Override - public void onDestroy(@NonNull LifecycleOwner owner) { - model.removeCallbacks(LauncherPreviewRenderer.this); - widgetHolder.destroy(); - } - }); - } - - @Override - public InsettableFrameLayout getRootView() { - return mRootView; - } - - @NonNull - @Override - public LauncherBindableItemsContainer getContent() { - return this; - } - - @Override - public BaseDragLayer getDragLayer() { - return mRootView; - } - - @Override - public DeviceProfile getDeviceProfile() { - return mDp; - } - - @Override - public Hotseat getHotseat() { - return mHotseat; - } - - /** - * Returns the device profile based on resource configuration for previewing various display - * sizes - */ - private DeviceProfile getDeviceProfileForPreview(Context context) { - float density = context.getResources().getDisplayMetrics().density; - Configuration config = context.getResources().getConfiguration(); - - return mIdp.getBestMatch( - config.screenWidthDp * density, - config.screenHeightDp * density, - WindowManagerProxy.INSTANCE.get(context).getRotation(context) - ); - } - - @Override - public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { - if ("TextClock".equals(name)) { - // Workaround for TextClock accessing handler for unregistering ticker. - return new TextClock(context, attrs) { - - @Override - public Handler getHandler() { - return mUiHandler; - } - }; - } else if (!"fragment".equals(name)) { - return null; - } - - TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PreviewFragment); - FragmentWithPreview f = (FragmentWithPreview) Fragment.instantiate( - context, ta.getString(R.styleable.PreviewFragment_android_name)); - f.enterPreviewMode(context); - f.onInit(null); - - View view = f.onCreateView(LayoutInflater.from(context), (ViewGroup) parent, null); - view.setId(ta.getInt(R.styleable.PreviewFragment_android_id, View.NO_ID)); - ta.recycle(); - return view; - } - - @Override - public View onCreateView(String name, Context context, AttributeSet attrs) { - return onCreateView(null, name, context, attrs); - } - - /** - * Hides the components in the bottom row. - * - * @param hide True to hide and false to show. - */ - @UiThread - public void hideBottomRow(boolean hide) { - if (mDp.isTaskbarPresent) { - // hotseat icons on bottom - mHotseat.setIconsAlpha(hide ? 0 : 1, ALPHA_CHANNEL_PREVIEW_RENDERER); - if (mDp.isQsbInline) { - mHotseat.setQsbAlpha(hide ? 0 : 1, ALPHA_CHANNEL_PREVIEW_RENDERER); - } - } else { - mHotseat.setQsbAlpha(hide ? 0 : 1, ALPHA_CHANNEL_PREVIEW_RENDERER); - } - } - - @Override - public CellLayout getScreenWithId(int screenId) { - return mWorkspaceScreens.get(screenId); - } - - @Override - public CellPosMapper getCellPosMapper() { - return CellPosMapper.DEFAULT; - } - - private List getAllLayouts() { - List screens = new ArrayList<>(mWorkspaceScreens.values()); - screens.add(getHotseat()); - return screens; - } - - @Nullable - @Override - public View mapOverItems(@NonNull ItemOperator op) { - return Workspace.mapOverCellLayouts(getAllLayouts().toArray(new CellLayout[0]), op); - } - - private void dispatchVisibilityAggregated(View view, boolean isVisible) { - // Similar to View.dispatchVisibilityAggregated implementation. - final boolean thisVisible = view.getVisibility() == VISIBLE; - if (thisVisible || !isVisible) { - view.onVisibilityAggregated(isVisible); - } - - if (view instanceof ViewGroup vg) { - isVisible = thisVisible && isVisible; - int count = vg.getChildCount(); - - for (int i = 0; i < count; i++) { - dispatchVisibilityAggregated(vg.getChildAt(i), isVisible); - } - } - } - - @Override - public void bindCompleteModel(@NonNull WorkspaceData itemIdMap, boolean isBindingSync) { - getAllLayouts().forEach(CellLayout::removeAllViews); - - // Separate the items that are on the current screen, and the other remaining items. - itemIdMap.stream() - .filter(currentScreenContentFilter(IntSet.wrap(mWorkspaceScreens.keySet()))) - .forEach(this::inflateAndAdd); - populateHotseatPredictions(itemIdMap); - - // Add first page QSB - if (PreferenceExtensionsKt.firstBlocking(mPreferenceManager2.getEnableSmartspace())) { - CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID); - if (firstScreen != null) { - View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen, false); - // TODO: set bgHandler on qsb when it is BaseTemplateCard, which requires API - // changes. - CellLayoutLayoutParams lp = new CellLayoutLayoutParams( - 0, 0, firstScreen.getCountX(), 1); - lp.canReorder = false; - firstScreen.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true); - } - } - measureAndLayoutRootView(); - dispatchVisibilityAggregated(mRootView, true); - measureAndLayoutRootView(); - // Additional measure for views which use auto text size API - measureAndLayoutRootView(); - initialRender.complete(mRootView); - } - - @Override - public void bindItemsUpdated(@NonNull Set updates) { - updateContainerItems(updates, this); - } - - private void populateHotseatPredictions(WorkspaceData itemIdMap) { - List predictions = itemIdMap.getPredictedContents(CONTAINER_HOTSEAT_PREDICTION); - int predictionIndex = 0; - for (int rank = 0; rank < mDp.numShownHotseatIcons; rank++) { - if (predictions.size() <= predictionIndex) continue; - - int cellX = mHotseat.getCellXFromOrder(rank); - int cellY = mHotseat.getCellYFromOrder(rank); - if (mHotseat.isOccupied(cellX, cellY)) continue; - - WorkspaceItemInfo itemInfo = - new WorkspaceItemInfo((WorkspaceItemInfo) predictions.get(predictionIndex)); - predictionIndex++; - itemInfo.rank = rank; - itemInfo.cellX = cellX; - itemInfo.cellY = cellY; - itemInfo.screenId = rank; - inflateAndAdd(itemInfo); - } - } - - private void inflateAndAdd(ItemInfo itemInfo) { - View view = mItemInflater.inflateItem(itemInfo); - if (view != null) { - addInScreenFromBind(view, itemInfo); - } - } - - private void measureAndLayoutRootView() { - int width = mDp.getDeviceProperties().getWidthPx(); - int height = mDp.getDeviceProperties().getHeightPx(); - mRootView.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY)); - mRootView.layout(0, 0, width, height); - } - - /** - * Returns the insets of the screen closest to the display given by the context - */ - private static Rect getInsets(Context context) { - Display display = context.getDisplay(); - return DisplayController.INSTANCE.get(context).getInfo().supportedBounds.stream() - .filter(w -> w.rotationHint == display.getRotation()) - .min(comparingDouble(w -> - Math.pow(display.getWidth() - w.availableSize.x, 2) - + Math.pow(display.getHeight() - w.availableSize.y, 2))) - .map(w -> new Rect(w.insets)) - .orElse(new Rect()); - } - - /** Root layout for launcher preview that intercepts all touch events. */ - public static class LauncherPreviewLayout extends SimpleDragLayer { - public LauncherPreviewLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - return true; - } - } -}