1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.launcher3.widget;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Rect;
22import android.graphics.drawable.Drawable;
23import android.support.v7.widget.LinearLayoutManager;
24import android.support.v7.widget.RecyclerView.State;
25import android.util.AttributeSet;
26import android.util.Log;
27import android.view.View;
28import android.widget.Toast;
29
30import com.android.launcher3.BaseContainerView;
31import com.android.launcher3.CellLayout;
32import com.android.launcher3.DeleteDropTarget;
33import com.android.launcher3.DeviceProfile;
34import com.android.launcher3.DragController;
35import com.android.launcher3.DragSource;
36import com.android.launcher3.DropTarget.DragObject;
37import com.android.launcher3.Folder;
38import com.android.launcher3.IconCache;
39import com.android.launcher3.ItemInfo;
40import com.android.launcher3.Launcher;
41import com.android.launcher3.LauncherAppState;
42import com.android.launcher3.PendingAddItemInfo;
43import com.android.launcher3.R;
44import com.android.launcher3.Utilities;
45import com.android.launcher3.WidgetPreviewLoader;
46import com.android.launcher3.Workspace;
47import com.android.launcher3.model.WidgetsModel;
48import com.android.launcher3.util.Thunk;
49
50/**
51 * The widgets list view container.
52 */
53public class WidgetsContainerView extends BaseContainerView
54        implements View.OnLongClickListener, View.OnClickListener, DragSource {
55    private static final String TAG = "WidgetsContainerView";
56    private static final boolean LOGD = false;
57
58    /* Global instances that are used inside this container. */
59    @Thunk Launcher mLauncher;
60    private DragController mDragController;
61    private IconCache mIconCache;
62
63    /* Recycler view related member variables */
64    private WidgetsRecyclerView mRecyclerView;
65    private WidgetsListAdapter mAdapter;
66
67    /* Touch handling related member variables. */
68    private Toast mWidgetInstructionToast;
69
70    /* Rendering related. */
71    private WidgetPreviewLoader mWidgetPreviewLoader;
72
73    public WidgetsContainerView(Context context) {
74        this(context, null);
75    }
76
77    public WidgetsContainerView(Context context, AttributeSet attrs) {
78        this(context, attrs, 0);
79    }
80
81    public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
82        super(context, attrs, defStyleAttr);
83        mLauncher = (Launcher) context;
84        mDragController = mLauncher.getDragController();
85        mAdapter = new WidgetsListAdapter(context, this, this, mLauncher);
86        mIconCache = (LauncherAppState.getInstance()).getIconCache();
87        if (LOGD) {
88            Log.d(TAG, "WidgetsContainerView constructor");
89        }
90    }
91
92    @Override
93    protected void onFinishInflate() {
94        super.onFinishInflate();
95        mRecyclerView = (WidgetsRecyclerView) getContentView().findViewById(R.id.widgets_list_view);
96        mRecyclerView.setAdapter(mAdapter);
97        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
98    }
99
100    //
101    // Returns views used for launcher transitions.
102    //
103
104    public void scrollToTop() {
105        mRecyclerView.scrollToPosition(0);
106    }
107
108    //
109    // Touch related handling.
110    //
111
112    @Override
113    public void onClick(View v) {
114        // When we have exited widget tray or are in transition, disregard clicks
115        if (!mLauncher.isWidgetsViewVisible()
116                || mLauncher.getWorkspace().isSwitchingState()
117                || !(v instanceof WidgetCell)) return;
118
119        // Let the user know that they have to long press to add a widget
120        if (mWidgetInstructionToast != null) {
121            mWidgetInstructionToast.cancel();
122        }
123
124        CharSequence msg = Utilities.wrapForTts(
125                getContext().getText(R.string.long_press_widget_to_add),
126                getContext().getString(R.string.long_accessible_way_to_add));
127        mWidgetInstructionToast = Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT);
128        mWidgetInstructionToast.show();
129    }
130
131    @Override
132    public boolean onLongClick(View v) {
133        if (LOGD) {
134            Log.d(TAG, String.format("onLonglick [v=%s]", v));
135        }
136        // Return early if this is not initiated from a touch
137        if (!v.isInTouchMode()) return false;
138        // When we have exited all apps or are in transition, disregard long clicks
139        if (!mLauncher.isWidgetsViewVisible() ||
140                mLauncher.getWorkspace().isSwitchingState()) return false;
141        // Return if global dragging is not enabled
142        if (!mLauncher.isDraggingEnabled()) return false;
143
144        boolean status = beginDragging(v);
145        if (status && v.getTag() instanceof PendingAddWidgetInfo) {
146            WidgetHostViewLoader hostLoader = new WidgetHostViewLoader(mLauncher, v);
147            boolean preloadStatus = hostLoader.preloadWidget();
148            if (LOGD) {
149                Log.d(TAG, String.format("preloading widget [status=%s]", preloadStatus));
150            }
151            mLauncher.getDragController().addDragListener(hostLoader);
152        }
153        return status;
154    }
155
156    private boolean beginDragging(View v) {
157        if (v instanceof WidgetCell) {
158            if (!beginDraggingWidget((WidgetCell) v)) {
159                return false;
160            }
161        } else {
162            Log.e(TAG, "Unexpected dragging view: " + v);
163        }
164
165        // We don't enter spring-loaded mode if the drag has been cancelled
166        if (mLauncher.getDragController().isDragging()) {
167            // Go into spring loaded mode (must happen before we startDrag())
168            mLauncher.enterSpringLoadedDragMode();
169        }
170
171        return true;
172    }
173
174    private boolean beginDraggingWidget(WidgetCell v) {
175        // Get the widget preview as the drag representation
176        WidgetImageView image = (WidgetImageView) v.findViewById(R.id.widget_preview);
177        PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag();
178
179        // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and
180        // we abort the drag.
181        if (image.getBitmap() == null) {
182            return false;
183        }
184
185        // Compose the drag image
186        Bitmap preview;
187        float scale = 1f;
188        final Rect bounds = image.getBitmapBounds();
189
190        if (createItemInfo instanceof PendingAddWidgetInfo) {
191            // This can happen in some weird cases involving multi-touch. We can't start dragging
192            // the widget if this is null, so we break out.
193
194            PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) createItemInfo;
195            int[] size = mLauncher.getWorkspace().estimateItemSize(createWidgetInfo, true);
196
197            Bitmap icon = image.getBitmap();
198            float minScale = 1.25f;
199            int maxWidth = Math.min((int) (icon.getWidth() * minScale), size[0]);
200
201            int[] previewSizeBeforeScale = new int[1];
202            preview = getWidgetPreviewLoader().generateWidgetPreview(mLauncher,
203                    createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale);
204
205            if (previewSizeBeforeScale[0] < icon.getWidth()) {
206                // The icon has extra padding around it.
207                int padding = (icon.getWidth() - previewSizeBeforeScale[0]) / 2;
208                if (icon.getWidth() > image.getWidth()) {
209                    padding = padding * image.getWidth() / icon.getWidth();
210                }
211
212                bounds.left += padding;
213                bounds.right -= padding;
214            }
215            scale = bounds.width() / (float) preview.getWidth();
216        } else {
217            PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag();
218            Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.activityInfo);
219            preview = Utilities.createIconBitmap(icon, mLauncher);
220            createItemInfo.spanX = createItemInfo.spanY = 1;
221            scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / preview.getWidth();
222        }
223
224        // Don't clip alpha values for the drag outline if we're using the default widget preview
225        boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo &&
226                (((PendingAddWidgetInfo) createItemInfo).previewImage == 0));
227
228        // Start the drag
229        mLauncher.lockScreenOrientation();
230        mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, preview, clipAlpha);
231        mDragController.startDrag(image, preview, this, createItemInfo,
232                bounds, DragController.DRAG_ACTION_COPY, scale);
233
234        preview.recycle();
235        return true;
236    }
237
238    //
239    // Drag related handling methods that implement {@link DragSource} interface.
240    //
241
242    @Override
243    public boolean supportsFlingToDelete() {
244        return false;
245    }
246
247    @Override
248    public boolean supportsAppInfoDropTarget() {
249        return true;
250    }
251
252    /*
253     * Both this method and {@link #supportsFlingToDelete} has to return {@code false} for the
254     * {@link DeleteDropTarget} to be invisible.)
255     */
256    @Override
257    public boolean supportsDeleteDropTarget() {
258        return false;
259    }
260
261    @Override
262    public float getIntrinsicIconScaleFactor() {
263        return 0;
264    }
265
266    @Override
267    public void onFlingToDeleteCompleted() {
268        // We just dismiss the drag when we fling, so cleanup here
269        mLauncher.exitSpringLoadedDragModeDelayed(true,
270                Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
271        mLauncher.unlockScreenOrientation(false);
272    }
273
274    @Override
275    public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete,
276            boolean success) {
277        if (LOGD) {
278            Log.d(TAG, "onDropCompleted");
279        }
280        if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
281                !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
282            // Exit spring loaded mode if we have not successfully dropped or have not handled the
283            // drop in Workspace
284            mLauncher.exitSpringLoadedDragModeDelayed(true,
285                    Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
286        }
287        mLauncher.unlockScreenOrientation(false);
288
289        // Display an error message if the drag failed due to there not being enough space on the
290        // target layout we were dropping on.
291        if (!success) {
292            boolean showOutOfSpaceMessage = false;
293            if (target instanceof Workspace) {
294                int currentScreen = mLauncher.getCurrentWorkspaceScreen();
295                Workspace workspace = (Workspace) target;
296                CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen);
297                ItemInfo itemInfo = (ItemInfo) d.dragInfo;
298                if (layout != null) {
299                    showOutOfSpaceMessage =
300                            !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY);
301                }
302            }
303            if (showOutOfSpaceMessage) {
304                mLauncher.showOutOfSpaceMessage(false);
305            }
306            d.deferDragViewCleanupPostAnimation = false;
307        }
308    }
309
310    //
311    // Container rendering related.
312    //
313    @Override
314    protected void onUpdateBgPadding(Rect padding, Rect bgPadding) {
315        if (Utilities.isRtl(getResources())) {
316            getContentView().setPadding(0, bgPadding.top,
317                    bgPadding.right, bgPadding.bottom);
318            mRecyclerView.updateBackgroundPadding(new Rect(bgPadding.left, 0, 0, 0));
319        } else {
320            getContentView().setPadding(bgPadding.left, bgPadding.top,
321                    0, bgPadding.bottom);
322            mRecyclerView.updateBackgroundPadding(new Rect(0, 0, bgPadding.right, 0));
323        }
324    }
325
326    /**
327     * Initialize the widget data model.
328     */
329    public void addWidgets(WidgetsModel model) {
330        mRecyclerView.setWidgets(model);
331        mAdapter.setWidgetsModel(model);
332        mAdapter.notifyDataSetChanged();
333    }
334
335    public boolean isEmpty() {
336        return mAdapter.getItemCount() == 0;
337    }
338
339    private WidgetPreviewLoader getWidgetPreviewLoader() {
340        if (mWidgetPreviewLoader == null) {
341            mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache();
342        }
343        return mWidgetPreviewLoader;
344    }
345}