Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2015 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 
     17 package com.android.packageinstaller.permission.ui;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.graphics.drawable.Icon;
     25 import android.os.Bundle;
     26 import android.util.SparseArray;
     27 import android.view.LayoutInflater;
     28 import android.view.View;
     29 import android.view.View.OnClickListener;
     30 import android.view.View.OnLayoutChangeListener;
     31 import android.view.ViewGroup;
     32 import android.view.ViewParent;
     33 import android.view.ViewRootImpl;
     34 import android.view.WindowManager.LayoutParams;
     35 import android.view.animation.Animation;
     36 import android.view.animation.AnimationUtils;
     37 import android.view.animation.Interpolator;
     38 import android.widget.Button;
     39 import android.widget.CheckBox;
     40 import android.widget.ImageView;
     41 import android.widget.TextView;
     42 
     43 import com.android.internal.widget.ButtonBarLayout;
     44 import com.android.packageinstaller.R;
     45 
     46 import java.util.ArrayList;
     47 
     48 final class GrantPermissionsDefaultViewHandler
     49         implements GrantPermissionsViewHandler, OnClickListener {
     50 
     51     public static final String ARG_GROUP_NAME = "ARG_GROUP_NAME";
     52     public static final String ARG_GROUP_COUNT = "ARG_GROUP_COUNT";
     53     public static final String ARG_GROUP_INDEX = "ARG_GROUP_INDEX";
     54     public static final String ARG_GROUP_ICON = "ARG_GROUP_ICON";
     55     public static final String ARG_GROUP_MESSAGE = "ARG_GROUP_MESSAGE";
     56     public static final String ARG_GROUP_SHOW_DO_NOT_ASK = "ARG_GROUP_SHOW_DO_NOT_ASK";
     57     public static final String ARG_GROUP_DO_NOT_ASK_CHECKED = "ARG_GROUP_DO_NOT_ASK_CHECKED";
     58 
     59     // Animation parameters.
     60     private static final long SIZE_START_DELAY = 300;
     61     private static final long SIZE_START_LENGTH = 233;
     62     private static final long FADE_OUT_START_DELAY = 300;
     63     private static final long FADE_OUT_START_LENGTH = 217;
     64     private static final long TRANSLATE_START_DELAY = 367;
     65     private static final long TRANSLATE_LENGTH = 317;
     66     private static final long GROUP_UPDATE_DELAY = 400;
     67     private static final long DO_NOT_ASK_CHECK_DELAY = 450;
     68 
     69     private final Context mContext;
     70 
     71     private ResultListener mResultListener;
     72 
     73     private String mGroupName;
     74     private int mGroupCount;
     75     private int mGroupIndex;
     76     private Icon mGroupIcon;
     77     private CharSequence mGroupMessage;
     78     private boolean mShowDonNotAsk;
     79     private boolean mDoNotAskChecked;
     80 
     81     private ImageView mIconView;
     82     private TextView mCurrentGroupView;
     83     private TextView mMessageView;
     84     private CheckBox mDoNotAskCheckbox;
     85     private Button mAllowButton;
     86 
     87     private ArrayList<ViewHeightController> mHeightControllers;
     88     private ManualLayoutFrame mRootView;
     89 
     90     // Needed for animation
     91     private ViewGroup mDescContainer;
     92     private ViewGroup mCurrentDesc;
     93     private ViewGroup mNextDesc;
     94 
     95     private ViewGroup mDialogContainer;
     96 
     97     private final Runnable mUpdateGroup = new Runnable() {
     98         @Override
     99         public void run() {
    100             updateGroup();
    101         }
    102     };
    103 
    104     GrantPermissionsDefaultViewHandler(Context context) {
    105         mContext = context;
    106     }
    107 
    108     @Override
    109     public GrantPermissionsDefaultViewHandler setResultListener(ResultListener listener) {
    110         mResultListener = listener;
    111         return this;
    112     }
    113 
    114     @Override
    115     public void saveInstanceState(Bundle arguments) {
    116         arguments.putString(ARG_GROUP_NAME, mGroupName);
    117         arguments.putInt(ARG_GROUP_COUNT, mGroupCount);
    118         arguments.putInt(ARG_GROUP_INDEX, mGroupIndex);
    119         arguments.putParcelable(ARG_GROUP_ICON, mGroupIcon);
    120         arguments.putCharSequence(ARG_GROUP_MESSAGE, mGroupMessage);
    121         arguments.putBoolean(ARG_GROUP_SHOW_DO_NOT_ASK, mShowDonNotAsk);
    122         arguments.putBoolean(ARG_GROUP_DO_NOT_ASK_CHECKED, mDoNotAskCheckbox.isChecked());
    123     }
    124 
    125     @Override
    126     public void loadInstanceState(Bundle savedInstanceState) {
    127         mGroupName = savedInstanceState.getString(ARG_GROUP_NAME);
    128         mGroupMessage = savedInstanceState.getCharSequence(ARG_GROUP_MESSAGE);
    129         mGroupIcon = savedInstanceState.getParcelable(ARG_GROUP_ICON);
    130         mGroupCount = savedInstanceState.getInt(ARG_GROUP_COUNT);
    131         mGroupIndex = savedInstanceState.getInt(ARG_GROUP_INDEX);
    132         mShowDonNotAsk = savedInstanceState.getBoolean(ARG_GROUP_SHOW_DO_NOT_ASK);
    133         mDoNotAskChecked = savedInstanceState.getBoolean(ARG_GROUP_DO_NOT_ASK_CHECKED);
    134     }
    135 
    136     @Override
    137     public void updateUi(String groupName, int groupCount, int groupIndex, Icon icon,
    138             CharSequence message, boolean showDonNotAsk) {
    139         mGroupName = groupName;
    140         mGroupCount = groupCount;
    141         mGroupIndex = groupIndex;
    142         mGroupIcon = icon;
    143         mGroupMessage = message;
    144         mShowDonNotAsk = showDonNotAsk;
    145         mDoNotAskChecked = false;
    146         // If this is a second (or later) permission and the views exist, then animate.
    147         if (mIconView != null) {
    148             if (mGroupIndex > 0) {
    149                 // The first message will be announced as the title of the activity, all others
    150                 // we need to announce ourselves.
    151                 mDescContainer.announceForAccessibility(message);
    152                 animateToPermission();
    153             } else {
    154                 updateDescription();
    155                 updateGroup();
    156                 updateDoNotAskCheckBox();
    157             }
    158         }
    159     }
    160 
    161     private void animateToPermission() {
    162         if (mHeightControllers == null) {
    163             // We need to manually control the height of any views heigher than the root that
    164             // we inflate.  Find all the views up to the root and create ViewHeightControllers for
    165             // them.
    166             mHeightControllers = new ArrayList<>();
    167             ViewRootImpl viewRoot = mRootView.getViewRootImpl();
    168             ViewParent v = mRootView.getParent();
    169             addHeightController(mDialogContainer);
    170             addHeightController(mRootView);
    171             while (v != viewRoot) {
    172                 addHeightController((View) v);
    173                 v = v.getParent();
    174             }
    175             // On the heighest level view, we want to setTop rather than setBottom to control the
    176             // height, this way the dialog will grow up rather than down.
    177             ViewHeightController realRootView =
    178                     mHeightControllers.get(mHeightControllers.size() - 1);
    179             realRootView.setControlTop(true);
    180         }
    181 
    182         // Grab the current height/y positions, then wait for the layout to change,
    183         // so we can get the end height/y positions.
    184         final SparseArray<Float> startPositions = getViewPositions();
    185         final int startHeight = mRootView.getLayoutHeight();
    186         mRootView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
    187             @Override
    188             public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
    189                     int oldTop, int oldRight, int oldBottom) {
    190                 mRootView.removeOnLayoutChangeListener(this);
    191                 SparseArray<Float> endPositions = getViewPositions();
    192                 int endHeight = mRootView.getLayoutHeight();
    193                 if (startPositions.get(R.id.do_not_ask_checkbox) == 0
    194                         && endPositions.get(R.id.do_not_ask_checkbox) != 0) {
    195                     // If the checkbox didn't have a position before but has one now then set
    196                     // the start position to the end position because it just became visible.
    197                     startPositions.put(R.id.do_not_ask_checkbox,
    198                             endPositions.get(R.id.do_not_ask_checkbox));
    199                 }
    200                 animateYPos(startPositions, endPositions, endHeight - startHeight);
    201             }
    202         });
    203 
    204         // Fade out old description group and scale out the icon for it.
    205         Interpolator interpolator = AnimationUtils.loadInterpolator(mContext,
    206                 android.R.interpolator.fast_out_linear_in);
    207         mIconView.animate()
    208                 .scaleX(0)
    209                 .scaleY(0)
    210                 .setStartDelay(FADE_OUT_START_DELAY)
    211                 .setDuration(FADE_OUT_START_LENGTH)
    212                 .setInterpolator(interpolator)
    213                 .start();
    214         mCurrentDesc.animate()
    215                 .alpha(0)
    216                 .setStartDelay(FADE_OUT_START_DELAY)
    217                 .setDuration(FADE_OUT_START_LENGTH)
    218                 .setInterpolator(interpolator)
    219                 .setListener(null)
    220                 .start();
    221 
    222         // Update the index of the permission after the animations have started.
    223         mCurrentGroupView.getHandler().postDelayed(mUpdateGroup, GROUP_UPDATE_DELAY);
    224 
    225         // Add the new description and translate it in.
    226         mNextDesc = (ViewGroup) LayoutInflater.from(mContext).inflate(
    227                 R.layout.permission_description, mDescContainer, false);
    228 
    229         mMessageView = (TextView) mNextDesc.findViewById(R.id.permission_message);
    230         mIconView = (ImageView) mNextDesc.findViewById(R.id.permission_icon);
    231         updateDescription();
    232 
    233         int width = mDescContainer.getRootView().getWidth();
    234         mDescContainer.addView(mNextDesc);
    235         mNextDesc.setTranslationX(width);
    236 
    237         final View oldDesc = mCurrentDesc;
    238         // Remove the old view from the description, so that we can shrink if necessary.
    239         mDescContainer.removeView(oldDesc);
    240         oldDesc.setPadding(mDescContainer.getLeft(), mDescContainer.getTop(),
    241                 mRootView.getRight() - mDescContainer.getRight(), 0);
    242         mRootView.addView(oldDesc);
    243 
    244         mCurrentDesc = mNextDesc;
    245         mNextDesc.animate()
    246                 .translationX(0)
    247                 .setStartDelay(TRANSLATE_START_DELAY)
    248                 .setDuration(TRANSLATE_LENGTH)
    249                 .setInterpolator(AnimationUtils.loadInterpolator(mContext,
    250                         android.R.interpolator.linear_out_slow_in))
    251                 .setListener(new AnimatorListenerAdapter() {
    252                     @Override
    253                     public void onAnimationEnd(Animator animation) {
    254                         // This is the longest animation, when it finishes, we are done.
    255                         mRootView.removeView(oldDesc);
    256                     }
    257                 })
    258                 .start();
    259 
    260         boolean visibleBefore = mDoNotAskCheckbox.getVisibility() == View.VISIBLE;
    261         updateDoNotAskCheckBox();
    262         boolean visibleAfter = mDoNotAskCheckbox.getVisibility() == View.VISIBLE;
    263         if (visibleBefore != visibleAfter) {
    264             Animation anim = AnimationUtils.loadAnimation(mContext,
    265                     visibleAfter ? android.R.anim.fade_in : android.R.anim.fade_out);
    266             anim.setStartOffset(visibleAfter ? DO_NOT_ASK_CHECK_DELAY : 0);
    267             mDoNotAskCheckbox.startAnimation(anim);
    268         }
    269     }
    270 
    271     private void addHeightController(View v) {
    272         ViewHeightController heightController = new ViewHeightController(v);
    273         heightController.setHeight(v.getHeight());
    274         mHeightControllers.add(heightController);
    275     }
    276 
    277     private SparseArray<Float> getViewPositions() {
    278         SparseArray<Float> locMap = new SparseArray<>();
    279         final int N = mDialogContainer.getChildCount();
    280         for (int i = 0; i < N; i++) {
    281             View child = mDialogContainer.getChildAt(i);
    282             if (child.getId() <= 0) {
    283                 // Only track views with ids.
    284                 continue;
    285             }
    286             locMap.put(child.getId(), child.getY());
    287         }
    288         return locMap;
    289     }
    290 
    291     private void animateYPos(SparseArray<Float> startPositions, SparseArray<Float> endPositions,
    292             int heightDiff) {
    293         final int N = startPositions.size();
    294         for (int i = 0; i < N; i++) {
    295             int key = startPositions.keyAt(i);
    296             float start = startPositions.get(key);
    297             float end = endPositions.get(key);
    298             if (start != end) {
    299                 final View child = mDialogContainer.findViewById(key);
    300                 child.setTranslationY(start - end);
    301                 child.animate()
    302                         .setStartDelay(SIZE_START_DELAY)
    303                         .setDuration(SIZE_START_LENGTH)
    304                         .translationY(0)
    305                         .start();
    306             }
    307         }
    308         for (int i = 0; i < mHeightControllers.size(); i++) {
    309             mHeightControllers.get(i).animateAddHeight(heightDiff);
    310         }
    311     }
    312 
    313     @Override
    314     public View createView() {
    315         mRootView = (ManualLayoutFrame) LayoutInflater.from(mContext)
    316                 .inflate(R.layout.grant_permissions, null);
    317         ((ButtonBarLayout) mRootView.findViewById(R.id.button_group)).setAllowStacking(
    318                 Resources.getSystem().getBoolean(
    319                         com.android.internal.R.bool.allow_stacked_button_bar));
    320 
    321         mDialogContainer = (ViewGroup) mRootView.findViewById(R.id.dialog_container);
    322         mMessageView = (TextView) mRootView.findViewById(R.id.permission_message);
    323         mIconView = (ImageView) mRootView.findViewById(R.id.permission_icon);
    324         mCurrentGroupView = (TextView) mRootView.findViewById(R.id.current_page_text);
    325         mDoNotAskCheckbox = (CheckBox) mRootView.findViewById(R.id.do_not_ask_checkbox);
    326         mAllowButton = (Button) mRootView.findViewById(R.id.permission_allow_button);
    327 
    328         mDescContainer = (ViewGroup) mRootView.findViewById(R.id.desc_container);
    329         mCurrentDesc = (ViewGroup) mRootView.findViewById(R.id.perm_desc_root);
    330 
    331         mAllowButton.setOnClickListener(this);
    332         mRootView.findViewById(R.id.permission_deny_button).setOnClickListener(this);
    333         mDoNotAskCheckbox.setOnClickListener(this);
    334 
    335         if (mGroupName != null) {
    336             updateDescription();
    337             updateGroup();
    338             updateDoNotAskCheckBox();
    339         }
    340 
    341         return mRootView;
    342     }
    343 
    344     @Override
    345     public void updateWindowAttributes(LayoutParams outLayoutParams) {
    346         // No-op
    347     }
    348 
    349     private void updateDescription() {
    350         mIconView.setImageDrawable(mGroupIcon.loadDrawable(mContext));
    351         mMessageView.setText(mGroupMessage);
    352     }
    353 
    354     private void updateGroup() {
    355         if (mGroupCount > 1) {
    356             mCurrentGroupView.setVisibility(View.VISIBLE);
    357             mCurrentGroupView.setText(mContext.getString(R.string.current_permission_template,
    358                     mGroupIndex + 1, mGroupCount));
    359         } else {
    360             mCurrentGroupView.setVisibility(View.INVISIBLE);
    361         }
    362     }
    363 
    364     private void updateDoNotAskCheckBox() {
    365         if (mShowDonNotAsk) {
    366             mDoNotAskCheckbox.setVisibility(View.VISIBLE);
    367             mDoNotAskCheckbox.setOnClickListener(this);
    368             mDoNotAskCheckbox.setChecked(mDoNotAskChecked);
    369         } else {
    370             mDoNotAskCheckbox.setVisibility(View.GONE);
    371             mDoNotAskCheckbox.setOnClickListener(null);
    372         }
    373     }
    374 
    375     @Override
    376     public void onClick(View view) {
    377         switch (view.getId()) {
    378             case R.id.permission_allow_button:
    379                 if (mResultListener != null) {
    380                     view.clearAccessibilityFocus();
    381                     mResultListener.onPermissionGrantResult(mGroupName, true, false);
    382                 }
    383                 break;
    384             case R.id.permission_deny_button:
    385                 mAllowButton.setEnabled(true);
    386                 if (mResultListener != null) {
    387                     view.clearAccessibilityFocus();
    388                     mResultListener.onPermissionGrantResult(mGroupName, false,
    389                             mDoNotAskCheckbox.isChecked());
    390                 }
    391                 break;
    392             case R.id.do_not_ask_checkbox:
    393                 mAllowButton.setEnabled(!mDoNotAskCheckbox.isChecked());
    394                 break;
    395         }
    396     }
    397 
    398     @Override
    399     public void onBackPressed() {
    400         if (mResultListener != null) {
    401             final boolean doNotAskAgain = mDoNotAskCheckbox.isChecked();
    402             mResultListener.onPermissionGrantResult(mGroupName, false, doNotAskAgain);
    403         }
    404     }
    405 
    406     /**
    407      * Manually controls the height of a view through getBottom/setTop.  Also listens
    408      * for layout changes and sets the height again to be sure it doesn't change.
    409      */
    410     private static final class ViewHeightController implements OnLayoutChangeListener {
    411         private final View mView;
    412         private int mHeight;
    413         private int mNextHeight;
    414         private boolean mControlTop;
    415         private ObjectAnimator mAnimator;
    416 
    417         public ViewHeightController(View view) {
    418             mView = view;
    419             mView.addOnLayoutChangeListener(this);
    420         }
    421 
    422         public void setControlTop(boolean controlTop) {
    423             mControlTop = controlTop;
    424         }
    425 
    426         public void animateAddHeight(int heightDiff) {
    427             if (heightDiff != 0) {
    428                 if (mNextHeight == 0) {
    429                     mNextHeight = mHeight;
    430                 }
    431                 mNextHeight += heightDiff;
    432                 if (mAnimator != null) {
    433                     mAnimator.cancel();
    434                 }
    435                 mAnimator = ObjectAnimator.ofInt(this, "height", mHeight, mNextHeight);
    436                 mAnimator.setStartDelay(SIZE_START_DELAY);
    437                 mAnimator.setDuration(SIZE_START_LENGTH);
    438                 mAnimator.start();
    439             }
    440         }
    441 
    442         public void setHeight(int height) {
    443             mHeight = height;
    444             updateHeight();
    445         }
    446 
    447         @Override
    448         public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
    449                 int oldTop, int oldRight, int oldBottom) {
    450             // Ensure that the height never changes.
    451             updateHeight();
    452         }
    453 
    454         private void updateHeight() {
    455             if (mControlTop) {
    456                 mView.setTop(mView.getBottom() - mHeight);
    457             } else {
    458                 mView.setBottom(mView.getTop() + mHeight);
    459             }
    460         }
    461     }
    462 }
    463