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
17package com.android.packageinstaller.permission.ui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.drawable.Icon;
25import android.os.Bundle;
26import android.util.SparseArray;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.View.OnClickListener;
30import android.view.View.OnLayoutChangeListener;
31import android.view.ViewGroup;
32import android.view.ViewParent;
33import android.view.ViewRootImpl;
34import android.view.WindowManager.LayoutParams;
35import android.view.animation.Animation;
36import android.view.animation.AnimationUtils;
37import android.view.animation.Interpolator;
38import android.widget.Button;
39import android.widget.CheckBox;
40import android.widget.ImageView;
41import android.widget.TextView;
42
43import com.android.internal.widget.ButtonBarLayout;
44import com.android.packageinstaller.R;
45
46import java.util.ArrayList;
47
48final 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