1/*
2 * Copyright (C) 2016 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.internal.view;
18
19import android.content.Context;
20import android.graphics.PixelFormat;
21import android.graphics.Rect;
22import android.util.Slog;
23import android.view.Gravity;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.WindowManager;
27import android.view.WindowManagerGlobal;
28import android.widget.TextView;
29
30public class TooltipPopup {
31    private static final String TAG = "TooltipPopup";
32
33    private final Context mContext;
34
35    private final View mContentView;
36    private final TextView mMessageView;
37
38    private final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams();
39    private final Rect mTmpDisplayFrame = new Rect();
40    private final int[] mTmpAnchorPos = new int[2];
41    private final int[] mTmpAppPos = new int[2];
42
43    public TooltipPopup(Context context) {
44        mContext = context;
45
46        mContentView = LayoutInflater.from(mContext).inflate(
47                com.android.internal.R.layout.tooltip, null);
48        mMessageView = (TextView) mContentView.findViewById(
49                com.android.internal.R.id.message);
50
51        mLayoutParams.setTitle(
52                mContext.getString(com.android.internal.R.string.tooltip_popup_title));
53        mLayoutParams.packageName = mContext.getOpPackageName();
54        mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL;
55        mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
56        mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
57        mLayoutParams.format = PixelFormat.TRANSLUCENT;
58        mLayoutParams.windowAnimations = com.android.internal.R.style.Animation_Tooltip;
59        mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
60                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
61    }
62
63    public void show(View anchorView, int anchorX, int anchorY, boolean fromTouch,
64            CharSequence tooltipText) {
65        if (isShowing()) {
66            hide();
67        }
68
69        mMessageView.setText(tooltipText);
70
71        computePosition(anchorView, anchorX, anchorY, fromTouch, mLayoutParams);
72
73        WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
74        wm.addView(mContentView, mLayoutParams);
75    }
76
77    public void hide() {
78        if (!isShowing()) {
79            return;
80        }
81
82        WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
83        wm.removeView(mContentView);
84    }
85
86    public View getContentView() {
87        return mContentView;
88    }
89
90    public boolean isShowing() {
91        return mContentView.getParent() != null;
92    }
93
94    private void computePosition(View anchorView, int anchorX, int anchorY, boolean fromTouch,
95            WindowManager.LayoutParams outParams) {
96        outParams.token = anchorView.getApplicationWindowToken();
97
98        final int tooltipPreciseAnchorThreshold = mContext.getResources().getDimensionPixelOffset(
99                com.android.internal.R.dimen.tooltip_precise_anchor_threshold);
100
101        final int offsetX;
102        if (anchorView.getWidth() >= tooltipPreciseAnchorThreshold) {
103            // Wide view. Align the tooltip horizontally to the precise X position.
104            offsetX = anchorX;
105        } else {
106            // Otherwise anchor the tooltip to the view center.
107            offsetX = anchorView.getWidth() / 2;  // Center on the view horizontally.
108        }
109
110        final int offsetBelow;
111        final int offsetAbove;
112        if (anchorView.getHeight() >= tooltipPreciseAnchorThreshold) {
113            // Tall view. Align the tooltip vertically to the precise Y position.
114            final int offsetExtra = mContext.getResources().getDimensionPixelOffset(
115                    com.android.internal.R.dimen.tooltip_precise_anchor_extra_offset);
116            offsetBelow = anchorY + offsetExtra;
117            offsetAbove = anchorY - offsetExtra;
118        } else {
119            // Otherwise anchor the tooltip to the view center.
120            offsetBelow = anchorView.getHeight();  // Place below the view in most cases.
121            offsetAbove = 0;  // Place above the view if the tooltip does not fit below.
122        }
123
124        outParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
125
126        final int tooltipOffset = mContext.getResources().getDimensionPixelOffset(
127                fromTouch ? com.android.internal.R.dimen.tooltip_y_offset_touch
128                        : com.android.internal.R.dimen.tooltip_y_offset_non_touch);
129
130        // Find the main app window. The popup window will be positioned relative to it.
131        final View appView = WindowManagerGlobal.getInstance().getWindowView(
132                anchorView.getApplicationWindowToken());
133        if (appView == null) {
134            Slog.e(TAG, "Cannot find app view");
135            return;
136        }
137        appView.getWindowVisibleDisplayFrame(mTmpDisplayFrame);
138        appView.getLocationOnScreen(mTmpAppPos);
139
140        anchorView.getLocationOnScreen(mTmpAnchorPos);
141        mTmpAnchorPos[0] -= mTmpAppPos[0];
142        mTmpAnchorPos[1] -= mTmpAppPos[1];
143        // mTmpAnchorPos is now relative to the main app window.
144
145        outParams.x = mTmpAnchorPos[0] + offsetX - appView.getWidth() / 2;
146
147        final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
148        mContentView.measure(spec, spec);
149        final int tooltipHeight = mContentView.getMeasuredHeight();
150
151        final int yAbove = mTmpAnchorPos[1] + offsetAbove - tooltipOffset - tooltipHeight;
152        final int yBelow = mTmpAnchorPos[1] + offsetBelow + tooltipOffset;
153        if (fromTouch) {
154            if (yAbove >= 0) {
155                outParams.y = yAbove;
156            } else {
157                outParams.y = yBelow;
158            }
159        } else {
160            // Use mTmpDisplayFrame.height() as the lower boundary instead of appView.getHeight(),
161            // as the latter includes the navigation bar, and tooltips do not look good over
162            // the navigation bar.
163            if (yBelow + tooltipHeight <= mTmpDisplayFrame.height()) {
164                outParams.y = yBelow;
165            } else {
166                outParams.y = yAbove;
167            }
168        }
169    }
170}
171