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.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.ViewTreeObserver; 33 import android.view.animation.DecelerateInterpolator; 34 import android.widget.Button; 35 import android.widget.TextView; 36 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 import androidx.fragment.app.Fragment; 40 import androidx.fragment.app.FragmentActivity; 41 import androidx.lifecycle.LiveData; 42 import androidx.slice.Slice; 43 import androidx.recyclerview.widget.LinearLayoutManager; 44 import androidx.recyclerview.widget.RecyclerView; 45 import androidx.slice.SliceMetadata; 46 import androidx.slice.widget.SliceLiveData; 47 48 import com.android.internal.annotations.VisibleForTesting; 49 import com.android.settings.R; 50 import com.android.settings.overlay.FeatureFactory; 51 import com.android.settings.panel.PanelLoggingContract.PanelClosedKeys; 52 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 53 import com.google.android.setupdesign.DividerItemDecoration; 54 55 import java.util.ArrayList; 56 import java.util.List; 57 58 public class PanelFragment extends Fragment { 59 60 private static final String TAG = "PanelFragment"; 61 62 /** 63 * Duration of the animation entering the screen, in milliseconds. 64 */ 65 private static final int DURATION_ANIMATE_PANEL_EXPAND_MS = 250; 66 67 /** 68 * Duration of the animation exiting the screen, in milliseconds. 69 */ 70 private static final int DURATION_ANIMATE_PANEL_COLLAPSE_MS = 200; 71 72 /** 73 * Duration of timeout waiting for Slice data to bind, in milliseconds. 74 */ 75 private static final int DURATION_SLICE_BINDING_TIMEOUT_MS = 250; 76 77 private View mLayoutView; 78 private TextView mTitleView; 79 private Button mSeeMoreButton; 80 private Button mDoneButton; 81 private RecyclerView mPanelSlices; 82 83 private PanelContent mPanel; 84 private MetricsFeatureProvider mMetricsProvider; 85 private String mPanelClosedKey; 86 87 private final List<LiveData<Slice>> mSliceLiveData = new ArrayList<>(); 88 89 @VisibleForTesting 90 PanelSlicesLoaderCountdownLatch mPanelSlicesLoaderCountdownLatch; 91 92 private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener = () -> { 93 return false; 94 }; 95 96 private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = 97 new ViewTreeObserver.OnGlobalLayoutListener() { 98 @Override 99 public void onGlobalLayout() { 100 animateIn(); 101 if (mPanelSlices != null) { 102 mPanelSlices.getViewTreeObserver().removeOnGlobalLayoutListener(this); 103 } 104 } 105 }; 106 107 private PanelSlicesAdapter mAdapter; 108 109 @Nullable 110 @Override onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)111 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 112 @Nullable Bundle savedInstanceState) { 113 mLayoutView = inflater.inflate(R.layout.panel_layout, container, false); 114 createPanelContent(); 115 return mLayoutView; 116 } 117 118 /** 119 * Animate the old panel out from the screen, then update the panel with new content once the 120 * animation is done. 121 * <p> 122 * Takes the entire panel and animates out from behind the navigation bar. 123 * <p> 124 * Call createPanelContent() once animation end. 125 */ updatePanelWithAnimation()126 void updatePanelWithAnimation() { 127 final View panelContent = mLayoutView.findViewById(R.id.panel_container); 128 final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView, 129 0.0f /* startY */, panelContent.getHeight() /* endY */, 130 1.0f /* startAlpha */, 0.0f /* endAlpha */, 131 DURATION_ANIMATE_PANEL_COLLAPSE_MS); 132 133 final ValueAnimator animator = new ValueAnimator(); 134 animator.setFloatValues(0.0f, 1.0f); 135 animatorSet.play(animator); 136 animatorSet.addListener(new AnimatorListenerAdapter() { 137 @Override 138 public void onAnimationEnd(Animator animation) { 139 createPanelContent(); 140 } 141 }); 142 animatorSet.start(); 143 } 144 createPanelContent()145 private void createPanelContent() { 146 final FragmentActivity activity = getActivity(); 147 if (activity == null) { 148 return; 149 } 150 151 if (mLayoutView == null) { 152 activity.finish(); 153 return; 154 } 155 156 mPanelSlices = mLayoutView.findViewById(R.id.panel_parent_layout); 157 mSeeMoreButton = mLayoutView.findViewById(R.id.see_more); 158 mDoneButton = mLayoutView.findViewById(R.id.done); 159 mTitleView = mLayoutView.findViewById(R.id.panel_title); 160 161 // Make the panel layout gone here, to avoid janky animation when updating from old panel. 162 // We will make it visible once the panel is ready to load. 163 mPanelSlices.setVisibility(View.GONE); 164 165 final Bundle arguments = getArguments(); 166 final String panelType = 167 arguments.getString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT); 168 final String callingPackageName = 169 arguments.getString(SettingsPanelActivity.KEY_CALLING_PACKAGE_NAME); 170 final String mediaPackageName = 171 arguments.getString(SettingsPanelActivity.KEY_MEDIA_PACKAGE_NAME); 172 173 // TODO (b/124399577) transform interface to take a context and bundle. 174 mPanel = FeatureFactory.getFactory(activity) 175 .getPanelFeatureProvider() 176 .getPanel(activity, panelType, mediaPackageName); 177 178 if (mPanel == null) { 179 activity.finish(); 180 return; 181 } 182 183 mMetricsProvider = FeatureFactory.getFactory(activity).getMetricsFeatureProvider(); 184 185 mPanelSlices.setLayoutManager(new LinearLayoutManager((activity))); 186 187 // Add predraw listener to remove the animation and while we wait for Slices to load. 188 mLayoutView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener); 189 190 // Start loading Slices. When finished, the Panel will animate in. 191 loadAllSlices(); 192 193 mTitleView.setText(mPanel.getTitle()); 194 mSeeMoreButton.setOnClickListener(getSeeMoreListener()); 195 mDoneButton.setOnClickListener(getCloseListener()); 196 197 // If getSeeMoreIntent() is null, hide the mSeeMoreButton. 198 if (mPanel.getSeeMoreIntent() == null) { 199 mSeeMoreButton.setVisibility(View.GONE); 200 } 201 202 // Log panel opened. 203 mMetricsProvider.action( 204 0 /* attribution */, 205 SettingsEnums.PAGE_VISIBLE /* opened panel - Action */, 206 mPanel.getMetricsCategory(), 207 callingPackageName, 208 0 /* value */); 209 } 210 loadAllSlices()211 private void loadAllSlices() { 212 mSliceLiveData.clear(); 213 final List<Uri> sliceUris = mPanel.getSlices(); 214 mPanelSlicesLoaderCountdownLatch = new PanelSlicesLoaderCountdownLatch(sliceUris.size()); 215 216 for (Uri uri : sliceUris) { 217 final LiveData<Slice> sliceLiveData = SliceLiveData.fromUri(getActivity(), uri); 218 219 // Add slice first to make it in order. Will remove it later if there's an error. 220 mSliceLiveData.add(sliceLiveData); 221 222 sliceLiveData.observe(getViewLifecycleOwner(), slice -> { 223 // If the Slice has already loaded, do nothing. 224 if (mPanelSlicesLoaderCountdownLatch.isSliceLoaded(uri)) { 225 return; 226 } 227 228 /** 229 * Watching for the {@link Slice} to load. 230 * <p> 231 * If the Slice comes back {@code null} or with the Error attribute, remove the 232 * Slice data from the list, and mark the Slice as loaded. 233 * <p> 234 * If the Slice has come back fully loaded, then mark the Slice as loaded. No 235 * other actions required since we already have the Slice data in the list. 236 * <p> 237 * If the Slice does not match the above condition, we will still want to mark 238 * it as loaded after 250ms timeout to avoid delay showing up the panel for 239 * too long. Since we are still having the Slice data in the list, the Slice 240 * will show up later once it is loaded. 241 */ 242 final SliceMetadata metadata = SliceMetadata.from(getActivity(), slice); 243 if (slice == null || metadata.isErrorSlice()) { 244 mSliceLiveData.remove(sliceLiveData); 245 mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); 246 } else if (metadata.getLoadingState() == SliceMetadata.LOADED_ALL) { 247 mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); 248 } else { 249 Handler handler = new Handler(); 250 handler.postDelayed(() -> { 251 mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); 252 loadPanelWhenReady(); 253 }, DURATION_SLICE_BINDING_TIMEOUT_MS); 254 } 255 256 loadPanelWhenReady(); 257 }); 258 } 259 } 260 261 /** 262 * When all of the Slices have loaded for the first time, then we can setup the 263 * {@link RecyclerView}. 264 * <p> 265 * When the Recyclerview has been laid out, we can begin the animation with the 266 * {@link mOnGlobalLayoutListener}, which calls {@link #animateIn()}. 267 */ loadPanelWhenReady()268 private void loadPanelWhenReady() { 269 if (mPanelSlicesLoaderCountdownLatch.isPanelReadyToLoad()) { 270 mAdapter = new PanelSlicesAdapter( 271 this, mSliceLiveData, mPanel.getMetricsCategory()); 272 mPanelSlices.setAdapter(mAdapter); 273 mPanelSlices.getViewTreeObserver() 274 .addOnGlobalLayoutListener(mOnGlobalLayoutListener); 275 mPanelSlices.setVisibility(View.VISIBLE); 276 277 DividerItemDecoration itemDecoration = new DividerItemDecoration(getActivity()); 278 itemDecoration 279 .setDividerCondition(DividerItemDecoration.DIVIDER_CONDITION_BOTH); 280 mPanelSlices.addItemDecoration(itemDecoration); 281 } 282 } 283 284 /** 285 * Animate a Panel onto the screen. 286 * <p> 287 * Takes the entire panel and animates in from behind the navigation bar. 288 * <p> 289 * Relies on the Panel being having a fixed height to begin the animation. 290 */ animateIn()291 private void animateIn() { 292 final View panelContent = mLayoutView.findViewById(R.id.panel_container); 293 final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView, 294 panelContent.getHeight() /* startY */, 0.0f /* endY */, 295 0.0f /* startAlpha */, 1.0f /* endAlpha */, 296 DURATION_ANIMATE_PANEL_EXPAND_MS); 297 final ValueAnimator animator = new ValueAnimator(); 298 animator.setFloatValues(0.0f, 1.0f); 299 animatorSet.play(animator); 300 animatorSet.start(); 301 // Remove the predraw listeners on the Panel. 302 mLayoutView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener); 303 } 304 305 /** 306 * Build an {@link AnimatorSet} to animate the Panel, {@param parentView} in or out of the 307 * screen, based on the positional parameters {@param startY}, {@param endY}, the parameters 308 * for alpha changes {@param startAlpha}, {@param endAlpha}, and the {@param duration} in 309 * milliseconds. 310 */ 311 @NonNull buildAnimatorSet(@onNull View parentView, float startY, float endY, float startAlpha, float endAlpha, int duration)312 private static AnimatorSet buildAnimatorSet(@NonNull View parentView, float startY, float endY, 313 float startAlpha, float endAlpha, int duration) { 314 final View sheet = parentView.findViewById(R.id.panel_container); 315 final AnimatorSet animatorSet = new AnimatorSet(); 316 animatorSet.setDuration(duration); 317 animatorSet.setInterpolator(new DecelerateInterpolator()); 318 animatorSet.playTogether( 319 ObjectAnimator.ofFloat(sheet, View.TRANSLATION_Y, startY, endY), 320 ObjectAnimator.ofFloat(sheet, View.ALPHA, startAlpha,endAlpha)); 321 return animatorSet; 322 } 323 324 @Override onDestroyView()325 public void onDestroyView() { 326 super.onDestroyView(); 327 328 if (TextUtils.isEmpty(mPanelClosedKey)) { 329 mPanelClosedKey = PanelClosedKeys.KEY_OTHERS; 330 } 331 332 mMetricsProvider.action( 333 0 /* attribution */, 334 SettingsEnums.PAGE_HIDE, 335 mPanel.getMetricsCategory(), 336 mPanelClosedKey, 337 0 /* value */); 338 } 339 340 @VisibleForTesting getSeeMoreListener()341 View.OnClickListener getSeeMoreListener() { 342 return (v) -> { 343 mPanelClosedKey = PanelClosedKeys.KEY_SEE_MORE; 344 final FragmentActivity activity = getActivity(); 345 activity.startActivityForResult(mPanel.getSeeMoreIntent(), 0); 346 activity.finish(); 347 }; 348 } 349 350 @VisibleForTesting getCloseListener()351 View.OnClickListener getCloseListener() { 352 return (v) -> { 353 mPanelClosedKey = PanelClosedKeys.KEY_DONE; 354 getActivity().finish(); 355 }; 356 } 357 } 358