| /* |
| * Copyright (C) 2018 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.graphics; |
| |
| import static android.view.View.MeasureSpec.EXACTLY; |
| import static android.view.View.MeasureSpec.makeMeasureSpec; |
| import static android.view.View.VISIBLE; |
| |
| import android.annotation.TargetApi; |
| import android.app.Fragment; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.TypedArray; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Rect; |
| import android.graphics.drawable.AdaptiveIconDrawable; |
| import android.graphics.drawable.ColorDrawable; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Process; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.ContextThemeWrapper; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.TextClock; |
| |
| import com.android.launcher3.BubbleTextView; |
| 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.LauncherSettings.Favorites; |
| import com.android.launcher3.R; |
| import com.android.launcher3.WorkspaceItemInfo; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.WorkspaceLayoutManager; |
| import com.android.launcher3.allapps.SearchUiManager; |
| import com.android.launcher3.config.FeatureFlags; |
| import com.android.launcher3.icons.BaseIconFactory; |
| import com.android.launcher3.icons.BitmapInfo; |
| import com.android.launcher3.icons.BitmapRenderer; |
| import com.android.launcher3.views.ActivityContext; |
| import com.android.launcher3.views.BaseDragLayer; |
| |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.CountDownLatch; |
| |
| /** |
| * 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 |
| */ |
| @TargetApi(Build.VERSION_CODES.O) |
| public class LauncherPreviewRenderer implements Callable<Bitmap> { |
| |
| private static final String TAG = "LauncherPreviewRenderer"; |
| |
| private final Handler mUiHandler; |
| private final Context mContext; |
| private final InvariantDeviceProfile mIdp; |
| private final DeviceProfile mDp; |
| private final Rect mInsets; |
| |
| private final WorkspaceItemInfo mWorkspaceItemInfo; |
| |
| public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp) { |
| mUiHandler = new Handler(Looper.getMainLooper()); |
| mContext = context; |
| mIdp = idp; |
| mDp = idp.portraitProfile.copy(context); |
| |
| // TODO: get correct insets once display cutout API is available. |
| mInsets = new Rect(); |
| mInsets.left = mInsets.right = (mDp.widthPx - mDp.availableWidthPx) / 2; |
| mInsets.top = mInsets.bottom = (mDp.heightPx - mDp.availableHeightPx) / 2; |
| mDp.updateInsets(mInsets); |
| |
| BaseIconFactory iconFactory = |
| new BaseIconFactory(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize) { }; |
| BitmapInfo iconInfo = iconFactory.createBadgedIconBitmap(new AdaptiveIconDrawable( |
| new ColorDrawable(Color.WHITE), new ColorDrawable(Color.WHITE)), |
| Process.myUserHandle(), |
| Build.VERSION.SDK_INT); |
| |
| mWorkspaceItemInfo = new WorkspaceItemInfo(); |
| mWorkspaceItemInfo.applyFrom(iconInfo); |
| mWorkspaceItemInfo.intent = new Intent(); |
| mWorkspaceItemInfo.contentDescription = mWorkspaceItemInfo.title = |
| context.getString(R.string.label_application); |
| } |
| |
| @Override |
| public Bitmap call() { |
| return BitmapRenderer.createHardwareBitmap(mDp.widthPx, mDp.heightPx, c -> { |
| |
| if (Looper.myLooper() == Looper.getMainLooper()) { |
| new MainThreadRenderer(mContext).renderScreenShot(c); |
| } else { |
| CountDownLatch latch = new CountDownLatch(1); |
| Utilities.postAsyncCallback(mUiHandler, () -> { |
| new MainThreadRenderer(mContext).renderScreenShot(c); |
| latch.countDown(); |
| }); |
| |
| try { |
| latch.await(); |
| } catch (Exception e) { |
| Log.e(TAG, "Error drawing on main thread", e); |
| } |
| } |
| }); |
| } |
| |
| private class MainThreadRenderer extends ContextThemeWrapper |
| implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 { |
| |
| private final LayoutInflater mHomeElementInflater; |
| private final InsettableFrameLayout mRootView; |
| |
| private final Hotseat mHotseat; |
| private final CellLayout mWorkspace; |
| |
| MainThreadRenderer(Context context) { |
| super(context, R.style.AppTheme); |
| |
| mHomeElementInflater = LayoutInflater.from( |
| new ContextThemeWrapper(this, R.style.HomeScreenElementTheme)); |
| mHomeElementInflater.setFactory2(this); |
| |
| mRootView = (InsettableFrameLayout) mHomeElementInflater.inflate( |
| R.layout.launcher_preview_layout, null, false); |
| mRootView.setInsets(mInsets); |
| measureView(mRootView, mDp.widthPx, mDp.heightPx); |
| |
| mHotseat = mRootView.findViewById(R.id.hotseat); |
| mHotseat.resetLayout(false); |
| |
| mWorkspace = mRootView.findViewById(R.id.workspace); |
| mWorkspace.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx, |
| mDp.workspacePadding.top, |
| mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx, |
| mDp.workspacePadding.bottom); |
| } |
| |
| @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)); |
| return view; |
| } |
| |
| @Override |
| public View onCreateView(String name, Context context, AttributeSet attrs) { |
| return onCreateView(null, name, context, attrs); |
| } |
| |
| @Override |
| public BaseDragLayer getDragLayer() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public DeviceProfile getDeviceProfile() { |
| return mDp; |
| } |
| |
| @Override |
| public Hotseat getHotseat() { |
| return mHotseat; |
| } |
| |
| @Override |
| public CellLayout getScreenWithId(int screenId) { |
| return mWorkspace; |
| } |
| |
| private void inflateAndAddIcon(WorkspaceItemInfo info) { |
| BubbleTextView icon = (BubbleTextView) mHomeElementInflater.inflate( |
| R.layout.app_icon, mWorkspace, false); |
| icon.applyFromWorkspaceItem(info); |
| addInScreenFromBind(icon, info); |
| } |
| |
| 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) { |
| isVisible = thisVisible && isVisible; |
| ViewGroup vg = (ViewGroup) view; |
| int count = vg.getChildCount(); |
| |
| for (int i = 0; i < count; i++) { |
| dispatchVisibilityAggregated(vg.getChildAt(i), isVisible); |
| } |
| } |
| } |
| |
| private void renderScreenShot(Canvas canvas) { |
| // Add hotseat icons |
| for (int i = 0; i < mIdp.numHotseatIcons; i++) { |
| WorkspaceItemInfo info = new WorkspaceItemInfo(mWorkspaceItemInfo); |
| info.container = Favorites.CONTAINER_HOTSEAT; |
| info.screenId = i; |
| inflateAndAddIcon(info); |
| } |
| |
| // Add workspace icons |
| for (int i = 0; i < mIdp.numColumns; i++) { |
| WorkspaceItemInfo info = new WorkspaceItemInfo(mWorkspaceItemInfo); |
| info.container = Favorites.CONTAINER_DESKTOP; |
| info.screenId = 0; |
| info.cellX = i; |
| info.cellY = mIdp.numRows - 1; |
| inflateAndAddIcon(info); |
| } |
| |
| // Add first page QSB |
| if (FeatureFlags.QSB_ON_FIRST_SCREEN) { |
| View qsb = mHomeElementInflater.inflate( |
| R.layout.search_container_workspace, mWorkspace, false); |
| CellLayout.LayoutParams lp = |
| new CellLayout.LayoutParams(0, 0, mWorkspace.getCountX(), 1); |
| lp.canReorder = false; |
| mWorkspace.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true); |
| } |
| |
| // Setup search view |
| SearchUiManager searchUiManager = |
| mRootView.findViewById(R.id.search_container_all_apps); |
| mRootView.findViewById(R.id.apps_view).setTranslationY( |
| mDp.heightPx - searchUiManager.getScrollRangeDelta(mInsets)); |
| |
| measureView(mRootView, mDp.widthPx, mDp.heightPx); |
| dispatchVisibilityAggregated(mRootView, true); |
| measureView(mRootView, mDp.widthPx, mDp.heightPx); |
| // Additional measure for views which use auto text size API |
| measureView(mRootView, mDp.widthPx, mDp.heightPx); |
| |
| mRootView.draw(canvas); |
| dispatchVisibilityAggregated(mRootView, false); |
| } |
| } |
| |
| private static void measureView(View view, int width, int height) { |
| view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY)); |
| view.layout(0, 0, width, height); |
| } |
| } |