1 /* 2 * Copyright (C) 2018 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.settings.panel; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.ValueAnimator; 24 import android.app.settings.SettingsEnums; 25 import android.net.Uri; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.text.TextUtils; 29 import android.view.Gravity; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.ViewTreeObserver; 34 import android.view.animation.DecelerateInterpolator; 35 import android.widget.Button; 36 import android.widget.ImageView; 37 import android.widget.LinearLayout; 38 import android.widget.ProgressBar; 39 import android.widget.TextView; 40 41 import androidx.annotation.NonNull; 42 import androidx.annotation.Nullable; 43 import androidx.core.graphics.drawable.IconCompat; 44 import androidx.fragment.app.Fragment; 45 import androidx.fragment.app.FragmentActivity; 46 import androidx.lifecycle.LifecycleObserver; 47 import androidx.lifecycle.LiveData; 48 import androidx.recyclerview.widget.LinearLayoutManager; 49 import androidx.recyclerview.widget.RecyclerView; 50 import androidx.slice.Slice; 51 import androidx.slice.SliceMetadata; 52 import androidx.slice.widget.SliceLiveData; 53 54 import com.android.internal.annotations.VisibleForTesting; 55 import com.android.settings.R; 56 import com.android.settings.overlay.FeatureFactory; 57 import com.android.settings.panel.PanelLoggingContract.PanelClosedKeys; 58 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 59 import com.android.settingslib.utils.ThreadUtils; 60 61 import com.google.android.setupdesign.DividerItemDecoration; 62 63 import java.util.ArrayList; 64 import java.util.Arrays; 65 import java.util.LinkedHashMap; 66 import java.util.List; 67 import java.util.Map; 68 69 @Deprecated(forRemoval = true) 70 public class PanelFragment extends Fragment { 71 72 private static final String TAG = "PanelFragment"; 73 74 /** 75 * Duration of the animation entering the screen, in milliseconds. 76 */ 77 private static final int DURATION_ANIMATE_PANEL_EXPAND_MS = 250; 78 79 /** 80 * Duration of the animation exiting the screen, in milliseconds. 81 */ 82 private static final int DURATION_ANIMATE_PANEL_COLLAPSE_MS = 200; 83 84 /** 85 * Duration of timeout waiting for Slice data to bind, in milliseconds. 86 */ 87 private static final int DURATION_SLICE_BINDING_TIMEOUT_MS = 250; 88 89 @VisibleForTesting 90 View mLayoutView; 91 private TextView mTitleView; 92 private Button mSeeMoreButton; 93 private Button mDoneButton; 94 private RecyclerView mPanelSlices; 95 private PanelContent mPanel; 96 private MetricsFeatureProvider mMetricsProvider; 97 private String mPanelClosedKey; 98 private LinearLayout mPanelHeader; 99 private ImageView mTitleIcon; 100 private LinearLayout mTitleGroup; 101 private LinearLayout mHeaderLayout; 102 private TextView mHeaderTitle; 103 private TextView mHeaderSubtitle; 104 private int mMaxHeight; 105 private boolean mPanelCreating; 106 private ProgressBar mProgressBar; 107 108 private final Map<Uri, LiveData<Slice>> mSliceLiveData = new LinkedHashMap<>(); 109 110 @VisibleForTesting 111 PanelSlicesLoaderCountdownLatch mPanelSlicesLoaderCountdownLatch; 112 113 private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener = () -> { 114 return false; 115 }; 116 117 private final ViewTreeObserver.OnGlobalLayoutListener mPanelLayoutListener = 118 new ViewTreeObserver.OnGlobalLayoutListener() { 119 @Override 120 public void onGlobalLayout() { 121 if (mLayoutView.getHeight() > mMaxHeight) { 122 final ViewGroup.LayoutParams params = mLayoutView.getLayoutParams(); 123 params.height = mMaxHeight; 124 mLayoutView.setLayoutParams(params); 125 } 126 } 127 }; 128 129 private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = 130 new ViewTreeObserver.OnGlobalLayoutListener() { 131 @Override 132 public void onGlobalLayout() { 133 animateIn(); 134 if (mPanelSlices != null) { 135 mPanelSlices.getViewTreeObserver().removeOnGlobalLayoutListener(this); 136 } 137 mPanelCreating = false; 138 } 139 }; 140 141 private PanelSlicesAdapter mAdapter; 142 143 @Nullable 144 @Override onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)145 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 146 @Nullable Bundle savedInstanceState) { 147 mLayoutView = inflater.inflate(R.layout.panel_layout, container, false); 148 mLayoutView.getViewTreeObserver() 149 .addOnGlobalLayoutListener(mPanelLayoutListener); 150 mMaxHeight = getResources().getDimensionPixelSize(R.dimen.output_switcher_slice_max_height); 151 mPanelCreating = true; 152 createPanelContent(); 153 return mLayoutView; 154 } 155 156 /** 157 * Animate the old panel out from the screen, then update the panel with new content once the 158 * animation is done. 159 * <p> 160 * Takes the entire panel and animates out from behind the navigation bar. 161 * <p> 162 * Call createPanelContent() once animation end. 163 */ updatePanelWithAnimation()164 void updatePanelWithAnimation() { 165 mPanelCreating = true; 166 final View panelContent = mLayoutView.findViewById(R.id.panel_container); 167 final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView, 168 0.0f /* startY */, panelContent.getHeight() /* endY */, 169 1.0f /* startAlpha */, 0.0f /* endAlpha */, 170 DURATION_ANIMATE_PANEL_COLLAPSE_MS); 171 172 final ValueAnimator animator = new ValueAnimator(); 173 animator.setFloatValues(0.0f, 1.0f); 174 animatorSet.play(animator); 175 animatorSet.addListener(new AnimatorListenerAdapter() { 176 @Override 177 public void onAnimationEnd(Animator animation) { 178 createPanelContent(); 179 } 180 }); 181 animatorSet.start(); 182 } 183 isPanelCreating()184 boolean isPanelCreating() { 185 return mPanelCreating; 186 } 187 createPanelContent()188 private void createPanelContent() { 189 final FragmentActivity activity = getActivity(); 190 if (activity == null) { 191 return; 192 } 193 194 if (mLayoutView == null) { 195 activity.finish(); 196 return; 197 } 198 199 final ViewGroup.LayoutParams params = mLayoutView.getLayoutParams(); 200 params.height = ViewGroup.LayoutParams.WRAP_CONTENT; 201 mLayoutView.setLayoutParams(params); 202 203 mPanelSlices = mLayoutView.findViewById(R.id.panel_parent_layout); 204 mSeeMoreButton = mLayoutView.findViewById(R.id.see_more); 205 mDoneButton = mLayoutView.findViewById(R.id.done); 206 mTitleView = mLayoutView.findViewById(R.id.panel_title); 207 mPanelHeader = mLayoutView.findViewById(R.id.panel_header); 208 mTitleIcon = mLayoutView.findViewById(R.id.title_icon); 209 mTitleGroup = mLayoutView.findViewById(R.id.title_group); 210 mHeaderLayout = mLayoutView.findViewById(R.id.header_layout); 211 mHeaderTitle = mLayoutView.findViewById(R.id.header_title); 212 mHeaderSubtitle = mLayoutView.findViewById(R.id.header_subtitle); 213 mProgressBar = mLayoutView.findViewById(R.id.progress_bar); 214 215 // Make the panel layout gone here, to avoid janky animation when updating from old panel. 216 // We will make it visible once the panel is ready to load. 217 mPanelSlices.setVisibility(View.GONE); 218 // Remove the animator to avoid a RecyclerView crash. 219 mPanelSlices.setItemAnimator(null); 220 221 final Bundle arguments = getArguments(); 222 final String callingPackageName = 223 arguments.getString(SettingsPanelActivity.KEY_CALLING_PACKAGE_NAME); 224 225 mPanel = FeatureFactory.getFeatureFactory() 226 .getPanelFeatureProvider() 227 .getPanel(activity, arguments); 228 229 if (mPanel == null) { 230 activity.finish(); 231 return; 232 } 233 234 mPanel.registerCallback(new LocalPanelCallback()); 235 if (mPanel instanceof LifecycleObserver) { 236 getLifecycle().addObserver((LifecycleObserver) mPanel); 237 } 238 239 mMetricsProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 240 241 updateProgressBar(); 242 243 mPanelSlices.setLayoutManager(new LinearLayoutManager((activity))); 244 // Add predraw listener to remove the animation and while we wait for Slices to load. 245 mLayoutView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener); 246 247 // Start loading Slices. When finished, the Panel will animate in. 248 loadAllSlices(); 249 250 final IconCompat icon = mPanel.getIcon(); 251 final CharSequence title = mPanel.getTitle(); 252 final CharSequence subtitle = mPanel.getSubTitle(); 253 254 if (icon != null || (subtitle != null && subtitle.length() > 0)) { 255 enablePanelHeader(icon, title, subtitle); 256 } else { 257 enableTitle(title); 258 } 259 260 mSeeMoreButton.setOnClickListener(getSeeMoreListener()); 261 mDoneButton.setOnClickListener(getCloseListener()); 262 263 if (mPanel.isCustomizedButtonUsed()) { 264 enableCustomizedButton(); 265 } else if (mPanel.getSeeMoreIntent() == null) { 266 // If getSeeMoreIntent() is null hide the mSeeMoreButton. 267 mSeeMoreButton.setVisibility(View.GONE); 268 } 269 270 // Log panel opened. 271 mMetricsProvider.action( 272 0 /* attribution */, 273 SettingsEnums.PAGE_VISIBLE /* opened panel - Action */, 274 mPanel.getMetricsCategory(), 275 callingPackageName, 276 0 /* value */); 277 } 278 enablePanelHeader(IconCompat icon, CharSequence title, CharSequence subtitle)279 private void enablePanelHeader(IconCompat icon, CharSequence title, CharSequence subtitle) { 280 mTitleView.setVisibility(View.GONE); 281 mPanelHeader.setVisibility(View.VISIBLE); 282 mPanelHeader.setAccessibilityPaneTitle(title); 283 mHeaderTitle.setText(title); 284 mHeaderSubtitle.setText(subtitle); 285 mHeaderSubtitle.setAccessibilityPaneTitle(subtitle); 286 if (icon != null) { 287 mTitleGroup.setVisibility(View.VISIBLE); 288 mHeaderLayout.setGravity(Gravity.LEFT); 289 mTitleIcon.setImageIcon(icon.toIcon(getContext())); 290 if (mPanel.getHeaderIconIntent() != null) { 291 mTitleIcon.setOnClickListener(getHeaderIconListener()); 292 mTitleIcon.setLayoutParams(new LinearLayout.LayoutParams( 293 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 294 } else { 295 final int size = getResources().getDimensionPixelSize( 296 R.dimen.output_switcher_panel_icon_size); 297 mTitleIcon.setLayoutParams(new LinearLayout.LayoutParams(size, size)); 298 } 299 } else { 300 mTitleGroup.setVisibility(View.GONE); 301 mHeaderLayout.setGravity(Gravity.CENTER_HORIZONTAL); 302 } 303 } 304 enableTitle(CharSequence title)305 private void enableTitle(CharSequence title) { 306 mPanelHeader.setVisibility(View.GONE); 307 mTitleView.setVisibility(View.VISIBLE); 308 mTitleView.setAccessibilityPaneTitle(title); 309 mTitleView.setText(title); 310 } 311 enableCustomizedButton()312 private void enableCustomizedButton() { 313 final CharSequence customTitle = mPanel.getCustomizedButtonTitle(); 314 if (TextUtils.isEmpty(customTitle)) { 315 mSeeMoreButton.setVisibility(View.GONE); 316 } else { 317 mSeeMoreButton.setVisibility(View.VISIBLE); 318 mSeeMoreButton.setText(customTitle); 319 } 320 } 321 updateProgressBar()322 private void updateProgressBar() { 323 if (mPanel.isProgressBarVisible()) { 324 mProgressBar.setVisibility(View.VISIBLE); 325 } else { 326 mProgressBar.setVisibility(View.GONE); 327 } 328 } 329 loadAllSlices()330 private void loadAllSlices() { 331 mSliceLiveData.clear(); 332 final List<Uri> sliceUris = mPanel.getSlices(); 333 mPanelSlicesLoaderCountdownLatch = new PanelSlicesLoaderCountdownLatch(sliceUris.size()); 334 335 for (Uri uri : sliceUris) { 336 final LiveData<Slice> sliceLiveData = SliceLiveData.fromUri(getActivity(), uri, 337 (int type, Throwable source)-> { 338 removeSliceLiveData(uri); 339 mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); 340 }); 341 342 // Add slice first to make it in order. Will remove it later if there's an error. 343 mSliceLiveData.put(uri, sliceLiveData); 344 345 sliceLiveData.observe(getViewLifecycleOwner(), slice -> { 346 347 // If the Slice has already loaded, refresh list with slice data. 348 if (mPanelSlicesLoaderCountdownLatch.isSliceLoaded(uri)) { 349 if (mAdapter != null) { 350 int itemIndex = (new ArrayList<>(mSliceLiveData.keySet())).indexOf(uri); 351 mAdapter.notifyItemChanged(itemIndex); 352 } 353 return; 354 } 355 356 /** 357 * Watching for the {@link Slice} to load. 358 * <p> 359 * If the Slice comes back {@code null} or with the Error attribute, if slice 360 * uri is not in the allowlist, remove the Slice data from the list, otherwise 361 * keep the Slice data. 362 * <p> 363 * If the Slice has come back fully loaded, then mark the Slice as loaded. No 364 * other actions required since we already have the Slice data in the list. 365 * <p> 366 * If the Slice does not match the above condition, we will still want to mark 367 * it as loaded after 250ms timeout to avoid delay showing up the panel for 368 * too long. Since we are still having the Slice data in the list, the Slice 369 * will show up later once it is loaded. 370 */ 371 final SliceMetadata metadata = SliceMetadata.from(getActivity(), slice); 372 if (slice == null || metadata.isErrorSlice()) { 373 removeSliceLiveData(uri); 374 mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); 375 } else if (metadata.getLoadingState() == SliceMetadata.LOADED_ALL) { 376 mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); 377 } else { 378 Handler handler = new Handler(); 379 handler.postDelayed(() -> { 380 mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); 381 loadPanelWhenReady(); 382 }, DURATION_SLICE_BINDING_TIMEOUT_MS); 383 } 384 385 loadPanelWhenReady(); 386 }); 387 } 388 } 389 removeSliceLiveData(Uri uri)390 private void removeSliceLiveData(Uri uri) { 391 final List<String> allowList = Arrays.asList( 392 getResources().getStringArray( 393 R.array.config_panel_keep_observe_uri)); 394 if (!allowList.contains(uri.toString())) { 395 mSliceLiveData.remove(uri); 396 } 397 } 398 399 /** 400 * When all of the Slices have loaded for the first time, then we can setup the 401 * {@link RecyclerView}. 402 * <p> 403 * When the Recyclerview has been laid out, we can begin the animation with the 404 * {@link mOnGlobalLayoutListener}, which calls {@link #animateIn()}. 405 */ loadPanelWhenReady()406 private void loadPanelWhenReady() { 407 if (mPanelSlicesLoaderCountdownLatch.isPanelReadyToLoad()) { 408 mAdapter = new PanelSlicesAdapter( 409 this, mSliceLiveData, mPanel.getMetricsCategory()); 410 mPanelSlices.setAdapter(mAdapter); 411 mPanelSlices.getViewTreeObserver() 412 .addOnGlobalLayoutListener(mOnGlobalLayoutListener); 413 mPanelSlices.setVisibility(View.VISIBLE); 414 415 final FragmentActivity activity = getActivity(); 416 if (activity == null) { 417 return; 418 } 419 final DividerItemDecoration itemDecoration = new DividerItemDecoration(activity); 420 itemDecoration 421 .setDividerCondition(DividerItemDecoration.DIVIDER_CONDITION_BOTH); 422 if (mPanelSlices.getItemDecorationCount() == 0) { 423 mPanelSlices.addItemDecoration(itemDecoration); 424 } 425 } 426 } 427 428 /** 429 * Animate a Panel onto the screen. 430 * <p> 431 * Takes the entire panel and animates in from behind the navigation bar. 432 * <p> 433 * Relies on the Panel being having a fixed height to begin the animation. 434 */ animateIn()435 private void animateIn() { 436 final View panelContent = mLayoutView.findViewById(R.id.panel_container); 437 final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView, 438 panelContent.getHeight() /* startY */, 0.0f /* endY */, 439 0.0f /* startAlpha */, 1.0f /* endAlpha */, 440 DURATION_ANIMATE_PANEL_EXPAND_MS); 441 final ValueAnimator animator = new ValueAnimator(); 442 animator.setFloatValues(0.0f, 1.0f); 443 animatorSet.play(animator); 444 animatorSet.start(); 445 // Remove the predraw listeners on the Panel. 446 mLayoutView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener); 447 } 448 449 /** 450 * Build an {@link AnimatorSet} to animate the Panel, {@param parentView} in or out of the 451 * screen, based on the positional parameters {@param startY}, {@param endY}, the parameters 452 * for alpha changes {@param startAlpha}, {@param endAlpha}, and the {@param duration} in 453 * milliseconds. 454 */ 455 @NonNull buildAnimatorSet(@onNull View parentView, float startY, float endY, float startAlpha, float endAlpha, int duration)456 private static AnimatorSet buildAnimatorSet(@NonNull View parentView, float startY, float endY, 457 float startAlpha, float endAlpha, int duration) { 458 final View sheet = parentView.findViewById(R.id.panel_container); 459 final AnimatorSet animatorSet = new AnimatorSet(); 460 animatorSet.setDuration(duration); 461 animatorSet.setInterpolator(new DecelerateInterpolator()); 462 animatorSet.playTogether( 463 ObjectAnimator.ofFloat(sheet, View.TRANSLATION_Y, startY, endY), 464 ObjectAnimator.ofFloat(sheet, View.ALPHA, startAlpha, endAlpha)); 465 return animatorSet; 466 } 467 468 @Override onDestroyView()469 public void onDestroyView() { 470 super.onDestroyView(); 471 472 if (TextUtils.isEmpty(mPanelClosedKey)) { 473 mPanelClosedKey = PanelClosedKeys.KEY_OTHERS; 474 } 475 476 if (mLayoutView != null) { 477 mLayoutView.getViewTreeObserver().removeOnGlobalLayoutListener(mPanelLayoutListener); 478 } 479 if (mPanel != null) { 480 mMetricsProvider.action( 481 0 /* attribution */, 482 SettingsEnums.PAGE_HIDE, 483 mPanel.getMetricsCategory(), 484 mPanelClosedKey, 485 0 /* value */); 486 } 487 } 488 489 @VisibleForTesting getSeeMoreListener()490 View.OnClickListener getSeeMoreListener() { 491 return (v) -> { 492 mPanelClosedKey = PanelClosedKeys.KEY_SEE_MORE; 493 final FragmentActivity activity = getActivity(); 494 if (mPanel.isCustomizedButtonUsed()) { 495 mPanel.onClickCustomizedButton(activity); 496 } else { 497 activity.startActivityForResult(mPanel.getSeeMoreIntent(), 0); 498 activity.finish(); 499 } 500 }; 501 } 502 503 @VisibleForTesting getCloseListener()504 View.OnClickListener getCloseListener() { 505 return (v) -> { 506 mPanelClosedKey = PanelClosedKeys.KEY_DONE; 507 getActivity().finish(); 508 }; 509 } 510 511 @VisibleForTesting 512 View.OnClickListener getHeaderIconListener() { 513 return (v) -> { 514 final FragmentActivity activity = getActivity(); 515 activity.startActivity(mPanel.getHeaderIconIntent()); 516 }; 517 } 518 519 int getPanelViewType() { 520 return mPanel.getViewType(); 521 } 522 523 @Deprecated(forRemoval = true) 524 class LocalPanelCallback implements PanelContentCallback { 525 526 @Override 527 public void onCustomizedButtonStateChanged() { 528 ThreadUtils.postOnMainThread(() -> { 529 enableCustomizedButton(); 530 }); 531 } 532 533 @Override 534 public void onHeaderChanged() { 535 ThreadUtils.postOnMainThread(() -> { 536 enablePanelHeader(mPanel.getIcon(), mPanel.getTitle(), mPanel.getSubTitle()); 537 }); 538 } 539 540 @Override 541 public void forceClose() { 542 mPanelClosedKey = PanelClosedKeys.KEY_OTHERS; 543 getFragmentActivity().finish(); 544 } 545 546 @Override 547 public void onTitleChanged() { 548 ThreadUtils.postOnMainThread(() -> { 549 enableTitle(mPanel.getTitle()); 550 }); 551 } 552 553 @Override 554 public void onProgressBarVisibleChanged() { 555 ThreadUtils.postOnMainThread(() -> { 556 updateProgressBar(); 557 }); 558 } 559 560 @VisibleForTesting 561 FragmentActivity getFragmentActivity() { 562 return getActivity(); 563 } 564 } 565 } 566