| /* |
| * Copyright (C) 2021 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.internal.view.inline; |
| |
| import static android.view.autofill.Helper.sVerbose; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.content.ContextWrapper; |
| import android.graphics.drawable.Drawable; |
| import android.transition.Transition; |
| import android.util.Slog; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.WindowManager; |
| import android.widget.LinearLayout; |
| import android.widget.PopupWindow; |
| import android.widget.inline.InlineContentView; |
| |
| import java.io.PrintWriter; |
| |
| /** |
| * UI container for the inline suggestion tooltip. |
| */ |
| public final class InlineTooltipUi extends PopupWindow implements AutoCloseable { |
| private static final String TAG = "InlineTooltipUi"; |
| |
| private final WindowManager mWm; |
| private final ViewGroup mContentContainer; |
| |
| private boolean mShowing; |
| |
| private WindowManager.LayoutParams mWindowLayoutParams; |
| |
| private final View.OnAttachStateChangeListener mAnchorOnAttachStateChangeListener = |
| new View.OnAttachStateChangeListener() { |
| @Override |
| public void onViewAttachedToWindow(View v) { |
| /* ignore - handled by the super class */ |
| } |
| |
| @Override |
| public void onViewDetachedFromWindow(View v) { |
| dismiss(); |
| } |
| }; |
| |
| private final View.OnLayoutChangeListener mAnchoredOnLayoutChangeListener = |
| new View.OnLayoutChangeListener() { |
| int mHeight; |
| @Override |
| public void onLayoutChange(View v, int left, int top, int right, int bottom, |
| int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| if (mHeight != bottom - top) { |
| mHeight = bottom - top; |
| adjustPosition(); |
| } |
| } |
| }; |
| |
| public InlineTooltipUi(@NonNull Context context) { |
| mContentContainer = new LinearLayout(new ContextWrapper(context)); |
| mWm = context.getSystemService(WindowManager.class); |
| |
| setTouchModal(false); |
| setOutsideTouchable(true); |
| setInputMethodMode(INPUT_METHOD_NOT_NEEDED); |
| setFocusable(false); |
| } |
| |
| /** |
| * Sets the content view for inline suggestions tooltip |
| * @param v the content view of {@link android.widget.inline.InlineContentView} |
| */ |
| public void setTooltipView(@NonNull InlineContentView v) { |
| mContentContainer.removeAllViews(); |
| mContentContainer.addView(v); |
| mContentContainer.setVisibility(View.VISIBLE); |
| } |
| |
| @Override |
| public void close() { |
| hide(); |
| } |
| |
| @Override |
| protected boolean hasContentView() { |
| return true; |
| } |
| |
| @Override |
| protected boolean hasDecorView() { |
| return true; |
| } |
| |
| @Override |
| protected WindowManager.LayoutParams getDecorViewLayoutParams() { |
| return mWindowLayoutParams; |
| } |
| |
| /** |
| * The effective {@code update} method that should be called by its clients. |
| */ |
| public void update(View anchor) { |
| // set to the application type with the highest z-order |
| setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); |
| |
| // The first time to show up, the height of tooltip is zero, |
| // so set the offset Y to 2 * anchor height. |
| final int achoredHeight = mContentContainer.getHeight(); |
| final int offsetY = (achoredHeight == 0) |
| ? -anchor.getHeight() << 1 : -anchor.getHeight() - achoredHeight; |
| if (!isShowing()) { |
| setWidth(WindowManager.LayoutParams.WRAP_CONTENT); |
| setHeight(WindowManager.LayoutParams.WRAP_CONTENT); |
| showAsDropDown(anchor, 0 , offsetY, Gravity.TOP | Gravity.CENTER_HORIZONTAL); |
| } else { |
| update(anchor, 0 , offsetY, WindowManager.LayoutParams.WRAP_CONTENT, |
| WindowManager.LayoutParams.WRAP_CONTENT); |
| } |
| } |
| |
| @Override |
| protected void update(View anchor, WindowManager.LayoutParams params) { |
| // update content view for the anchor is scrolling |
| if (anchor.isVisibleToUser()) { |
| show(params); |
| } else { |
| hide(); |
| } |
| } |
| |
| @Override |
| public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { |
| if (isShowing()) { |
| return; |
| } |
| |
| setShowing(true); |
| setDropDown(true); |
| attachToAnchor(anchor, xoff, yoff, gravity); |
| final WindowManager.LayoutParams p = mWindowLayoutParams = createPopupLayoutParams( |
| anchor.getWindowToken()); |
| final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, |
| p.width, p.height, gravity, getAllowScrollingAnchorParent()); |
| updateAboveAnchor(aboveAnchor); |
| p.accessibilityIdOfAnchor = anchor.getAccessibilityViewId(); |
| p.packageName = anchor.getContext().getPackageName(); |
| show(p); |
| } |
| |
| @Override |
| protected void attachToAnchor(View anchor, int xoff, int yoff, int gravity) { |
| super.attachToAnchor(anchor, xoff, yoff, gravity); |
| anchor.addOnAttachStateChangeListener(mAnchorOnAttachStateChangeListener); |
| } |
| |
| @Override |
| protected void detachFromAnchor() { |
| final View anchor = getAnchor(); |
| if (anchor != null) { |
| anchor.removeOnAttachStateChangeListener(mAnchorOnAttachStateChangeListener); |
| } |
| super.detachFromAnchor(); |
| } |
| |
| @Override |
| public void dismiss() { |
| if (!isShowing() || isTransitioningToDismiss()) { |
| return; |
| } |
| |
| setShowing(false); |
| setTransitioningToDismiss(true); |
| |
| hide(); |
| detachFromAnchor(); |
| if (getOnDismissListener() != null) { |
| getOnDismissListener().onDismiss(); |
| } |
| } |
| |
| private void adjustPosition() { |
| View anchor = getAnchor(); |
| if (anchor == null) return; |
| update(anchor); |
| } |
| |
| private void show(WindowManager.LayoutParams params) { |
| if (sVerbose) { |
| Slog.v(TAG, "show()"); |
| } |
| mWindowLayoutParams = params; |
| |
| try { |
| params.packageName = "android"; |
| params.setTitle("Autofill Inline Tooltip"); // Title is set for debugging purposes |
| if (!mShowing) { |
| params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
| | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; |
| params.privateFlags |= |
| WindowManager.LayoutParams.PRIVATE_FLAG_NOT_MAGNIFIABLE; |
| mContentContainer.addOnLayoutChangeListener(mAnchoredOnLayoutChangeListener); |
| mWm.addView(mContentContainer, params); |
| mShowing = true; |
| } else { |
| mWm.updateViewLayout(mContentContainer, params); |
| } |
| } catch (WindowManager.BadTokenException e) { |
| Slog.d(TAG, "Failed with token " + params.token + " gone."); |
| } catch (IllegalStateException e) { |
| // WM throws an ISE if mContentView was added twice; this should never happen - |
| // since show() and hide() are always called in the UIThread - but when it does, |
| // it should not crash the system. |
| Slog.wtf(TAG, "Exception showing window " + params, e); |
| } |
| } |
| |
| private void hide() { |
| if (sVerbose) { |
| Slog.v(TAG, "hide()"); |
| } |
| try { |
| if (mShowing) { |
| mContentContainer.removeOnLayoutChangeListener(mAnchoredOnLayoutChangeListener); |
| mWm.removeView(mContentContainer); |
| mShowing = false; |
| } |
| } catch (IllegalStateException e) { |
| // WM might thrown an ISE when removing the mContentView; this should never |
| // happen - since show() and hide() are always called in the UIThread - but if it |
| // does, it should not crash the system. |
| Slog.e(TAG, "Exception hiding window ", e); |
| } |
| } |
| |
| @Override |
| public int getAnimationStyle() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public Drawable getBackground() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public View getContentView() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public float getElevation() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public Transition getEnterTransition() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public Transition getExitTransition() { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setBackgroundDrawable(Drawable background) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setContentView(View contentView) { |
| if (contentView != null) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| } |
| |
| @Override |
| public void setElevation(float elevation) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setEnterTransition(Transition enterTransition) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setExitTransition(Transition exitTransition) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| @Override |
| public void setTouchInterceptor(View.OnTouchListener l) { |
| throw new IllegalStateException("You can't call this!"); |
| } |
| |
| /** |
| * Dumps status |
| */ |
| public void dump(@NonNull PrintWriter pw, @Nullable String prefix) { |
| |
| pw.print(prefix); |
| |
| if (mContentContainer != null) { |
| pw.print(prefix); pw.print("Window: "); |
| final String prefix2 = prefix + " "; |
| pw.println(); |
| pw.print(prefix2); pw.print("showing: "); pw.println(mShowing); |
| pw.print(prefix2); pw.print("view: "); pw.println(mContentContainer); |
| if (mWindowLayoutParams != null) { |
| pw.print(prefix2); pw.print("params: "); pw.println(mWindowLayoutParams); |
| } |
| pw.print(prefix2); pw.print("screen coordinates: "); |
| if (mContentContainer == null) { |
| pw.println("N/A"); |
| } else { |
| final int[] coordinates = mContentContainer.getLocationOnScreen(); |
| pw.print(coordinates[0]); pw.print("x"); pw.println(coordinates[1]); |
| } |
| } |
| } |
| } |