• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
GrantPermissionsDefaultViewHandler(Context context)104     GrantPermissionsDefaultViewHandler(Context context) {
105         mContext = context;
106     }
107 
108     @Override
setResultListener(ResultListener listener)109     public GrantPermissionsDefaultViewHandler setResultListener(ResultListener listener) {
110         mResultListener = listener;
111         return this;
112     }
113 
114     @Override
saveInstanceState(Bundle arguments)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
loadInstanceState(Bundle savedInstanceState)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
updateUi(String groupName, int groupCount, int groupIndex, Icon icon, CharSequence message, boolean showDonNotAsk)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 
animateToPermission()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 
addHeightController(View v)271     private void addHeightController(View v) {
272         ViewHeightController heightController = new ViewHeightController(v);
273         heightController.setHeight(v.getHeight());
274         mHeightControllers.add(heightController);
275     }
276 
getViewPositions()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 
animateYPos(SparseArray<Float> startPositions, SparseArray<Float> endPositions, int heightDiff)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
createView()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
updateWindowAttributes(LayoutParams outLayoutParams)345     public void updateWindowAttributes(LayoutParams outLayoutParams) {
346         // No-op
347     }
348 
updateDescription()349     private void updateDescription() {
350         mIconView.setImageDrawable(mGroupIcon.loadDrawable(mContext));
351         mMessageView.setText(mGroupMessage);
352     }
353 
updateGroup()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 
updateDoNotAskCheckBox()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
onClick(View view)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
onBackPressed()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 
ViewHeightController(View view)417         public ViewHeightController(View view) {
418             mView = view;
419             mView.addOnLayoutChangeListener(this);
420         }
421 
setControlTop(boolean controlTop)422         public void setControlTop(boolean controlTop) {
423             mControlTop = controlTop;
424         }
425 
animateAddHeight(int heightDiff)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 
setHeight(int height)442         public void setHeight(int height) {
443             mHeight = height;
444             updateHeight();
445         }
446 
447         @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)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 
updateHeight()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