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.handheld;
18
19import android.app.Activity;
20import android.content.Intent;
21import android.graphics.drawable.Icon;
22import android.os.Bundle;
23import android.view.LayoutInflater;
24import android.view.View;
25import android.view.View.OnClickListener;
26import android.view.ViewGroup;
27import android.view.WindowManager.LayoutParams;
28import android.view.accessibility.AccessibilityNodeInfo;
29import android.view.animation.AnimationUtils;
30import android.view.animation.Interpolator;
31import android.widget.Button;
32import android.widget.CheckBox;
33import android.widget.ImageView;
34import android.widget.TextView;
35
36import com.android.packageinstaller.R;
37import com.android.packageinstaller.permission.ui.ButtonBarLayout;
38import com.android.packageinstaller.permission.ui.GrantPermissionsViewHandler;
39import com.android.packageinstaller.permission.ui.ManagePermissionsActivity;
40import com.android.packageinstaller.permission.ui.ManualLayoutFrame;
41
42public class GrantPermissionsViewHandlerImpl implements GrantPermissionsViewHandler,
43        OnClickListener {
44
45    public static final String ARG_GROUP_NAME = "ARG_GROUP_NAME";
46    public static final String ARG_GROUP_COUNT = "ARG_GROUP_COUNT";
47    public static final String ARG_GROUP_INDEX = "ARG_GROUP_INDEX";
48    public static final String ARG_GROUP_ICON = "ARG_GROUP_ICON";
49    public static final String ARG_GROUP_MESSAGE = "ARG_GROUP_MESSAGE";
50    public static final String ARG_GROUP_SHOW_DO_NOT_ASK = "ARG_GROUP_SHOW_DO_NOT_ASK";
51    public static final String ARG_GROUP_DO_NOT_ASK_CHECKED = "ARG_GROUP_DO_NOT_ASK_CHECKED";
52
53    // Animation parameters.
54    private static final long OUT_DURATION = 200;
55    private static final long IN_DURATION = 300;
56
57    private final Activity mActivity;
58    private final String mAppPackageName;
59    private final boolean mPermissionReviewRequired;
60
61    private ResultListener mResultListener;
62
63    private String mGroupName;
64    private int mGroupCount;
65    private int mGroupIndex;
66    private Icon mGroupIcon;
67    private CharSequence mGroupMessage;
68    private boolean mShowDonNotAsk;
69    private boolean mDoNotAskChecked;
70
71    private ImageView mIconView;
72    private TextView mCurrentGroupView;
73    private TextView mMessageView;
74    private CheckBox mDoNotAskCheckbox;
75    private Button mAllowButton;
76    private Button mMoreInfoButton;
77
78    private ManualLayoutFrame mRootView;
79
80    // Needed for animation
81    private ViewGroup mDescContainer;
82    private ViewGroup mCurrentDesc;
83    private ViewGroup mDialogContainer;
84    private ButtonBarLayout mButtonBar;
85
86    public GrantPermissionsViewHandlerImpl(Activity activity, String appPackageName) {
87        mActivity = activity;
88        mAppPackageName = appPackageName;
89        mPermissionReviewRequired = activity.getPackageManager().isPermissionReviewModeEnabled();
90    }
91
92    @Override
93    public GrantPermissionsViewHandlerImpl setResultListener(ResultListener listener) {
94        mResultListener = listener;
95        return this;
96    }
97
98    @Override
99    public void saveInstanceState(Bundle arguments) {
100        arguments.putString(ARG_GROUP_NAME, mGroupName);
101        arguments.putInt(ARG_GROUP_COUNT, mGroupCount);
102        arguments.putInt(ARG_GROUP_INDEX, mGroupIndex);
103        arguments.putParcelable(ARG_GROUP_ICON, mGroupIcon);
104        arguments.putCharSequence(ARG_GROUP_MESSAGE, mGroupMessage);
105        arguments.putBoolean(ARG_GROUP_SHOW_DO_NOT_ASK, mShowDonNotAsk);
106        arguments.putBoolean(ARG_GROUP_DO_NOT_ASK_CHECKED, mDoNotAskCheckbox.isChecked());
107    }
108
109    @Override
110    public void loadInstanceState(Bundle savedInstanceState) {
111        mGroupName = savedInstanceState.getString(ARG_GROUP_NAME);
112        mGroupMessage = savedInstanceState.getCharSequence(ARG_GROUP_MESSAGE);
113        mGroupIcon = savedInstanceState.getParcelable(ARG_GROUP_ICON);
114        mGroupCount = savedInstanceState.getInt(ARG_GROUP_COUNT);
115        mGroupIndex = savedInstanceState.getInt(ARG_GROUP_INDEX);
116        mShowDonNotAsk = savedInstanceState.getBoolean(ARG_GROUP_SHOW_DO_NOT_ASK);
117        mDoNotAskChecked = savedInstanceState.getBoolean(ARG_GROUP_DO_NOT_ASK_CHECKED);
118
119        updateDoNotAskCheckBox();
120    }
121
122    @Override
123    public void updateUi(String groupName, int groupCount, int groupIndex, Icon icon,
124            CharSequence message, boolean showDonNotAsk) {
125        mGroupName = groupName;
126        mGroupCount = groupCount;
127        mGroupIndex = groupIndex;
128        mGroupIcon = icon;
129        mGroupMessage = message;
130        mShowDonNotAsk = showDonNotAsk;
131        mDoNotAskChecked = false;
132        // If this is a second (or later) permission and the views exist, then animate.
133        if (mIconView != null) {
134            if (mGroupIndex > 0) {
135                animateToPermission();
136            } else {
137                updateDescription();
138                updateGroup();
139                updateDoNotAskCheckBox();
140            }
141        }
142    }
143
144    public void onConfigurationChanged() {
145        mRootView.onConfigurationChanged();
146    }
147
148    private void animateOldContent(Runnable callback) {
149        // Fade out old description group and scale out the icon for it.
150        Interpolator interpolator = AnimationUtils.loadInterpolator(mActivity,
151                android.R.interpolator.fast_out_linear_in);
152
153        // Icon scale to zero
154        mIconView.animate()
155                .scaleX(0)
156                .scaleY(0)
157                .setDuration(OUT_DURATION)
158                .setInterpolator(interpolator)
159                .start();
160
161        // Description fade out
162        mCurrentDesc.animate()
163                .alpha(0)
164                .setDuration(OUT_DURATION)
165                .setInterpolator(interpolator)
166                .withEndAction(callback)
167                .start();
168
169        // Checkbox fade out if needed
170        if (!mShowDonNotAsk && mDoNotAskCheckbox.getVisibility() == View.VISIBLE) {
171            mDoNotAskCheckbox.animate()
172                    .alpha(0)
173                    .setDuration(OUT_DURATION)
174                    .setInterpolator(interpolator)
175                    .start();
176        }
177    }
178
179    private void attachNewContent(final Runnable callback) {
180        mCurrentDesc = (ViewGroup) LayoutInflater.from(mActivity).inflate(
181                R.layout.permission_description, mDescContainer, false);
182        mDescContainer.removeAllViews();
183        mDescContainer.addView(mCurrentDesc);
184
185        mDialogContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
186                @Override
187                public void onLayoutChange(View v, int left, int top, int right, int bottom,
188                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
189                    mDialogContainer.removeOnLayoutChangeListener(this);
190
191                    // Prepare new content to the right to be moved in
192                    final int containerWidth = mDescContainer.getWidth();
193                    mCurrentDesc.setTranslationX(containerWidth);
194
195                    // How much scale for the dialog to appear the same?
196                    final int oldDynamicHeight = oldBottom - oldTop - mButtonBar.getHeight();
197                    final float scaleY = (float) oldDynamicHeight / mDescContainer.getHeight();
198
199                    // How much to translate for the dialog to appear the same?
200                    final int translationCompensatingScale = (int) (scaleY
201                            * mDescContainer.getHeight() - mDescContainer.getHeight()) / 2;
202                    final int translationY = (oldTop - top) + translationCompensatingScale;
203
204                    // Animate to the current layout
205                    mDescContainer.setScaleY(scaleY);
206                    mDescContainer.setTranslationY(translationY);
207                    mDescContainer.animate()
208                            .translationY(0)
209                            .scaleY(1.0f)
210                            .setInterpolator(AnimationUtils.loadInterpolator(mActivity,
211                                    android.R.interpolator.linear_out_slow_in))
212                            .setDuration(IN_DURATION)
213                            .withEndAction(callback)
214                            .start();
215                }
216            }
217        );
218
219        mMessageView = (TextView) mCurrentDesc.findViewById(R.id.permission_message);
220        mIconView = (ImageView) mCurrentDesc.findViewById(R.id.permission_icon);
221
222        final boolean doNotAskWasShown = mDoNotAskCheckbox.getVisibility() == View.VISIBLE;
223
224        updateDescription();
225        updateGroup();
226        updateDoNotAskCheckBox();
227
228        if (!doNotAskWasShown && mShowDonNotAsk) {
229            mDoNotAskCheckbox.setAlpha(0);
230        }
231    }
232
233    private void animateNewContent() {
234        Interpolator interpolator = AnimationUtils.loadInterpolator(mActivity,
235                android.R.interpolator.linear_out_slow_in);
236
237        // Description slide in
238        mCurrentDesc.animate()
239                .translationX(0)
240                .setDuration(IN_DURATION)
241                .setInterpolator(interpolator)
242                .start();
243
244        // Checkbox fade in if needed
245        if (mShowDonNotAsk && mDoNotAskCheckbox.getVisibility() == View.VISIBLE
246                && mDoNotAskCheckbox.getAlpha() < 1.0f) {
247            mDoNotAskCheckbox.setAlpha(0);
248            mDoNotAskCheckbox.animate()
249                    .alpha(1.0f)
250                    .setDuration(IN_DURATION)
251                    .setInterpolator(interpolator)
252                    .start();
253        }
254    }
255
256    private void animateToPermission() {
257        // Remove the old content
258        animateOldContent(new Runnable() {
259            @Override
260            public void run() {
261                // Add the new content
262                attachNewContent(new Runnable() {
263                    @Override
264                    public void run() {
265                        // Animate the new content
266                        animateNewContent();
267                    }
268                });
269            }
270        });
271    }
272
273    @Override
274    public View createView() {
275        mRootView = (ManualLayoutFrame) LayoutInflater.from(mActivity)
276                .inflate(R.layout.grant_permissions, null);
277        mButtonBar = (ButtonBarLayout) mRootView.findViewById(R.id.button_group);
278        mButtonBar.setAllowStacking(true);
279        mMessageView = (TextView) mRootView.findViewById(R.id.permission_message);
280        mIconView = (ImageView) mRootView.findViewById(R.id.permission_icon);
281        mCurrentGroupView = (TextView) mRootView.findViewById(R.id.current_page_text);
282        mDoNotAskCheckbox = (CheckBox) mRootView.findViewById(R.id.do_not_ask_checkbox);
283
284        mAllowButton = (Button) mRootView.findViewById(R.id.permission_allow_button);
285        mAllowButton.setOnClickListener(this);
286
287        if (mPermissionReviewRequired) {
288            mMoreInfoButton = (Button) mRootView.findViewById(R.id.permission_more_info_button);
289            mMoreInfoButton.setVisibility(View.VISIBLE);
290            mMoreInfoButton.setOnClickListener(this);
291        }
292
293        mDialogContainer = (ViewGroup) mRootView.findViewById(R.id.dialog_container);
294        mDescContainer = (ViewGroup) mRootView.findViewById(R.id.desc_container);
295        mCurrentDesc = (ViewGroup) mRootView.findViewById(R.id.perm_desc_root);
296
297        mRootView.findViewById(R.id.permission_deny_button).setOnClickListener(this);
298        mDoNotAskCheckbox.setOnClickListener(this);
299
300        if (mGroupName != null) {
301            updateDescription();
302            updateGroup();
303            updateDoNotAskCheckBox();
304        }
305
306        return mRootView;
307    }
308
309    @Override
310    public void updateWindowAttributes(LayoutParams outLayoutParams) {
311        // No-op
312    }
313
314    private void updateDescription() {
315        if (mGroupIcon != null) {
316            mIconView.setImageDrawable(mGroupIcon.loadDrawable(mActivity));
317        }
318        mMessageView.setText(mGroupMessage);
319    }
320
321    private void updateGroup() {
322        if (mGroupCount > 1) {
323            mCurrentGroupView.setVisibility(View.VISIBLE);
324            mCurrentGroupView.setText(mActivity.getString(R.string.current_permission_template,
325                    mGroupIndex + 1, mGroupCount));
326        } else {
327            mCurrentGroupView.setVisibility(View.GONE);
328        }
329    }
330
331    private void updateDoNotAskCheckBox() {
332        if (mShowDonNotAsk) {
333            mDoNotAskCheckbox.setVisibility(View.VISIBLE);
334            mDoNotAskCheckbox.setOnClickListener(this);
335            mDoNotAskCheckbox.setChecked(mDoNotAskChecked);
336            mAllowButton.setEnabled(!mDoNotAskChecked);
337        } else {
338            mDoNotAskCheckbox.setVisibility(View.GONE);
339            mDoNotAskCheckbox.setOnClickListener(null);
340            mAllowButton.setEnabled(true);
341        }
342    }
343
344    @Override
345    public void onClick(View view) {
346        switch (view.getId()) {
347            case R.id.permission_allow_button:
348                if (mResultListener != null) {
349                    view.performAccessibilityAction(
350                            AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
351                    mResultListener.onPermissionGrantResult(mGroupName, true, false);
352                }
353                break;
354            case R.id.permission_deny_button:
355                mAllowButton.setEnabled(true);
356                if (mResultListener != null) {
357                    view.performAccessibilityAction(
358                            AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
359                    mResultListener.onPermissionGrantResult(mGroupName, false,
360                            mShowDonNotAsk && mDoNotAskCheckbox.isChecked());
361                }
362                break;
363            case R.id.permission_more_info_button:
364                Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
365                intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mAppPackageName);
366                intent.putExtra(ManagePermissionsActivity.EXTRA_ALL_PERMISSIONS, true);
367                mActivity.startActivity(intent);
368                break;
369            case R.id.do_not_ask_checkbox:
370                mAllowButton.setEnabled(!mDoNotAskCheckbox.isChecked());
371                break;
372        }
373    }
374
375    @Override
376    public void onBackPressed() {
377        if (mResultListener != null) {
378            final boolean doNotAskAgain = mDoNotAskCheckbox.isChecked();
379            mResultListener.onPermissionGrantResult(mGroupName, false, doNotAskAgain);
380        }
381    }
382}
383