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