--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+-->
+<com.android.launcher3.QsbContainerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/qsb_container"
+ android:padding="0dp" />
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:layout_margin="16dp"
+ android:layout_gravity="center_vertical"
+ android:background="@drawable/quantum_panel_shape"
+ android:elevation="2dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="0dp"
+ android:id="@+id/btn_qsb_search"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:paddingStart="16dp"
+ android:text="@string/abandoned_search"
+ android:textColor="@color/quantum_panel_text_color"
+ android:textAppearance="?android:textAppearanceMedium"
+ android:clickable="true"
+ android:background="?android:attr/selectableItemBackground" />
+ <ImageView
+ android:layout_width="48dp"
+ android:id="@+id/btn_qsb_setup"
+ android:clickable="true"
+ android:visibility="gone"
+ android:layout_height="match_parent"
+ android:src="@drawable/ic_setting"
+ android:tint="@color/quantum_panel_text_color"
+ android:contentDescription="@string/gadget_setup_text"
+ android:padding="8dp"
+ android:background="?android:attr/selectableItemBackground" />
+ </LinearLayout>
+</FrameLayout>
\ No newline at end of file
/**
* Indicates whether this item can be reordered. Always true except in the case of the
- * the AllApps button.
+ * the AllApps button and QSB place holder.
*/
public boolean canReorder = true;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
-import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import com.android.launcher3.logging.LoggerUtils;
import com.android.launcher3.logging.UserEventDispatcher;
import com.android.launcher3.model.WidgetsModel;
-import com.android.launcher3.pageindicators.PageIndicator;
import com.android.launcher3.pageindicators.PageIndicatorLine;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.util.ComponentKey;
static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION =
"com.android.launcher3.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION";
+ public static final String ACTION_APPWIDGET_HOST_RESET =
+ "com.android.launcher3.intent.ACTION_APPWIDGET_HOST_RESET";
+
// Type: int
private static final String RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen";
// Type: int
static final String INTRO_SCREEN_DISMISSED = "launcher.intro_screen_dismissed";
static final String FIRST_RUN_ACTIVITY_DISPLAYED = "launcher.first_run_activity_displayed";
- private static final String QSB_WIDGET_ID = "qsb_widget_id";
- private static final String QSB_WIDGET_PROVIDER = "qsb_widget_provider";
-
/** The different states that Launcher can be in. */
enum State { NONE, WORKSPACE, WORKSPACE_SPRING_LOADED, APPS, APPS_SPRING_LOADED,
WIDGETS, WIDGETS_SPRING_LOADED }
private static int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 5;
@Thunk static int NEW_APPS_ANIMATION_DELAY = 500;
- private final BroadcastReceiver mCloseSystemDialogsReceiver
- = new CloseSystemDialogsIntentReceiver();
+ private final BroadcastReceiver mUiBroadcastReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
+ closeSystemDialogs();
+ } else if (ACTION_APPWIDGET_HOST_RESET.equals(intent.getAction())) {
+ if (mAppWidgetHost != null) {
+ mAppWidgetHost.startListening();
+ }
+ }
+ }
+ };
@Thunk Workspace mWorkspace;
private View mLauncherView;
@Thunk WidgetsContainerView mWidgetsView;
@Thunk WidgetsModel mWidgetsModel;
- private AppWidgetHostView mQsb;
-
private Bundle mSavedState;
// We set the state in both onCreate and then onNewIntent in some cases, which causes both
// scroll issues (because the workspace may not have been measured yet) and extra work.
Selection.setSelection(mDefaultKeySsb, 0);
IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
- registerReceiver(mCloseSystemDialogsReceiver, filter);
+ filter.addAction(ACTION_APPWIDGET_HOST_RESET);
+ registerReceiver(mUiBroadcastReceiver, filter);
mRotationEnabled = getResources().getBoolean(R.bool.allow_rotation);
// In case we are on a device with locked rotation, we should look at preferences to check
} else if (resultCode == RESULT_OK) {
addAppWidgetImpl(appWidgetId, mPendingAddInfo, null,
mPendingAddWidgetInfo, ON_ACTIVITY_RESULT_ANIMATION_DELAY);
-
- // When the user has granted permission to bind widgets, we should check to see if
- // we can inflate the default search bar widget.
- getOrCreateQsbBar();
}
return;
} else if (requestCode == REQUEST_PICK_WALLPAPER) {
if (!isWorkspaceLoading()) {
getWorkspace().reinflateWidgetsIfNecessary();
}
- reinflateQSBIfNecessary();
if (DEBUG_RESUME_TIME) {
Log.d(TAG, "Time spent in onResume: " + (System.currentTimeMillis() - startTime));
mWorkspace.setHapticFeedbackEnabled(false);
mWorkspace.setOnLongClickListener(this);
mWorkspace.setup(dragController);
+ mWorkspace.bindAndInitFirstWorkspaceScreen(null /* recycled qsb */);
dragController.addDragListener(mWorkspace);
// Get the search/delete/uninstall bar
dragController.addDropTarget(mWorkspace);
if (mSearchDropTargetBar != null) {
mSearchDropTargetBar.setup(this, dragController);
- mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar());
}
if (mAppInfoDropTargetBar != null) {
mAppInfoDropTargetBar.setup(this, dragController);
((AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE))
.removeAccessibilityStateChangeListener(this);
- unregisterReceiver(mCloseSystemDialogsReceiver);
+ unregisterReceiver(mUiBroadcastReceiver);
LauncherAnimUtils.onDestroyActivity();
appSearchData = new Bundle();
appSearchData.putString("source", "launcher-search");
}
- Rect sourceBounds = new Rect();
- if (mSearchDropTargetBar != null) {
- sourceBounds = mSearchDropTargetBar.getSearchBarBounds();
- }
+
+ // TODO send proper bounds.
+ Rect sourceBounds = null;
boolean clearTextImmediately = startSearch(initialQuery, selectInitialQuery,
appSearchData, sourceBounds);
}
/**
- * Re-listen when widget host is reset.
- */
- @Override
- public void onAppWidgetHostReset() {
- if (mAppWidgetHost != null) {
- mAppWidgetHost.startListening();
- }
-
- // Recreate the QSB, as the widget has been reset.
- bindSearchProviderChanged();
- }
-
- /**
* Launches the intent referred by the clicked shortcut.
*
* @param v The view representing the clicked shortcut.
return (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch());
}
- public View getOrCreateQsbBar() {
- if (launcherCallbacksProvidesSearch()) {
- return mLauncherCallbacks.getQsbBar();
- }
-
- if (mQsb == null) {
- AppWidgetProviderInfo searchProvider = Utilities.getSearchWidgetProvider(this);
- if (searchProvider == null) {
- return null;
- }
-
- Bundle opts = new Bundle();
- opts.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY,
- AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX);
-
- // Determine the min and max dimensions of the widget.
- LauncherAppState app = LauncherAppState.getInstance();
- DeviceProfile portraitProfile = app.getInvariantDeviceProfile().portraitProfile;
- DeviceProfile landscapeProfile = app.getInvariantDeviceProfile().landscapeProfile;
- float density = getResources().getDisplayMetrics().density;
- Point searchDimens = portraitProfile.getSearchBarDimensForWidgetOpts(getResources());
- int maxHeight = (int) (searchDimens.y / density);
- int minHeight = maxHeight;
- int maxWidth = (int) (searchDimens.x / density);
- int minWidth = maxWidth;
- if (!landscapeProfile.isVerticalBarLayout()) {
- searchDimens = landscapeProfile.getSearchBarDimensForWidgetOpts(getResources());
- maxHeight = (int) Math.max(maxHeight, searchDimens.y / density);
- minHeight = (int) Math.min(minHeight, searchDimens.y / density);
- maxWidth = (int) Math.max(maxWidth, searchDimens.x / density);
- minWidth = (int) Math.min(minWidth, searchDimens.x / density);
- }
- opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, maxHeight);
- opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, minHeight);
- opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, maxWidth);
- opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, minWidth);
- if (LOGD) {
- Log.d(TAG, "QSB widget options: maxHeight=" + maxHeight + " minHeight=" + minHeight
- + " maxWidth=" + maxWidth + " minWidth=" + minWidth);
- }
-
- if (mLauncherCallbacks != null) {
- opts.putAll(mLauncherCallbacks.getAdditionalSearchWidgetOptions());
- }
-
- int widgetId = mSharedPrefs.getInt(QSB_WIDGET_ID, -1);
- AppWidgetProviderInfo widgetInfo = mAppWidgetManager.getAppWidgetInfo(widgetId);
- if (!searchProvider.provider.flattenToString().equals(
- mSharedPrefs.getString(QSB_WIDGET_PROVIDER, null))
- || (widgetInfo == null)
- || !widgetInfo.provider.equals(searchProvider.provider)) {
- // A valid widget is not already bound.
- if (widgetId > -1) {
- mAppWidgetHost.deleteAppWidgetId(widgetId);
- widgetId = -1;
- }
-
- // Try to bind a new widget
- widgetId = mAppWidgetHost.allocateAppWidgetId();
-
- if (!AppWidgetManagerCompat.getInstance(this)
- .bindAppWidgetIdIfAllowed(widgetId, searchProvider, opts)) {
- mAppWidgetHost.deleteAppWidgetId(widgetId);
- widgetId = -1;
- }
-
- mSharedPrefs.edit()
- .putInt(QSB_WIDGET_ID, widgetId)
- .putString(QSB_WIDGET_PROVIDER, searchProvider.provider.flattenToString())
- .apply();
- }
-
- mAppWidgetHost.setQsbWidgetId(widgetId);
- if (widgetId != -1) {
- mQsb = mAppWidgetHost.createView(this, widgetId, searchProvider);
- mQsb.setId(R.id.qsb_widget);
- if (!Utilities.containsAll(
- AppWidgetManager.getInstance(this).getAppWidgetOptions(widgetId), opts)) {
- // Launcher should not be updating the options often.
- FileLog.d(TAG, "Options for QSB were not same");
- mQsb.updateAppWidgetOptions(opts);
- }
- mQsb.setPadding(0, 0, 0, 0);
- mSearchDropTargetBar.addView(mQsb);
- mSearchDropTargetBar.setQsbSearchBar(mQsb);
- }
- }
- return mQsb;
- }
-
- private void reinflateQSBIfNecessary() {
- if (mQsb instanceof LauncherAppWidgetHostView &&
- ((LauncherAppWidgetHostView) mQsb).isReinflateRequired()) {
- mSearchDropTargetBar.removeView(mQsb);
- mQsb = null;
- mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar());
- }
- }
-
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
final boolean result = super.dispatchPopulateAccessibilityEvent(event);
}
/**
- * Receives notifications when system dialogs are to be closed.
- */
- @Thunk class CloseSystemDialogsIntentReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- closeSystemDialogs();
- }
- }
-
- /**
* If the activity is currently paused, signal that we need to run the passed Runnable
* in onResume.
*
@Override
public void bindScreens(ArrayList<Long> orderedScreenIds) {
- bindAddScreens(orderedScreenIds);
-
- // If there are no screens, we need to have an empty screen
- if (orderedScreenIds.size() == 0) {
- mWorkspace.addExtraEmptyScreen();
+ // Make sure the first screen is always at the start.
+ if (orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != 0) {
+ orderedScreenIds.remove(Workspace.FIRST_SCREEN_ID);
+ orderedScreenIds.add(0, Workspace.FIRST_SCREEN_ID);
+ mModel.updateWorkspaceScreenOrder(this, orderedScreenIds);
}
+ bindAddScreens(orderedScreenIds);
// Create the custom content page (this call updates mDefaultScreen which calls
// setCurrentPage() so ensure that all pages are added before calling this).
}
}
- @Override
- public void bindAddScreens(ArrayList<Long> orderedScreenIds) {
+ private void bindAddScreens(ArrayList<Long> orderedScreenIds) {
int count = orderedScreenIds.size();
for (int i = 0; i < count; i++) {
- mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(orderedScreenIds.get(i));
+ long screenId = orderedScreenIds.get(i);
+ if (screenId != Workspace.FIRST_SCREEN_ID) {
+ // No need to bind the first screen, as its always bound.
+ mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(screenId);
+ }
}
}
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
ShortcutInfo info = (ShortcutInfo) item;
view = createShortcut(info);
-
- /*
- * TODO: FIX collision case
- */
- if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
- CellLayout cl = mWorkspace.getScreenWithId(item.screenId);
- if (cl != null && cl.isOccupied(item.cellX, item.cellY)) {
- View v = cl.getChildAt(item.cellX, item.cellY);
- Object tag = v.getTag();
- String desc = "Collision while binding workspace item: " + item
- + ". Collides with " + tag;
- if (ProviderConfig.IS_DOGFOOD_BUILD) {
- throw (new RuntimeException(desc));
- } else {
- Log.d(TAG, desc);
- }
- }
- }
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
view = FolderIcon.fromXml(R.layout.folder_icon, this,
throw new RuntimeException("Invalid Item Type");
}
+ /*
+ * Remove colliding items.
+ */
+ if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+ CellLayout cl = mWorkspace.getScreenWithId(item.screenId);
+ if (cl != null && cl.isOccupied(item.cellX, item.cellY)) {
+ View v = cl.getChildAt(item.cellX, item.cellY);
+ Object tag = v.getTag();
+ String desc = "Collision while binding workspace item: " + item
+ + ". Collides with " + tag;
+ if (ProviderConfig.IS_DOGFOOD_BUILD) {
+ throw (new RuntimeException(desc));
+ } else {
+ Log.d(TAG, desc);
+ LauncherModel.deleteItemFromDatabase(this, item);
+ continue;
+ }
+ }
+ }
workspace.addInScreenFromBind(view, item.container, item.screenId, item.cellX,
item.cellY, 1, 1);
if (animateIcons) {
return LauncherCallbacks.SEARCH_BAR_HEIGHT_NORMAL;
}
- public void bindSearchProviderChanged() {
- if (mSearchDropTargetBar == null) {
- return;
- }
- if (mQsb != null) {
- mSearchDropTargetBar.removeView(mQsb);
- mQsb = null;
- }
- mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar());
- }
-
/**
* A runnable that we can dequeue and re-enqueue when all applications are bound (to prevent
* multiple calls to bind the same list.)
// Register intent receivers
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_LOCALE_CHANGED);
- filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
// For handling managed profiles
filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED);
filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED);
private final ArrayList<Runnable> mProviderChangeListeners = new ArrayList<Runnable>();
- private int mQsbWidgetId = -1;
private Launcher mLauncher;
public LauncherAppWidgetHost(Launcher launcher, int hostId) {
mLauncher = launcher;
}
- public void setQsbWidgetId(int widgetId) {
- mQsbWidgetId = widgetId;
- }
-
@Override
protected AppWidgetHostView onCreateView(Context context, int appWidgetId,
AppWidgetProviderInfo appWidget) {
- if (appWidgetId == mQsbWidgetId) {
- return new LauncherAppWidgetHostView(context) {
-
- @Override
- protected View getErrorView() {
- // For the QSB, show an empty view instead of an error view.
- return new View(getContext());
- }
- };
- }
return new LauncherAppWidgetHostView(context);
}
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mChildrenFocused;
+ protected int mErrorViewId = R.layout.appwidget_error;
+
public LauncherAppWidgetHostView(Context context) {
super(context);
mContext = context;
@Override
protected View getErrorView() {
- return mInflater.inflate(R.layout.appwidget_error, this, false);
+ return mInflater.inflate(mErrorViewId, this, false);
}
public void updateLastInflationOrientation() {
package com.android.launcher3;
-import android.app.SearchManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end,
boolean forceAnimateIcons);
public void bindScreens(ArrayList<Long> orderedScreenIds);
- public void bindAddScreens(ArrayList<Long> orderedScreenIds);
public void finishBindingItems();
public void bindAppWidget(LauncherAppWidgetInfo info);
public void bindAllApplications(ArrayList<AppInfo> apps);
public void bindAppInfosRemoved(ArrayList<AppInfo> appInfos);
public void notifyWidgetProvidersChanged();
public void bindWidgetsModel(WidgetsModel model);
- public void bindSearchProviderChanged();
public boolean isAllAppsButtonRank(int rank);
public void onPageBoundSynchronously(int page);
public void executeOnNextDraw(ViewOnDrawExecutor executor);
if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
// If we have changed locale we need to clear out the labels in all apps/workspace.
forceReload();
- } else if (SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED.equals(action)) {
- Callbacks callbacks = getCallback();
- if (callbacks != null) {
- callbacks.bindSearchProviderChanged();
- }
} else if (LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED.equals(action)
|| LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) {
UserManagerCompat.getInstance(context).enableAndResetCache();
}
if (!occupied.containsKey(item.screenId)) {
- occupied.put(item.screenId, new GridOccupancy(countX + 1, countY + 1));
+ GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1);
+ if (item.screenId == Workspace.FIRST_SCREEN_ID) {
+ // Mark the first row as occupied in order to account for the QSB.
+ screen.markCells(0, 0, countX + 1, 1, true);
+ }
+ occupied.put(item.screenId, screen);
}
final GridOccupancy occupancy = occupied.get(item.screenId);
// Database was just created, so wipe any previous widgets
if (mWidgetHostResetHandler != null) {
new AppWidgetHost(mContext, Launcher.APPWIDGET_HOST_ID).deleteHost();
- mWidgetHostResetHandler.sendEmptyMessage(
- ChangeListenerWrapper.MSG_APP_WIDGET_HOST_RESET);
+ mWidgetHostResetHandler.sendMessage(Message.obtain(
+ mWidgetHostResetHandler,
+ ChangeListenerWrapper.MSG_APP_WIDGET_HOST_RESET,
+ mContext
+ ));
}
// Set the flag for empty DB
mListener.onExtractedColorsChanged();
break;
case MSG_APP_WIDGET_HOST_RESET:
- mListener.onAppWidgetHostReset();
+ Context context = (Context) msg.obj;
+ if (context != null) {
+ context.sendBroadcast(new Intent(Launcher.ACTION_APPWIDGET_HOST_RESET)
+ .setPackage(context.getPackageName()));
+ }
break;
}
}
public void onLauncherProviderChange();
public void onExtractedColorsChanged();
-
- public void onAppWidgetHostReset();
}
if (DEBUG) Log.d(TAG, "mParentDownMotionY: " + mParentDownMotionY);
final int pageUnderPointIndex = getNearestHoverOverPageIndex();
- if (pageUnderPointIndex > -1 && pageUnderPointIndex != indexOfChild(mDragView)) {
+ // Do not allow any page to be moved to 0th position.
+ if (pageUnderPointIndex > 0 && pageUnderPointIndex != indexOfChild(mDragView)) {
mTempVisiblePagesRange[0] = 0;
mTempVisiblePagesRange[1] = getPageCount() - 1;
getFreeScrollPageRange(mTempVisiblePagesRange);
public boolean startReordering(View v) {
int dragViewIndex = indexOfChild(v);
- if (mTouchState != TOUCH_STATE_REST || dragViewIndex == -1) return false;
+ // Do not allow the first page to be moved around
+ if (mTouchState != TOUCH_STATE_REST || dragViewIndex <= 0) return false;
mTempVisiblePagesRange[0] = 0;
mTempVisiblePagesRange[1] = getPageCount() - 1;
animateOverviewPanelButtons(goingTowards == OVERVIEW);
} else if (startState == NORMAL) {
animateHotseatAndPageIndicator(goingTowards == NORMAL);
- animateQsb(goingTowards == NORMAL);
}
} else if (threshold == PinchThresholdManager.THRESHOLD_TWO) {
if (startState == OVERVIEW) {
animateHotseatAndPageIndicator(goingTowards == NORMAL);
- animateQsb(goingTowards == NORMAL);
animateScrim(goingTowards == OVERVIEW);
} else if (startState == NORMAL) {
animateOverviewPanelButtons(goingTowards == OVERVIEW);
}
}
- private void animateQsb(boolean show) {
- SearchDropTargetBar.State searchBarState = show ? SearchDropTargetBar.State.SEARCH_BAR
- : SearchDropTargetBar.State.INVISIBLE;
- mLauncher.getSearchDropTargetBar().animateToState(searchBarState, THRESHOLD_ANIM_DURATION);
- }
-
private void animateOverviewPanelButtons(boolean show) {
animateShowHideView(INDEX_OVERVIEW_PANEL_BUTTONS, mLauncher.getOverviewPanel(), show);
}
--- /dev/null
+/*
+ * Copyright (C) 2016 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;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.SearchManager;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.compat.AppWidgetManagerCompat;
+
+/**
+ * A frame layout which contains a QSB. This internally uses fragment to bind the view, which
+ * allows it to contain the logic for {@link Fragment#startActivityForResult(Intent, int)}.
+ */
+public class QsbContainerView extends FrameLayout {
+
+ private boolean mBound;
+
+ public QsbContainerView(Context context) {
+ super(context);
+ }
+
+ public QsbContainerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public QsbContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (!mBound) {
+ FragmentManager fm = ((Launcher) getContext()).getFragmentManager();
+ fm.beginTransaction().add(R.id.qsb_container, new QsbFragment()).commit();
+ mBound = true;
+ }
+ }
+
+ @Override
+ public void setPadding(int left, int top, int right, int bottom) {
+ super.setPadding(0, 0, 0, 0);
+ }
+
+ /**
+ * A fragment to display the QSB.
+ */
+ public static class QsbFragment extends Fragment implements View.OnClickListener {
+
+ private static final int REQUEST_BIND_QSB = 1;
+ private static final String QSB_WIDGET_ID = "qsb_widget_id";
+
+ private static int sSavedWidgetId = -1;
+
+ private AppWidgetProviderInfo mWidgetInfo;
+ private LauncherAppWidgetHostView mQsb;
+
+ private BroadcastReceiver mRebindReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ rebindFragment();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ IntentFilter filter = new IntentFilter(Launcher.ACTION_APPWIDGET_HOST_RESET);
+ filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
+ getContext().registerReceiver(mRebindReceiver, filter);
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ if (savedInstanceState != null) {
+ sSavedWidgetId = savedInstanceState.getInt(QSB_WIDGET_ID, -1);
+ }
+
+ Launcher launcher = (Launcher) getActivity();
+ mWidgetInfo = getSearchWidgetProvider(launcher);
+ if (mWidgetInfo == null) {
+ // There is no search provider, just show the default widget.
+ return getDefaultView(inflater, container, false);
+ }
+
+ SharedPreferences prefs = Utilities.getPrefs(launcher);
+ AppWidgetManagerCompat widgetManager = AppWidgetManagerCompat.getInstance(launcher);
+ LauncherAppWidgetHost widgetHost = launcher.getAppWidgetHost();
+ InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile();
+
+ Bundle opts = new Bundle();
+ Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(launcher, idp.numColumns, 1, null);
+ opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, size.left);
+ opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, size.top);
+ opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, size.right);
+ opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, size.bottom);
+
+ int widgetId = prefs.getInt(QSB_WIDGET_ID, -1);
+ AppWidgetProviderInfo widgetInfo = widgetManager.getAppWidgetInfo(widgetId);
+ boolean isWidgetBound = (widgetInfo != null) &&
+ widgetInfo.provider.equals(mWidgetInfo.provider);
+
+ if (!isWidgetBound) {
+ // widgetId is already bound and its not the correct provider.
+ // Delete the widget id.
+ if (widgetId > -1) {
+ widgetHost.deleteAppWidgetId(widgetId);
+ widgetId = -1;
+ }
+
+ widgetId = widgetHost.allocateAppWidgetId();
+ isWidgetBound = widgetManager.bindAppWidgetIdIfAllowed(widgetId, mWidgetInfo, opts);
+ if (!isWidgetBound) {
+ widgetHost.deleteAppWidgetId(widgetId);
+ widgetId = -1;
+ }
+ }
+
+ if (isWidgetBound) {
+ mQsb = (LauncherAppWidgetHostView)
+ widgetHost.createView(launcher, widgetId, mWidgetInfo);
+ mQsb.setId(R.id.qsb_widget);
+ mQsb.mErrorViewId = R.layout.qsb_default_view;
+
+ if (!Utilities.containsAll(AppWidgetManager.getInstance(launcher)
+ .getAppWidgetOptions(widgetId), opts)) {
+ mQsb.updateAppWidgetOptions(opts);
+ }
+ mQsb.setPadding(0, 0, 0, 0);
+ return mQsb;
+ }
+
+ // Return a default widget with setup icon.
+ return getDefaultView(inflater, container, true);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view.getId() == R.id.btn_qsb_search) {
+ getActivity().startSearch("", false, null, true);
+ } else if (view.getId() == R.id.btn_qsb_setup) {
+ // Allocate a new widget id for QSB
+ sSavedWidgetId = ((Launcher) getActivity())
+ .getAppWidgetHost().allocateAppWidgetId();
+ // Start intent for bind the widget
+ Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, sSavedWidgetId);
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, mWidgetInfo.provider);
+ startActivityForResult(intent, REQUEST_BIND_QSB);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(QSB_WIDGET_ID, sSavedWidgetId);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_BIND_QSB) {
+ if (resultCode == Activity.RESULT_OK) {
+ int widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ sSavedWidgetId);
+ Utilities.getPrefs(getContext()).edit().putInt(QSB_WIDGET_ID, widgetId).apply();
+ sSavedWidgetId = -1;
+ rebindFragment();
+ } else if (sSavedWidgetId != -1) {
+ ((Launcher) getActivity()).getAppWidgetHost().deleteAppWidgetId(sSavedWidgetId);
+ sSavedWidgetId = -1;
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mQsb != null && mQsb.isReinflateRequired()) {
+ rebindFragment();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ getContext().unregisterReceiver(mRebindReceiver);
+ super.onDestroy();
+ }
+
+ private void rebindFragment() {
+ if (getActivity() != null) {
+ // Recreate the fragment. This will cause the qsb to be inflated again.
+ getActivity().getFragmentManager().beginTransaction()
+ .replace(R.id.qsb_container, new QsbFragment()).commit();
+ }
+ }
+
+ private View getDefaultView(LayoutInflater inflater, ViewGroup parent, boolean showSetup) {
+ View v = inflater.inflate(R.layout.qsb_default_view, parent, false);
+ if (showSetup) {
+ View setupButton = v.findViewById(R.id.btn_qsb_setup);
+ setupButton.setVisibility(View.VISIBLE);
+ setupButton.setOnClickListener(this);
+ }
+ v.findViewById(R.id.btn_qsb_search).setOnClickListener(this);
+ return v;
+ }
+ }
+
+ /**
+ * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX}
+ * provided by the same package which is set to be global search activity.
+ * If widgetCategory is not supported, or no such widget is found, returns the first widget
+ * provided by the package.
+ */
+ public static AppWidgetProviderInfo getSearchWidgetProvider(Context context) {
+ SearchManager searchManager =
+ (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
+ ComponentName searchComponent = searchManager.getGlobalSearchActivity();
+ if (searchComponent == null) return null;
+ String providerPkg = searchComponent.getPackageName();
+
+ AppWidgetProviderInfo defaultWidgetForSearchPackage = null;
+
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) {
+ if (info.provider.getPackageName().equals(providerPkg) && info.configure == null) {
+ if ((info.widgetCategory & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) {
+ return info;
+ } else if (defaultWidgetForSearchPackage == null) {
+ defaultWidgetForSearchPackage = info;
+ }
+ }
+ }
+ return defaultWidgetForSearchPackage;
+ }
+}
package com.android.launcher3;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewDebug;
-import android.view.accessibility.AccessibilityManager;
-import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import com.android.launcher3.dragndrop.DragController;
*/
public class SearchDropTargetBar extends BaseDropTargetBar {
- private static final TimeInterpolator MOVE_DOWN_INTERPOLATOR = new DecelerateInterpolator(0.6f);
- private static final TimeInterpolator MOVE_UP_INTERPOLATOR = new DecelerateInterpolator(1.5f);
-
/** The different states that the search bar space can be in. */
public enum State {
- INVISIBLE (0f, 0f, 0f),
- INVISIBLE_TRANSLATED (0f, 0f, -1f),
- SEARCH_BAR (1f, 0f, 0f),
- DROP_TARGET (0f, 1f, 0f);
+ INVISIBLE (0f),
+ DROP_TARGET (1f);
- private final float mSearchBarAlpha;
private final float mDropTargetBarAlpha;
- private final float mTranslation;
- State(float sbAlpha, float dtbAlpha, float translation) {
- mSearchBarAlpha = sbAlpha;
+ State(float dtbAlpha) {
mDropTargetBarAlpha = dtbAlpha;
- mTranslation = translation;
}
-
}
@ViewDebug.ExportedProperty(category = "launcher")
- private State mState = State.SEARCH_BAR;
- @Thunk View mQSB;
+ private State mState = State.INVISIBLE;
// Drop targets
private ButtonDropTarget mDeleteDropTarget;
@Override
public void hideDropTargets() {
- animateToState(State.SEARCH_BAR, DEFAULT_DRAG_FADE_DURATION);
- }
-
- public void setQsbSearchBar(View qsb) {
- mQSB = qsb;
+ animateToState(State.INVISIBLE, DEFAULT_DRAG_FADE_DURATION);
}
/**
AlphaUpdateListener.updateVisibility(mDropTargetBar, mAccessibilityEnabled);
}
- if (mQSB != null) {
- boolean isVertical = ((Launcher) getContext()).getDeviceProfile()
- .isVerticalBarLayout();
- float translation = isVertical ? 0 : mState.mTranslation * getMeasuredHeight();
-
- if (duration > 0) {
- int translationChange = Float.compare(mQSB.getTranslationY(), translation);
-
- animateAlpha(mQSB, mState.mSearchBarAlpha,
- translationChange == 0 ? DEFAULT_INTERPOLATOR
- : (translationChange < 0 ? MOVE_DOWN_INTERPOLATOR
- : MOVE_UP_INTERPOLATOR));
-
- if (translationChange != 0) {
- mCurrentAnimation.play(
- ObjectAnimator.ofFloat(mQSB, View.TRANSLATION_Y, translation));
- }
- } else {
- mQSB.setTranslationY(translation);
- mQSB.setAlpha(mState.mSearchBarAlpha);
- AlphaUpdateListener.updateVisibility(mQSB, mAccessibilityEnabled);
- }
- }
-
// Start the final animation
if (duration > 0) {
if (animation != null) {
}
}
- /**
- * @return the bounds of the QSB search bar.
- */
- public Rect getSearchBarBounds() {
- if (mQSB != null) {
- final int[] pos = new int[2];
- mQSB.getLocationOnScreen(pos);
-
- final Rect rect = new Rect();
- rect.left = pos[0];
- rect.top = pos[1];
- rect.right = pos[0] + mQSB.getWidth();
- rect.bottom = pos[1] + mQSB.getHeight();
- return rect;
- } else {
- return null;
- }
- }
-
@Override
public void enableAccessibleDrag(boolean enable) {
- if (mQSB != null) {
- mQSB.setVisibility(enable ? View.GONE : View.VISIBLE);
- }
mDeleteDropTarget.enableAccessibleDrag(enable);
mUninstallDropTarget.enableAccessibleDrag(enable);
}
import android.annotation.TargetApi;
import android.app.Activity;
-import android.app.SearchManager;
import android.app.WallpaperManager;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProviderInfo;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
}
/**
- * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX}
- * provided by the same package which is set to be global search activity.
- * If widgetCategory is not supported, or no such widget is found, returns the first widget
- * provided by the package.
- */
- @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
- public static AppWidgetProviderInfo getSearchWidgetProvider(Context context) {
- SearchManager searchManager =
- (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
- ComponentName searchComponent = searchManager.getGlobalSearchActivity();
- if (searchComponent == null) return null;
- String providerPkg = searchComponent.getPackageName();
-
- AppWidgetProviderInfo defaultWidgetForSearchPackage = null;
-
- AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
- for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) {
- if (info.provider.getPackageName().equals(providerPkg)) {
- if (ATLEAST_JB_MR1) {
- if ((info.widgetCategory & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) {
- return info;
- } else if (defaultWidgetForSearchPackage == null) {
- defaultWidgetForSearchPackage = info;
- }
- } else {
- return info;
- }
- }
- }
- return defaultWidgetForSearchPackage;
- }
-
- /**
* Compresses the bitmap to a byte array for serialization.
*/
public static byte[] flattenBitmap(Bitmap bitmap) {
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.pageindicators.PageIndicator;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.LongArrayMap;
private static final boolean MAP_RECURSE = true;
// The screen id used for the empty screen always present to the right.
- public final static long EXTRA_EMPTY_SCREEN_ID = -201;
+ public static final long EXTRA_EMPTY_SCREEN_ID = -201;
+ // The is the first screen. It is always present, even if its empty.
+ public static final long FIRST_SCREEN_ID = 0;
+
private final static long CUSTOM_CONTENT_SCREEN_ID = -301;
private static final long CUSTOM_CONTENT_GESTURE_DELAY = 200;
// in all apps or customize mode)
enum State {
- NORMAL (SearchDropTargetBar.State.SEARCH_BAR, false, false),
- NORMAL_HIDDEN (SearchDropTargetBar.State.INVISIBLE_TRANSLATED, false, false),
+ NORMAL (SearchDropTargetBar.State.INVISIBLE, false, false),
+ NORMAL_HIDDEN (SearchDropTargetBar.State.INVISIBLE, false, false),
SPRING_LOADED (SearchDropTargetBar.State.DROP_TARGET, false, true),
OVERVIEW (SearchDropTargetBar.State.INVISIBLE, true, true),
OVERVIEW_HIDDEN (SearchDropTargetBar.State.INVISIBLE, true, false);
return mTouchState != TOUCH_STATE_REST;
}
+ /**
+ * Initializes and binds the first page
+ * @param qsb an exisitng qsb to recycle or null.
+ */
+ public void bindAndInitFirstWorkspaceScreen(View qsb) {
+ // Add the first page
+ CellLayout firstPage = insertNewWorkspaceScreen(Workspace.FIRST_SCREEN_ID, 0);
+
+ if (qsb == null) {
+ // Always add a QSB on the first screen.
+ qsb = mLauncher.getLayoutInflater().inflate(R.layout.qsb_container,
+ firstPage, false);
+ }
+
+ CellLayout.LayoutParams lp = (CellLayout.LayoutParams) qsb.getLayoutParams();
+ lp.cellX = 0;
+ lp.cellY = 0;
+ lp.cellHSpan = firstPage.getCountX();
+ lp.cellVSpan = 1;
+ lp.canReorder = false;
+
+ if (!firstPage.addViewToCellLayout(qsb, 0, R.id.qsb_container, lp, true)) {
+ Log.e(TAG, "Failed to add to item at (0, 0) to CellLayout");
+ }
+ }
+
public void removeAllWorkspaceScreens() {
// Disable all layout transitions before removing all pages to ensure that we don't get the
// transition animations competing with us changing the scroll when we add pages or the
removeCustomContentPage();
}
+ // Recycle the QSB widget
+ View qsb = findViewById(R.id.qsb_container);
+ if (qsb != null) {
+ ((ViewGroup) qsb.getParent()).removeView(qsb);
+ }
+
// Remove the pages and clear the screen models
removeAllViews();
mScreenOrder.clear();
mWorkspaceScreens.clear();
+ // Ensure that the first page is always present
+ bindAndInitFirstWorkspaceScreen(qsb);
+
// Re-enable the layout transitions
enableLayoutTransitions();
}
- public long insertNewWorkspaceScreenBeforeEmptyScreen(long screenId) {
+ public void insertNewWorkspaceScreenBeforeEmptyScreen(long screenId) {
// Find the index to insert this view into. If the empty screen exists, then
// insert it before that.
int insertIndex = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID);
if (insertIndex < 0) {
insertIndex = mScreenOrder.size();
}
- return insertNewWorkspaceScreen(screenId, insertIndex);
+ insertNewWorkspaceScreen(screenId, insertIndex);
}
- public long insertNewWorkspaceScreen(long screenId) {
- return insertNewWorkspaceScreen(screenId, getChildCount());
+ public void insertNewWorkspaceScreen(long screenId) {
+ insertNewWorkspaceScreen(screenId, getChildCount());
}
- public long insertNewWorkspaceScreen(long screenId, int insertIndex) {
+ public CellLayout insertNewWorkspaceScreen(long screenId, int insertIndex) {
if (mWorkspaceScreens.containsKey(screenId)) {
throw new RuntimeException("Screen id " + screenId + " already exists!");
}
if (delegate != null && delegate.isInAccessibleDrag()) {
newScreen.enableAccessibleDrag(true, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG);
}
- return screenId;
+
+ return newScreen;
}
public void createCustomContentContainer() {
for (int i = 0; i < total; i++) {
long id = mWorkspaceScreens.keyAt(i);
CellLayout cl = mWorkspaceScreens.valueAt(i);
- if (id >= 0 && cl.getShortcutsAndWidgets().getChildCount() == 0) {
+ // FIRST_SCREEN_ID can never be removed.
+ if (id > FIRST_SCREEN_ID && cl.getShortcutsAndWidgets().getChildCount() == 0) {
removeScreens.add(id);
}
}
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
import com.android.launcher3.backup.nano.BackupProtos;
import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.compat.PackageInstallerCompat;
// {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed,
// break the loop and abort migration by throwing an exception.
OptimalPlacementSolution placement = new OptimalPlacementSolution(
- new GridOccupancy(mTrgX, mTrgY), deepCopy(mCarryOver), true);
+ new GridOccupancy(mTrgX, mTrgY), deepCopy(mCarryOver), 0, true);
placement.find();
if (placement.finalPlacedItems.size() > 0) {
long newScreenId = LauncherSettings.Settings.call(
* Migrate a particular screen id.
* Strategy:
* 1) For all possible combinations of row and column, pick the one which causes the least
- * data loss: {@link #tryRemove(int, int, ArrayList, float[])}
+ * data loss: {@link #tryRemove(int, int, int, ArrayList, float[])}
* 2) Maintain a list of all lost items before this screen, and add any new item lost from
* this screen to that list as well.
* 3) If all those items from the above list can be placed on this screen, place them
* (otherwise they are placed on a new screen).
*/
private void migrateScreen(long screenId) {
+ // If we are migrating the first screen, do not touch the first row.
+ int startY = screenId == Workspace.FIRST_SCREEN_ID ? 1 : 0;
+
ArrayList<DbEntry> items = loadWorkspaceEntries(screenId);
int removedCol = Integer.MAX_VALUE;
// Try removing all possible combinations
for (int x = 0; x < mSrcX; x++) {
- for (int y = 0; y < mSrcY; y++) {
+ for (int y = startY; y < mSrcY; y++) {
// Use a deep copy when trying out a particular combination as it can change
// the underlying object.
- ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, deepCopy(items), outLoss);
+ ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, startY, deepCopy(items), outLoss);
if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1] < moveWt))) {
removeWt = outLoss[0];
if (!mCarryOver.isEmpty() && removeWt == 0) {
// No new items were removed in this step. Try placing all the items on this screen.
GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY);
+ occupied.markCells(0, 0, mTrgX, startY, true);
for (DbEntry item : finalItems) {
occupied.markCells(item, true);
}
OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied,
- deepCopy(mCarryOver), true);
+ deepCopy(mCarryOver), startY, true);
placement.find();
if (placement.lowestWeightLoss == 0) {
// All items got placed
* @param outLoss array of size 2. The first entry is filled with weight loss, and the second
* with the overall item movement.
*/
- private ArrayList<DbEntry> tryRemove(int col, int row, ArrayList<DbEntry> items,
- float[] outLoss) {
+ private ArrayList<DbEntry> tryRemove(int col, int row, int startY,
+ ArrayList<DbEntry> items, float[] outLoss) {
GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY);
+ occupied.markCells(0, 0, mTrgX, startY, true);
col = mShouldRemoveX ? col : Integer.MAX_VALUE;
row = mShouldRemoveY ? row : Integer.MAX_VALUE;
}
}
- OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied, removedItems);
+ OptimalPlacementSolution placement =
+ new OptimalPlacementSolution(occupied, removedItems, startY);
placement.find();
finalItems.addAll(placement.finalPlacedItems);
outLoss[0] = placement.lowestWeightLoss;
// linear placement.
private final boolean ignoreMove;
+ // The first row in the grid from where the placement should start.
+ private final int startY;
+
float lowestWeightLoss = Float.MAX_VALUE;
float lowestMoveCost = Float.MAX_VALUE;
ArrayList<DbEntry> finalPlacedItems;
- public OptimalPlacementSolution(GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace) {
- this(occupied, itemsToPlace, false);
+ public OptimalPlacementSolution(
+ GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace, int startY) {
+ this(occupied, itemsToPlace, startY, false);
}
public OptimalPlacementSolution(GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace,
- boolean ignoreMove) {
+ int startY, boolean ignoreMove) {
this.occupied = occupied;
this.itemsToPlace = itemsToPlace;
this.ignoreMove = ignoreMove;
+ this.startY = startY;
// Sort the items such that larger widgets appear first followed by 1x1 items
Collections.sort(this.itemsToPlace);
int myW = me.spanX;
int myH = me.spanY;
- for (int y = 0; y < mTrgY; y++) {
+ for (int y = startY; y < mTrgY; y++) {
for (int x = 0; x < mTrgX; x++) {
float newMoveCost = moveCost;
if (x != myX) {
int newDistance = Integer.MAX_VALUE;
int newX = Integer.MAX_VALUE, newY = Integer.MAX_VALUE;
- for (int y = 0; y < mTrgY; y++) {
+ for (int y = startY; y < mTrgY; y++) {
for (int x = 0; x < mTrgX; x++) {
if (!occupied.cells[x][y]) {
int dist = ignoreMove ? 0 :
public void markCells(int cellX, int cellY, int spanX, int spanY, boolean value) {
if (cellX < 0 || cellY < 0) return;
for (int x = cellX; x < cellX + spanX && x < mCountX; x++) {
- for (int y = cellY; y < cellY + spanY && y < mCountX; y++) {
+ for (int y = cellY; y < cellY + spanY && y < mCountY; y++) {
cells[x][y] = value;
}
}
*/
private void setupAndVerifyContents(
LauncherAppWidgetInfo item, Class<?> widgetClass, String desc) {
- // Add new screen
- long screenId = LauncherSettings.Settings.call(
- mResolver, LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
- .getLong(LauncherSettings.Settings.EXTRA_VALUE);
+ long screenId = Workspace.FIRST_SCREEN_ID;
+ // Update the screen id counter for the provider.
+ LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
+
+ if (screenId > Workspace.FIRST_SCREEN_ID) {
+ screenId = Workspace.FIRST_SCREEN_ID;
+ }
ContentValues v = new ContentValues();
v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, 0);
item.minSpanY = info.minSpanY;
item.user = mWidgetManager.getUser(info);
item.cellX = 0;
- item.cellY = 0;
+ item.cellY = 1;
item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
if (bindWidget) {
item.minSpanX = 2;
item.minSpanY = 2;
item.cellX = 0;
- item.cellY = 0;
+ item.cellY = 1;
item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
return item;
}
}});
}
+ public void testWorkspace_first_row_blocked() throws Exception {
+ // The first screen has one item on the 4th column which needs moving, as the first row
+ // will be kept empty.
+ long[][][] ids = createGrid(new int[][][]{{
+ { -1, -1, -1, -1},
+ { 3, 1, 7, 0},
+ { 8, 7, 7, -1},
+ { 5, 2, 7, -1},
+ }}, 0);
+
+ new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, new HashMap<String, Point>(),
+ new Point(4, 4), new Point(3, 4)).migrateWorkspace();
+
+ // Items in the second column of the first screen should get placed on a new screen.
+ verifyWorkspace(new long[][][] {{
+ { -1, -1, -1},
+ {ids[0][1][0], ids[0][1][1], ids[0][1][2]},
+ {ids[0][2][0], ids[0][2][1], ids[0][2][2]},
+ {ids[0][3][0], ids[0][3][1], ids[0][3][2]},
+ }, {
+ {ids[0][1][3]},
+ }});
+ }
+
+ public void testWorkspace_items_moved_to_empty_first_row() throws Exception {
+ // Items will get moved to the next screen to keep the first screen empty.
+ long[][][] ids = createGrid(new int[][][]{{
+ { -1, -1, -1, -1},
+ { 0, 1, 0, 0},
+ { 8, 7, 7, -1},
+ { 5, 6, 7, -1},
+ }}, 0);
+
+ new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, new HashMap<String, Point>(),
+ new Point(4, 4), new Point(3, 3)).migrateWorkspace();
+
+ // Items in the second column of the first screen should get placed on a new screen.
+ verifyWorkspace(new long[][][] {{
+ { -1, -1, -1},
+ {ids[0][2][0], ids[0][2][1], ids[0][2][2]},
+ {ids[0][3][0], ids[0][3][1], ids[0][3][2]},
+ }, {
+ {ids[0][1][1], ids[0][1][0], ids[0][1][2]},
+ {ids[0][1][3]},
+ }});
+ }
+
+ private long[][][] createGrid(int[][][] typeArray) throws Exception {
+ return createGrid(typeArray, 1);
+ }
+
/**
* Initializes the DB with dummy elements to represent the provided grid structure.
* @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
* two represent the workspace grid.
* @return the same grid representation where each entry is the corresponding item id.
*/
- private long[][][] createGrid(int[][][] typeArray) throws Exception {
+ private long[][][] createGrid(int[][][] typeArray, long startScreen) throws Exception {
+ LauncherSettings.Settings.call(getMockContentResolver(),
+ LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
long[][][] ids = new long[typeArray.length][][];
for (int i = 0; i < typeArray.length; i++) {
// Add screen to DB
- long screenId = LauncherSettings.Settings.call(getMockContentResolver(),
- LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
- .getLong(LauncherSettings.Settings.EXTRA_VALUE);
+ long screenId = startScreen + i;
+
+ // Keep the screen id counter up to date
+ LauncherSettings.Settings.call(getMockContentResolver(),
+ LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
ContentValues v = new ContentValues();
v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
} else {
assertEquals(1, c.getCount());
c.moveToNext();
- assertEquals(id, c.getLong(0));
+ assertEquals(String.format("Failed to verify item ad %d %d, %d", i, y, x),
+ id, c.getLong(0));
total++;
}
c.close();