fix: Adapt clean workspace to 16-dev-qpr1

This commit is contained in:
Pun Butrach
2026-01-10 20:51:21 +07:00
parent 2071da1898
commit 715a556a06
6 changed files with 96 additions and 340 deletions
+8 -1
View File
@@ -166,6 +166,8 @@
<string name="home_screen_label">Home screen</string>
<string name="home_screen_description">Feed, grid, icons</string>
<string name="home_screen_actions">Home screen actions</string>
<string name="dock_label">Dock</string>
<string name="dock_description">Search bar, icon count</string>
@@ -277,6 +279,9 @@
<string name="reset_custom_icons">Reset custom icons</string>
<string name="reset_custom_icons_confirmation">All custom icons will be reset. Do you want to continue?</string>
<string name="remove_all_views_from_home_screen">Clear home screen</string>
<string name="remove_all_views_from_home_screen_desc">Home screen will be cleared. Do you want to continue?</string>
<!-- Icon picker -->
<string name="icon_picker_default_category">Icons</string>
<string name="icon_picker_reset_to_default">Reset to default</string>
@@ -777,7 +782,7 @@
-->
<!-- Launcher strings used -->
<string name="all_apps_device_search_hint">Search apps, web, and more</string>
<string name="all_apps_device_search_hint">Search web and more</string>
<string name="all_apps_search_bar_hint">Search apps</string>
<string name="all_apps_no_search_results">No apps found matching \"<xliff:g example="Android" id="query">%1$s</xliff:g>\"</string>
@@ -909,4 +914,6 @@
<string name="grant_requested_permissions">Grant permissions</string>
<string name="permissions_needed">Permissions needed</string>
<string name="grant_requested_permissions_tap">Tap to grant permissions</string>
<!-- Message shown when all home screen views are removed -->
<string name="home_screen_all_views_removed_msg">Home screen cleared</string>
</resources>
@@ -7,10 +7,10 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import app.lawnchair.data.folder.model.FolderOrderUtils
import app.lawnchair.data.folder.model.FolderViewModel
import app.lawnchair.flowerpot.Flowerpot
import app.lawnchair.launcher
import app.lawnchair.preferences.PreferenceManager
import app.lawnchair.preferences2.PreferenceManager2
import app.lawnchair.util.categorizeAppsWithSystemAndGoogle
import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener
import com.android.launcher3.allapps.AllAppsStore
import com.android.launcher3.allapps.AlphabeticalAppsList
@@ -45,7 +45,6 @@ class LawnchairAlphabeticalAppsList<T>(
private val filteredList = mutableListOf<AppInfo>()
private val folderOrder = FolderOrderUtils.stringToIntList(prefs.drawerListOrder.get())
private val potsManager = Flowerpot.Manager.getInstance(context)
init {
context.launcher.deviceProfile.inv.addOnChangeListener(this)
@@ -88,8 +87,10 @@ class LawnchairAlphabeticalAppsList<T>(
if (isWorkOrPrivateSpace(appList)) return super.addAppsWithSections(appList, position)
if (!drawerListDefault) {
val categorizedApps = potsManager.categorizeApps(appList)
categorizedApps.forEach { (category, apps) ->
val validApps = appList.mapNotNull { it }
val finalCategorizedApps = categorizeAppsWithSystemAndGoogle(validApps, context)
finalCategorizedApps.forEach { (category, apps) ->
if (apps.size == 1) {
mAdapterItems.add(AdapterItem.asApp(apps.first()))
} else {
@@ -6,6 +6,7 @@ import app.lawnchair.LawnchairLauncher
import app.lawnchair.flowerpot.Flowerpot
import app.lawnchair.launcher
import app.lawnchair.launcherNullable
import app.lawnchair.util.categorizeAppsWithSystemAndGoogle
import app.lawnchair.util.restartLauncher
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
@@ -17,6 +18,7 @@ import com.android.launcher3.model.data.FolderInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.provider.RestoreDbTask
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.PackageManagerHelper
import java.io.File
import java.util.Locale
import kotlinx.coroutines.CompletableDeferred
@@ -101,9 +103,8 @@ class LawndeckManager(private val context: Context) {
onProgress?.invoke("Categorizing apps...")
// Use flowerpot to categorize apps
val potsManager = Flowerpot.Manager.getInstance(context)
val categorizedApps = potsManager.categorizeApps(apps.map { it as? AppInfo })
val validApps = apps.mapNotNull { it as? AppInfo }
val finalCategorizedApps = categorizeAppsWithSystemAndGoogle(validApps, context)
onProgress?.invoke("Adding apps to workspace...")
@@ -115,7 +116,7 @@ class LawndeckManager(private val context: Context) {
var singleAppCount = 0
// Process each category
categorizedApps.forEach { (category, categoryApps) ->
finalCategorizedApps.forEach { (category, categoryApps) ->
if (categoryApps.isEmpty()) return@forEach
if (categoryApps.size == 1) {
@@ -188,22 +189,32 @@ class LawndeckManager(private val context: Context) {
val activityInfo = activities[0]
val appInfo = AppInfo(context, activityInfo, user)
// Use flowerpot to categorize the app
val potsManager = Flowerpot.Manager.getInstance(context)
val categorizedApps = potsManager.categorizeApps(listOf(appInfo))
val intent = appInfo.intent
if (categorizedApps.isEmpty()) {
// No category found, add directly to workspace
ItemInstallQueue.INSTANCE.get(context).queueItem(packageName, user)
return
}
// Determine category: Google Apps > System Apps > Flowerpot categories
val category = when {
packageName.startsWith("com.google.") -> "Google Apps"
// Get the category for this app (categorizedApps is a Map<String, List<AppInfo>>)
val categoryEntry = categorizedApps.entries.firstOrNull() ?: run {
ItemInstallQueue.INSTANCE.get(context).queueItem(packageName, user)
return
intent != null && PackageManagerHelper.isSystemApp(context, intent) -> "System Apps"
else -> {
// Use flowerpot to categorize the app
val potsManager = Flowerpot.Manager.getInstance(context)
val categorizedApps = potsManager.categorizeApps(listOf(appInfo))
if (categorizedApps.isEmpty()) {
// No category found, add directly to workspace
ItemInstallQueue.INSTANCE.get(context).queueItem(packageName, user)
return
}
// Get the category from flowerpot
categorizedApps.entries.firstOrNull()?.key ?: run {
ItemInstallQueue.INSTANCE.get(context).queueItem(packageName, user)
return
}
}
}
val category = categoryEntry.key
// Check if there's already a folder for this category on workspace
val existingFolder = findFolderByCategory(category, dataModel)
@@ -16,6 +16,8 @@
package app.lawnchair.ui.preferences.destinations
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -44,6 +46,9 @@ import app.lawnchair.ui.preferences.components.layout.PreferenceGroup
import app.lawnchair.ui.preferences.components.layout.PreferenceLayout
import app.lawnchair.ui.preferences.navigation.HomeScreenGrid
import app.lawnchair.util.collectAsStateBlocking
import com.android.launcher3.Launcher
import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherSettings
import com.android.launcher3.R
import com.android.launcher3.Utilities
import kotlinx.coroutines.launch
@@ -60,6 +65,7 @@ fun HomeScreenPreferences(
val prefs = preferenceManager()
val prefs2 = preferenceManager2()
val scope = rememberCoroutineScope()
val context = LocalContext.current
PreferenceLayout(
label = stringResource(id = R.string.home_screen_label),
backArrowVisible = !LocalIsExpandedScreen.current,
@@ -101,6 +107,19 @@ fun HomeScreenPreferences(
)
}
}
PreferenceGroup(heading = stringResource(id = R.string.home_screen_actions)) {
Item {
ClickablePreference(
label = stringResource(id = R.string.remove_all_views_from_home_screen),
confirmationText = stringResource(id = R.string.remove_all_views_from_home_screen_desc),
onClick = {
scope.launch {
clearAllViewsFromHomeScreen(context)
}
},
)
}
}
val feedAvailable = OverlayCallbackImpl.minusOneAvailable(LocalContext.current)
val enableFeedAdapter = prefs2.enableFeed.getAdapter()
PreferenceGroup(heading = stringResource(id = R.string.minus_one)) {
@@ -275,6 +294,20 @@ fun HomeScreenPreferences(
}
}
private fun clearAllViewsFromHomeScreen(context: Context) {
val model = Launcher.getLauncher(context).modelWriter
val isViewsRemoved = model.clearAllHomeScreenViewsByType(
LauncherSettings.Favorites.CONTAINER_DESKTOP,
)
if (isViewsRemoved) {
Toast.makeText(
context,
R.string.home_screen_all_views_removed_msg,
Toast.LENGTH_SHORT,
).show()
}
}
@Composable
fun HomeScreenTextColorPreference(
modifier: Modifier = Modifier,
@@ -1,318 +0,0 @@
/*
* Copyright (C) 2013 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.
*
* Modifications copyright 2021, Lawnchair
*/
package com.android.launcher3;
import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
import static android.content.Context.RECEIVER_EXPORTED;
import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
import static com.android.launcher3.LauncherPrefs.ICON_STATE;
import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.LauncherApps;
import android.content.pm.LauncherApps.ArchiveCompatibilityParams;
import android.os.UserHandle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.os.BuildCompat;
import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.ModelLauncherCallbacks;
import com.android.launcher3.notification.NotificationListener;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.pm.InstallSessionTracker;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.util.LockedUserState;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.SettingsCache;
import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.TraceHelper;
import com.android.launcher3.widget.custom.CustomWidgetManager;
import app.lawnchair.LawnchairAppKt;
import app.lawnchair.icons.LawnchairIconProvider;
public class LauncherAppState implements SafeCloseable {
public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
// We do not need any synchronization for this variable as its only written on
// UI thread.
public static final MainThreadInitializedObject<LauncherAppState> INSTANCE = new MainThreadInitializedObject<LauncherAppState>(
LauncherAppState::new) {
@Override
protected void onPostInit(Context context) {
super.onPostInit(context);
LawnchairAppKt.getLawnchairApp(context).onLauncherAppStateCreated();
}
};
private Launcher mLauncher;
private final Context mContext;
private final LauncherModel mModel;
private final IconProvider mIconProvider;
private final IconCache mIconCache;
private final InvariantDeviceProfile mInvariantDeviceProfile;
private boolean mIsSafeModeEnabled;
private final RunnableList mOnTerminateCallback = new RunnableList();
public static LauncherAppState getInstance(Context context) {
return INSTANCE.get(context);
}
public static LauncherAppState getInstanceNoCreate() {
return INSTANCE.getNoCreate();
}
public Context getContext() {
return mContext;
}
@SuppressWarnings("NewApi")
public LauncherAppState(Context context) {
this(context, LauncherFiles.APP_ICONS_DB);
Log.v(Launcher.TAG, "LauncherAppState initiated");
Preconditions.assertUIThread();
mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
() -> context.getPackageManager().isSafeMode());
mInvariantDeviceProfile.addOnChangeListener(modelPropertiesChanged -> {
if (modelPropertiesChanged) {
refreshAndReloadLauncher();
}
});
ModelLauncherCallbacks callbacks = mModel.newModelCallbacks();
LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
launcherApps.registerCallback(callbacks);
mOnTerminateCallback.add(() -> mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks));
if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) {
ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
params.setEnableUnarchivalConfirmation(false);
launcherApps.setArchiveCompatibility(params);
}
SimpleBroadcastReceiver modelChangeReceiver = new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
modelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED,
ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
mOnTerminateCallback.add(() -> mContext.unregisterReceiver(modelChangeReceiver));
SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
.addUserEventListener(mModel::onUserEvent);
mOnTerminateCallback.add(userChangeListener::close);
if (enableSmartspaceRemovalToggle()) {
OnSharedPreferenceChangeListener firstPagePinnedItemListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) {
if (SMARTSPACE_ON_HOME_SCREEN.equals(key)) {
mModel.forceReload();
}
}
};
LauncherPrefs.getPrefs(mContext).registerOnSharedPreferenceChangeListener(
firstPagePinnedItemListener);
mOnTerminateCallback.add(() -> LauncherPrefs.getPrefs(mContext)
.unregisterOnSharedPreferenceChangeListener(firstPagePinnedItemListener));
}
LockedUserState.get(context).runOnUserUnlocked(() -> {
CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
cwm.setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts);
mOnTerminateCallback.add(() -> cwm.setWidgetRefreshCallback(null));
IconObserver observer = new IconObserver();
SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
observer, MODEL_EXECUTOR.getHandler());
mOnTerminateCallback.add(iconChangeTracker::close);
MODEL_EXECUTOR.execute(observer::verifyIconChanged);
LauncherPrefs.get(context).addListener(observer, THEMED_ICONS);
mOnTerminateCallback.add(
() -> LauncherPrefs.get(mContext).removeListener(observer, THEMED_ICONS));
InstallSessionTracker installSessionTracker = InstallSessionHelper.INSTANCE.get(context)
.registerInstallTracker(mModel);
mOnTerminateCallback.add(installSessionTracker::unregister);
});
// Register an observer to rebind the notification listener when dots are
// re-enabled.
SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
SettingsCache.OnChangeListener notificationLister = this::onNotificationSettingsChanged;
settingsCache.register(NOTIFICATION_BADGING_URI, notificationLister);
onNotificationSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
mOnTerminateCallback.add(() -> settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationLister));
// Register an observer to notify Launcher about Private Space settings toggle.
registerPrivateSpaceHideWhenLockListener(settingsCache);
}
public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
mContext = context;
mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
mIconProvider = new LawnchairIconProvider(context, Themes.isThemedIconEnabled(context));
mIconCache = new IconCache(mContext, mInvariantDeviceProfile,
iconCacheFileName, mIconProvider);
mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext),
PackageManagerHelper.INSTANCE.get(context), iconCacheFileName != null);
mOnTerminateCallback.add(mIconCache::close);
mOnTerminateCallback.add(mModel::destroy);
}
private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
if (areNotificationDotsEnabled) {
NotificationListener.requestRebind(new ComponentName(
mContext, NotificationListener.class));
}
}
private void registerPrivateSpaceHideWhenLockListener(SettingsCache settingsCache) {
SettingsCache.OnChangeListener psHideWhenLockChangedListener = this::onPrivateSpaceHideWhenLockChanged;
settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psHideWhenLockChangedListener);
mOnTerminateCallback.add(() -> settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI,
psHideWhenLockChangedListener));
}
private void onPrivateSpaceHideWhenLockChanged(boolean isPrivateSpaceHideOnLockEnabled) {
mModel.forceReload();
}
public void reloadIcons() {
refreshAndReloadLauncher();
}
public void clearAllViewsFromHomeScreen() {
final boolean isViewsRemoved =
mLauncher.getModelWriter().clearAllHomeScreenViewsByType(
LauncherSettings.Favorites.CONTAINER_DESKTOP);
if (isViewsRemoved) {
Toast.makeText(
mLauncher,
R.string.home_screen_all_views_removed_msg,
Toast.LENGTH_SHORT
).show();
}
}
private void refreshAndReloadLauncher() {
LauncherIcons.clearPool(mContext);
mIconCache.updateIconParams(
mInvariantDeviceProfile.fillResIconDpi, mInvariantDeviceProfile.iconBitmapSize);
mModel.forceReload();
}
/**
* Call from Application.onTerminate(), which is not guaranteed to ever be
* called.
*/
@Override
public void close() {
mOnTerminateCallback.executeAllAndDestroy();
}
public IconProvider getIconProvider() {
return mIconProvider;
}
public void setLauncher(Launcher launcher) {
mLauncher = launcher;
}
public Launcher getLauncher() {
return mLauncher;
}
public IconCache getIconCache() {
return mIconCache;
}
public LauncherModel getModel() {
return mModel;
}
public InvariantDeviceProfile getInvariantDeviceProfile() {
return mInvariantDeviceProfile;
}
public boolean isSafeModeEnabled() {
return mIsSafeModeEnabled;
}
/**
* Shorthand for {@link #getInvariantDeviceProfile()}
*/
public static InvariantDeviceProfile getIDP(Context context) {
return InvariantDeviceProfile.INSTANCE.get(context);
}
private class IconObserver
implements IconProvider.IconChangeListener, OnSharedPreferenceChangeListener {
@Override
public void onAppIconChanged(String packageName, UserHandle user) {
mModel.onAppIconChanged(packageName, user);
}
@Override
public void onSystemIconStateChanged(String iconState) {
IconShape.INSTANCE.get(mContext).pickBestShape(mContext);
refreshAndReloadLauncher();
LauncherPrefs.get(mContext).put(ICON_STATE, iconState);
}
void verifyIconChanged() {
String iconState = mIconProvider.getSystemIconState();
if (!iconState.equals(LauncherPrefs.get(mContext).get(ICON_STATE))) {
onSystemIconStateChanged(iconState);
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
if (Themes.KEY_THEMED_ICONS.equals(key)) {
mIconProvider.setIconThemeSupported(Themes.isThemedIconEnabled(mContext));
verifyIconChanged();
}
}
}
}
@@ -16,6 +16,7 @@
package com.android.launcher3.model;
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -153,6 +154,27 @@ public class ModelWriter {
throw e;
}
}
/**
* Clears all views from the home screen.
*/
public boolean clearAllHomeScreenViewsByType(int type) {
final ArrayList<ItemInfo> itemsToRemove = new ArrayList<>();
synchronized (mBgDataModel) {
for (ItemInfo item : mBgDataModel.itemsIdMap) {
if (item.container == type) {
itemsToRemove.add(item);
}
}
}
if (itemsToRemove.isEmpty()) {
return false;
}
deleteItemsFromDatabase(itemsToRemove, "clearAllHomeScreenViewsByType");
return true;
}
/**
* Move an item in the DB to a new <container, screen, cellX, cellY>