1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.ui;
6
7import android.content.Context;
8import android.graphics.Bitmap;
9import android.graphics.Canvas;
10import android.graphics.Rect;
11import android.util.Log;
12import android.view.SurfaceView;
13import android.view.View;
14import android.view.ViewGroup;
15import android.view.inputmethod.InputMethodManager;
16
17/**
18 * Utility functions for common Android UI tasks.
19 * This class is not supposed to be instantiated.
20 */
21public class UiUtils {
22    private static final String TAG = "UiUtils";
23
24    /**
25     * Guards this class from being instantiated.
26     */
27    private UiUtils() {
28    }
29
30    /** The minimum size of the bottom margin below the app to detect a keyboard. */
31    private static final float KEYBOARD_DETECT_BOTTOM_THRESHOLD_DP = 100;
32
33    /** A delegate that allows disabling keyboard visibility detection. */
34    private static KeyboardShowingDelegate sKeyboardShowingDelegate;
35
36    /**
37     * A delegate that can be implemented to override whether or not keyboard detection will be
38     * used.
39     */
40    public interface KeyboardShowingDelegate {
41        /**
42         * Will be called to determine whether or not to detect if the keyboard is visible.
43         * @param context A {@link Context} instance.
44         * @param view    A {@link View}.
45         * @return        Whether or not the keyboard check should be disabled.
46         */
47        boolean disableKeyboardCheck(Context context, View view);
48    }
49
50    /**
51     * Allows setting a delegate to override the default software keyboard visibility detection.
52     * @param delegate A {@link KeyboardShowingDelegate} instance.
53     */
54    public static void setKeyboardShowingDelegate(KeyboardShowingDelegate delegate) {
55        sKeyboardShowingDelegate = delegate;
56    }
57
58    /**
59     * Shows the software keyboard if necessary.
60     * @param view The currently focused {@link View}, which would receive soft keyboard input.
61     */
62    public static void showKeyboard(View view) {
63        InputMethodManager imm =
64                (InputMethodManager) view.getContext().getSystemService(
65                        Context.INPUT_METHOD_SERVICE);
66        // Only shows soft keyboard if there isn't an open physical keyboard.
67        imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
68    }
69
70    /**
71     * Hides the keyboard.
72     * @param view The {@link View} that is currently accepting input.
73     * @return Whether the keyboard was visible before.
74     */
75    public static boolean hideKeyboard(View view) {
76        InputMethodManager imm =
77                (InputMethodManager) view.getContext().getSystemService(
78                        Context.INPUT_METHOD_SERVICE);
79        return imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
80    }
81
82    /**
83     * Detects whether or not the keyboard is showing.  This is a best guess as there is no
84     * standardized/foolproof way to do this.
85     * @param context A {@link Context} instance.
86     * @param view    A {@link View}.
87     * @return        Whether or not the software keyboard is visible and taking up screen space.
88     */
89    public static boolean isKeyboardShowing(Context context, View view) {
90        if (sKeyboardShowingDelegate != null
91                && sKeyboardShowingDelegate.disableKeyboardCheck(context, view)) {
92            return false;
93        }
94
95        View rootView = view.getRootView();
96        if (rootView == null) return false;
97        Rect appRect = new Rect();
98        rootView.getWindowVisibleDisplayFrame(appRect);
99
100        final float density = context.getResources().getDisplayMetrics().density;
101        final float bottomMarginDp = Math.abs(rootView.getHeight() - appRect.height()) / density;
102        return bottomMarginDp > KEYBOARD_DETECT_BOTTOM_THRESHOLD_DP;
103    }
104
105    /**
106     * Inserts a {@link View} into a {@link ViewGroup} after directly before a given {@View}.
107     * @param container The {@link View} to add newView to.
108     * @param newView The new {@link View} to add.
109     * @param existingView The {@link View} to insert the newView before.
110     * @return The index where newView was inserted, or -1 if it was not inserted.
111     */
112    public static int insertBefore(ViewGroup container, View newView, View existingView) {
113        return insertView(container, newView, existingView, false);
114    }
115
116    /**
117     * Inserts a {@link View} into a {@link ViewGroup} after directly after a given {@View}.
118     * @param container The {@link View} to add newView to.
119     * @param newView The new {@link View} to add.
120     * @param existingView The {@link View} to insert the newView after.
121     * @return The index where newView was inserted, or -1 if it was not inserted.
122     */
123    public static int insertAfter(ViewGroup container, View newView, View existingView) {
124        return insertView(container, newView, existingView, true);
125    }
126
127    private static int insertView(
128            ViewGroup container, View newView, View existingView, boolean after) {
129        // See if the view has already been added.
130        int index = container.indexOfChild(newView);
131        if (index >= 0) return index;
132
133        // Find the location of the existing view.
134        index = container.indexOfChild(existingView);
135        if (index < 0) return -1;
136
137        // Add the view.
138        if (after) index++;
139        container.addView(newView, index);
140        return index;
141    }
142
143    /**
144     * Generates a scaled screenshot of the given view.  The maximum size of the screenshot is
145     * determined by maximumDimension.
146     *
147     * @param currentView      The view to generate a screenshot of.
148     * @param maximumDimension The maximum width or height of the generated screenshot.  The bitmap
149     *                         will be scaled to ensure the maximum width or height is equal to or
150     *                         less than this.  Any value <= 0, will result in no scaling.
151     * @param bitmapConfig     Bitmap config for the generated screenshot (ARGB_8888 or RGB_565).
152     * @return The screen bitmap of the view or null if a problem was encountered.
153     */
154    public static Bitmap generateScaledScreenshot(
155            View currentView, int maximumDimension, Bitmap.Config bitmapConfig) {
156        Bitmap screenshot = null;
157        boolean drawingCacheEnabled = currentView.isDrawingCacheEnabled();
158        try {
159            prepareViewHierarchyForScreenshot(currentView, true);
160            if (!drawingCacheEnabled) currentView.setDrawingCacheEnabled(true);
161            // Android has a maximum drawing cache size and if the drawing cache is bigger
162            // than that, getDrawingCache() returns null.
163            Bitmap originalBitmap = currentView.getDrawingCache();
164            if (originalBitmap != null) {
165                double originalHeight = originalBitmap.getHeight();
166                double originalWidth = originalBitmap.getWidth();
167                int newWidth = (int) originalWidth;
168                int newHeight = (int) originalHeight;
169                if (maximumDimension > 0) {
170                    double scale = maximumDimension / Math.max(originalWidth, originalHeight);
171                    newWidth = (int) Math.round(originalWidth * scale);
172                    newHeight = (int) Math.round(originalHeight * scale);
173                }
174                Bitmap scaledScreenshot =
175                        Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true);
176                if (scaledScreenshot.getConfig() != bitmapConfig) {
177                    screenshot = scaledScreenshot.copy(bitmapConfig, false);
178                    scaledScreenshot.recycle();
179                    scaledScreenshot = null;
180                } else {
181                    screenshot = scaledScreenshot;
182                }
183            } else if (currentView.getMeasuredHeight() > 0 && currentView.getMeasuredWidth() > 0) {
184                double originalHeight = currentView.getMeasuredHeight();
185                double originalWidth = currentView.getMeasuredWidth();
186                int newWidth = (int) originalWidth;
187                int newHeight = (int) originalHeight;
188                if (maximumDimension > 0) {
189                    double scale = maximumDimension / Math.max(originalWidth, originalHeight);
190                    newWidth = (int) Math.round(originalWidth * scale);
191                    newHeight = (int) Math.round(originalHeight * scale);
192                }
193                Bitmap bitmap = Bitmap.createBitmap(newWidth, newHeight, bitmapConfig);
194                Canvas canvas = new Canvas(bitmap);
195                canvas.scale((float) (newWidth / originalWidth),
196                        (float) (newHeight / originalHeight));
197                currentView.draw(canvas);
198                screenshot = bitmap;
199            }
200        } catch (OutOfMemoryError e) {
201            Log.d(TAG, "Unable to capture screenshot and scale it down." + e.getMessage());
202        } finally {
203            if (!drawingCacheEnabled) currentView.setDrawingCacheEnabled(false);
204            prepareViewHierarchyForScreenshot(currentView, false);
205        }
206        return screenshot;
207    }
208
209    private static void prepareViewHierarchyForScreenshot(View view, boolean takingScreenshot) {
210        if (view instanceof ViewGroup) {
211            ViewGroup viewGroup = (ViewGroup) view;
212            for (int i = 0; i < viewGroup.getChildCount(); i++) {
213                prepareViewHierarchyForScreenshot(viewGroup.getChildAt(i), takingScreenshot);
214            }
215        } else if (view instanceof SurfaceView) {
216            view.setWillNotDraw(!takingScreenshot);
217        }
218    }
219}
220