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