Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c174690e authored by Svetoslav Ganov's avatar Svetoslav Ganov
Browse files

Make the permission request dialog's layout robust

The old implementation was relying on a fixed window size
where the content is positioned by a custom layout manager.
It is possible however that subsequent permissions requests
do not fit in the window as its size is computed based on
the content of the first permissions request. There were
also cases where the content is chopped after a rotation
as the dialog size width was not re-evaluated while it should
be. Further, animation from one permission request state
to another was not properly done resulting in content
being chopped off in some cases.

The current approach is to have a dialog width for the
content activity but the height is as tall as the screen
allowing us to fit arbitrary large permission request
content. Also we are resetting the fixed width on a
configuration change so the dialog is robust to adjust
size as needed.

bug:24679384
bug:25755378

Change-Id: I4d23f81d8e59ce23bf9a27155ebb5ec6e2e6752c
parent f8f62986
Loading
Loading
Loading
Loading
+72 −55
Original line number Diff line number Diff line
@@ -17,25 +17,36 @@
<com.android.packageinstaller.permission.ui.ManualLayoutFrame
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >
    android:layout_height="fill_parent"
    android:clipChildren="false">

    <LinearLayout
        android:id="@+id/dialog_container"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:paddingTop="24dip"
        android:paddingBottom="8dip"
        android:paddingStart="22dip"
        android:paddingEnd="16dip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <FrameLayout
            android:id="@+id/desc_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >
            android:layout_height="wrap_content"
            android:paddingTop="24dip"
            android:paddingStart="22dip"
            android:paddingEnd="16dip"
            android:background="?android:attr/colorBackgroundFloating">
            <include
                layout="@layout/permission_description" />
        </FrameLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingBottom="8dip"
            android:paddingStart="22dip"
            android:paddingEnd="16dip"
            android:background="?android:attr/colorBackgroundFloating">

            <CheckBox
                android:id="@+id/do_not_ask_checkbox"
                android:layout_width="fill_parent"
@@ -46,7 +57,7 @@
                android:visibility="gone">
            </CheckBox>

        <com.android.internal.widget.ButtonBarLayout
            <com.android.packageinstaller.permission.ui.ButtonBarLayout
                android:id="@+id/button_group"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
@@ -62,8 +73,8 @@
                    android:paddingBottom="4dp"
                    android:paddingEnd="12dp"
                    android:singleLine="true"
                style="@android:style/TextAppearance.Material.Body2"
                android:textColor="@color/grant_permissions_progress_color"
                    style="?android:attr/textAppearanceSmall"
                    android:textColor="?android:attr/textColorSecondary"
                    android:visibility="invisible">
                </TextView>

@@ -72,14 +83,16 @@
                    android:layout_width="0dp"
                    android:layout_height="0dp"
                    android:layout_weight="1"
                android:visibility="invisible" />
                    android:visibility="invisible" >
                </Space>

                <Button
                    android:id="@+id/permission_deny_button"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    style="?android:attr/buttonBarButtonStyle"
                android:text="@string/grant_dialog_button_deny" />
                    android:text="@string/grant_dialog_button_deny" >
                </Button>

                <Button
                    android:id="@+id/permission_allow_button"
@@ -87,9 +100,13 @@
                    android:layout_height="wrap_content"
                    style="?android:attr/buttonBarButtonStyle"
                    android:layout_marginStart="8dip"
                android:text="@string/grant_dialog_button_allow" />
                    android:text="@string/grant_dialog_button_allow" >
                </Button>

        </com.android.internal.widget.ButtonBarLayout>
            </com.android.packageinstaller.permission.ui.ButtonBarLayout>

        </LinearLayout>

    </LinearLayout>

</com.android.packageinstaller.permission.ui.ManualLayoutFrame>
+6 −1
Original line number Diff line number Diff line
@@ -22,5 +22,10 @@
    </style>

    <style name="GrantPermissions"
            parent="@android:style/Theme.DeviceDefault.Light.Dialog.NoActionBar" />
           parent="@android:style/Theme.DeviceDefault.Light.Dialog.NoActionBar">
        <item name="*android:windowFixedHeightMajor">100%</item>
        <item name="*android:windowFixedHeightMinor">100%</item>
        <item name="android:windowBackground">@android:color/transparent</item>
    </style>

</resources>
+16 −1
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.hardware.camera2.utils.ArrayUtils;
@@ -44,7 +45,6 @@ import com.android.packageinstaller.permission.model.AppPermissionGroup;
import com.android.packageinstaller.permission.model.AppPermissions;
import com.android.packageinstaller.permission.model.Permission;
import com.android.packageinstaller.permission.utils.SafetyNetLogger;
import com.android.packageinstaller.permission.utils.Utils;

import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -172,6 +172,21 @@ public class GrantPermissionsActivity extends OverlayTouchActivity
        }
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        // We need to relayout the window as dialog width may be
        // different in landscape vs portrait which affect the min
        // window height needed to show all content. We have to
        // re-add the window to force it to be resized if needed.
        View decor = getWindow().getDecorView();
        getWindowManager().removeViewImmediate(decor);
        getWindowManager().addView(decor, decor.getLayoutParams());
        if (mViewHandler instanceof GrantPermissionsDefaultViewHandler) {
            ((GrantPermissionsDefaultViewHandler) mViewHandler).onConfigurationChanged();
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        View rootView = getWindow().getDecorView();
+106 −214
Original line number Diff line number Diff line
@@ -16,23 +16,14 @@

package com.android.packageinstaller.permission.ui;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewRootImpl;
import android.view.WindowManager.LayoutParams;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.Button;
@@ -43,9 +34,7 @@ import android.widget.TextView;
import com.android.internal.widget.ButtonBarLayout;
import com.android.packageinstaller.R;

import java.util.ArrayList;

final class GrantPermissionsDefaultViewHandler
public final class GrantPermissionsDefaultViewHandler
        implements GrantPermissionsViewHandler, OnClickListener {

    public static final String ARG_GROUP_NAME = "ARG_GROUP_NAME";
@@ -57,14 +46,8 @@ final class GrantPermissionsDefaultViewHandler
    public static final String ARG_GROUP_DO_NOT_ASK_CHECKED = "ARG_GROUP_DO_NOT_ASK_CHECKED";

    // Animation parameters.
    private static final long SIZE_START_DELAY = 300;
    private static final long SIZE_START_LENGTH = 233;
    private static final long FADE_OUT_START_DELAY = 300;
    private static final long FADE_OUT_START_LENGTH = 217;
    private static final long TRANSLATE_START_DELAY = 367;
    private static final long TRANSLATE_LENGTH = 317;
    private static final long GROUP_UPDATE_DELAY = 400;
    private static final long DO_NOT_ASK_CHECK_DELAY = 450;
    private static final long OUT_DURATION = 200;
    private static final long IN_DURATION = 300;

    private final Context mContext;

@@ -84,22 +67,13 @@ final class GrantPermissionsDefaultViewHandler
    private CheckBox mDoNotAskCheckbox;
    private Button mAllowButton;

    private ArrayList<ViewHeightController> mHeightControllers;
    private ManualLayoutFrame mRootView;

    // Needed for animation
    private ViewGroup mDescContainer;
    private ViewGroup mCurrentDesc;
    private ViewGroup mNextDesc;

    private ViewGroup mDialogContainer;

    private final Runnable mUpdateGroup = new Runnable() {
        @Override
        public void run() {
            updateGroup();
        }
    };
    private ButtonBarLayout mButtonBar;

    GrantPermissionsDefaultViewHandler(Context context) {
        mContext = context;
@@ -158,173 +132,148 @@ final class GrantPermissionsDefaultViewHandler
        }
    }

    private void animateToPermission() {
        if (mHeightControllers == null) {
            // We need to manually control the height of any views heigher than the root that
            // we inflate.  Find all the views up to the root and create ViewHeightControllers for
            // them.
            mHeightControllers = new ArrayList<>();
            ViewRootImpl viewRoot = mRootView.getViewRootImpl();
            ViewParent v = mRootView.getParent();
            addHeightController(mDialogContainer);
            addHeightController(mRootView);
            while (v != viewRoot) {
                addHeightController((View) v);
                v = v.getParent();
            }
            // On the heighest level view, we want to setTop rather than setBottom to control the
            // height, this way the dialog will grow up rather than down.
            ViewHeightController realRootView =
                    mHeightControllers.get(mHeightControllers.size() - 1);
            realRootView.setControlTop(true);
        }

        // Grab the current height/y positions, then wait for the layout to change,
        // so we can get the end height/y positions.
        final SparseArray<Float> startPositions = getViewPositions();
        final int startHeight = mRootView.getLayoutHeight();
        mRootView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
                    int oldTop, int oldRight, int oldBottom) {
                mRootView.removeOnLayoutChangeListener(this);
                SparseArray<Float> endPositions = getViewPositions();
                int endHeight = mRootView.getLayoutHeight();
                if (startPositions.get(R.id.do_not_ask_checkbox) == 0
                        && endPositions.get(R.id.do_not_ask_checkbox) != 0) {
                    // If the checkbox didn't have a position before but has one now then set
                    // the start position to the end position because it just became visible.
                    startPositions.put(R.id.do_not_ask_checkbox,
                            endPositions.get(R.id.do_not_ask_checkbox));
                }
                animateYPos(startPositions, endPositions, endHeight - startHeight);
    public void onConfigurationChanged() {
        mRootView.onConfigurationChanged();
    }
        });

    private void animateOldContent(Runnable callback) {
        // Fade out old description group and scale out the icon for it.
        Interpolator interpolator = AnimationUtils.loadInterpolator(mContext,
                android.R.interpolator.fast_out_linear_in);

        // Icon scale to zero
        mIconView.animate()
                .scaleX(0)
                .scaleY(0)
                .setStartDelay(FADE_OUT_START_DELAY)
                .setDuration(FADE_OUT_START_LENGTH)
                .setDuration(OUT_DURATION)
                .setInterpolator(interpolator)
                .start();

        // Description fade out
        mCurrentDesc.animate()
                .alpha(0)
                .setStartDelay(FADE_OUT_START_DELAY)
                .setDuration(FADE_OUT_START_LENGTH)
                .setDuration(OUT_DURATION)
                .setInterpolator(interpolator)
                .setListener(null)
                .withEndAction(callback)
                .start();

        // Update the index of the permission after the animations have started.
        mCurrentGroupView.getHandler().postDelayed(mUpdateGroup, GROUP_UPDATE_DELAY);
        // Checkbox fade out if needed
        if (!mShowDonNotAsk && mDoNotAskCheckbox.getVisibility() == View.VISIBLE) {
            mDoNotAskCheckbox.animate()
                    .alpha(0)
                    .setDuration(OUT_DURATION)
                    .setInterpolator(interpolator)
                    .start();
        }
    }

        // Add the new description and translate it in.
        mNextDesc = (ViewGroup) LayoutInflater.from(mContext).inflate(
    private void attachNewContent(final Runnable callback) {
        mCurrentDesc = (ViewGroup) LayoutInflater.from(mContext).inflate(
                R.layout.permission_description, mDescContainer, false);
        mDescContainer.removeAllViews();
        mDescContainer.addView(mCurrentDesc);

        mDialogContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View v, int left, int top, int right, int bottom,
                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
                    mDialogContainer.removeOnLayoutChangeListener(this);

                    // Prepare new content to the right to be moved in
                    final int containerWidth = mDescContainer.getWidth();
                    mCurrentDesc.setTranslationX(containerWidth);

                    // How much scale for the dialog to appear the same?
                    final int oldDynamicHeight = oldBottom - oldTop - mButtonBar.getHeight();
                    final float scaleY = (float) oldDynamicHeight / mDescContainer.getHeight();

                    // How much to translate for the dialog to appear the same?
                    final int translationCompensatingScale = (int) (scaleY
                            * mDescContainer.getHeight() - mDescContainer.getHeight()) / 2;
                    final int translationY = (oldTop - top) + translationCompensatingScale;

                    // Animate to the current layout
                    mDescContainer.setScaleY(scaleY);
                    mDescContainer.setTranslationY(translationY);
                    mDescContainer.animate()
                            .translationY(0)
                            .scaleY(1.0f)
                            .setInterpolator(AnimationUtils.loadInterpolator(mContext,
                                    android.R.interpolator.linear_out_slow_in))
                            .setDuration(IN_DURATION)
                            .withEndAction(callback)
                            .start();
                }
            }
        );

        mMessageView = (TextView) mCurrentDesc.findViewById(R.id.permission_message);
        mIconView = (ImageView) mCurrentDesc.findViewById(R.id.permission_icon);

        final boolean doNotAskWasShown = mDoNotAskCheckbox.getVisibility() == View.VISIBLE;

        mMessageView = (TextView) mNextDesc.findViewById(R.id.permission_message);
        mIconView = (ImageView) mNextDesc.findViewById(R.id.permission_icon);
        updateDescription();
        updateGroup();
        updateDoNotAskCheckBox();

        int width = mDescContainer.getRootView().getWidth();
        mDescContainer.addView(mNextDesc);
        mNextDesc.setTranslationX(width);
        if (!doNotAskWasShown && mShowDonNotAsk) {
            mDoNotAskCheckbox.setAlpha(0);
        }
    }

        final View oldDesc = mCurrentDesc;
        // Remove the old view from the description, so that we can shrink if necessary.
        mDescContainer.removeView(oldDesc);
        oldDesc.setPadding(mDescContainer.getLeft(), mDescContainer.getTop(),
                mRootView.getRight() - mDescContainer.getRight(), 0);
        mRootView.addView(oldDesc);
    private void animateNewContent() {
        Interpolator interpolator = AnimationUtils.loadInterpolator(mContext,
                android.R.interpolator.linear_out_slow_in);

        mCurrentDesc = mNextDesc;
        mNextDesc.animate()
        // Description slide in
        mCurrentDesc.animate()
                .translationX(0)
                .setStartDelay(TRANSLATE_START_DELAY)
                .setDuration(TRANSLATE_LENGTH)
                .setInterpolator(AnimationUtils.loadInterpolator(mContext,
                        android.R.interpolator.linear_out_slow_in))
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        // This is the longest animation, when it finishes, we are done.
                        mRootView.removeView(oldDesc);
                    }
                })
                .setDuration(IN_DURATION)
                .setInterpolator(interpolator)
                .start();

        boolean visibleBefore = mDoNotAskCheckbox.getVisibility() == View.VISIBLE;
        updateDoNotAskCheckBox();
        boolean visibleAfter = mDoNotAskCheckbox.getVisibility() == View.VISIBLE;
        if (visibleBefore != visibleAfter) {
            Animation anim = AnimationUtils.loadAnimation(mContext,
                    visibleAfter ? android.R.anim.fade_in : android.R.anim.fade_out);
            anim.setStartOffset(visibleAfter ? DO_NOT_ASK_CHECK_DELAY : 0);
            mDoNotAskCheckbox.startAnimation(anim);
        }
    }

    private void addHeightController(View v) {
        ViewHeightController heightController = new ViewHeightController(v);
        heightController.setHeight(v.getHeight());
        mHeightControllers.add(heightController);
    }

    private SparseArray<Float> getViewPositions() {
        SparseArray<Float> locMap = new SparseArray<>();
        final int N = mDialogContainer.getChildCount();
        for (int i = 0; i < N; i++) {
            View child = mDialogContainer.getChildAt(i);
            if (child.getId() <= 0) {
                // Only track views with ids.
                continue;
            }
            locMap.put(child.getId(), child.getY());
        }
        return locMap;
    }

    private void animateYPos(SparseArray<Float> startPositions, SparseArray<Float> endPositions,
            int heightDiff) {
        final int N = startPositions.size();
        for (int i = 0; i < N; i++) {
            int key = startPositions.keyAt(i);
            float start = startPositions.get(key);
            float end = endPositions.get(key);
            if (start != end) {
                final View child = mDialogContainer.findViewById(key);
                child.setTranslationY(start - end);
                child.animate()
                        .setStartDelay(SIZE_START_DELAY)
                        .setDuration(SIZE_START_LENGTH)
                        .translationY(0)
        // Checkbox fade in if needed
        if (mShowDonNotAsk && mDoNotAskCheckbox.getVisibility() == View.VISIBLE
                && mDoNotAskCheckbox.getAlpha() < 1.0f) {
            mDoNotAskCheckbox.setAlpha(0);
            mDoNotAskCheckbox.animate()
                    .alpha(1.0f)
                    .setDuration(IN_DURATION)
                    .setInterpolator(interpolator)
                    .start();
        }
    }
        for (int i = 0; i < mHeightControllers.size(); i++) {
            mHeightControllers.get(i).animateAddHeight(heightDiff);

    private void animateToPermission() {
        // Remove the old content
        animateOldContent(new Runnable() {
            @Override
            public void run() {
                // Add the new content
                attachNewContent(new Runnable() {
                    @Override
                    public void run() {
                        // Animate the new content
                        animateNewContent();
                    }
                });
            }
        });
    }

    @Override
    public View createView() {
        mRootView = (ManualLayoutFrame) LayoutInflater.from(mContext)
                .inflate(R.layout.grant_permissions, null);
        ((ButtonBarLayout) mRootView.findViewById(R.id.button_group)).setAllowStacking(
                Resources.getSystem().getBoolean(
                        com.android.internal.R.bool.allow_stacked_button_bar));

        mDialogContainer = (ViewGroup) mRootView.findViewById(R.id.dialog_container);
        mButtonBar = (ButtonBarLayout) mRootView.findViewById(R.id.button_group);
        mButtonBar.setAllowStacking(true);
        mMessageView = (TextView) mRootView.findViewById(R.id.permission_message);
        mIconView = (ImageView) mRootView.findViewById(R.id.permission_icon);
        mCurrentGroupView = (TextView) mRootView.findViewById(R.id.current_page_text);
        mDoNotAskCheckbox = (CheckBox) mRootView.findViewById(R.id.do_not_ask_checkbox);
        mAllowButton = (Button) mRootView.findViewById(R.id.permission_allow_button);

        mDialogContainer = (ViewGroup) mRootView.findViewById(R.id.dialog_container);
        mDescContainer = (ViewGroup) mRootView.findViewById(R.id.desc_container);
        mCurrentDesc = (ViewGroup) mRootView.findViewById(R.id.perm_desc_root);

@@ -402,61 +351,4 @@ final class GrantPermissionsDefaultViewHandler
            mResultListener.onPermissionGrantResult(mGroupName, false, doNotAskAgain);
        }
    }

    /**
     * Manually controls the height of a view through getBottom/setTop.  Also listens
     * for layout changes and sets the height again to be sure it doesn't change.
     */
    private static final class ViewHeightController implements OnLayoutChangeListener {
        private final View mView;
        private int mHeight;
        private int mNextHeight;
        private boolean mControlTop;
        private ObjectAnimator mAnimator;

        public ViewHeightController(View view) {
            mView = view;
            mView.addOnLayoutChangeListener(this);
        }

        public void setControlTop(boolean controlTop) {
            mControlTop = controlTop;
        }

        public void animateAddHeight(int heightDiff) {
            if (heightDiff != 0) {
                if (mNextHeight == 0) {
                    mNextHeight = mHeight;
                }
                mNextHeight += heightDiff;
                if (mAnimator != null) {
                    mAnimator.cancel();
                }
                mAnimator = ObjectAnimator.ofInt(this, "height", mHeight, mNextHeight);
                mAnimator.setStartDelay(SIZE_START_DELAY);
                mAnimator.setDuration(SIZE_START_LENGTH);
                mAnimator.start();
            }
        }

        public void setHeight(int height) {
            mHeight = height;
            updateHeight();
        }

        @Override
        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
                int oldTop, int oldRight, int oldBottom) {
            // Ensure that the height never changes.
            updateHeight();
        }

        private void updateHeight() {
            if (mControlTop) {
                mView.setTop(mView.getBottom() - mHeight);
            } else {
                mView.setBottom(mView.getTop() + mHeight);
            }
        }
    }
}
+29 −31

File changed.

Preview size limit exceeded, changes collapsed.