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 (mLayoutView == null) { 148 activity.finish(); 149 } 150 151 mPanelSlices = mLayoutView.findViewById(R.id.panel_parent_layout); 152 mSeeMoreButton = mLayoutView.findViewById(R.id.see_more); 153 mDoneButton = mLayoutView.findViewById(R.id.done); 154 mTitleView = mLayoutView.findViewById(R.id.panel_title); 155 156 // Make the panel layout gone here, to avoid janky animation when updating from old panel. 157 // We will make it visible once the panel is ready to load. 158 mPanelSlices.setVisibility(View.GONE); 159 160 final Bundle arguments = getArguments(); 161 final String panelType = 162 arguments.getString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT); 163 final String callingPackageName = 164 arguments.getString(SettingsPanelActivity.KEY_CALLING_PACKAGE_NAME); 165 final String mediaPackageName = 166 arguments.getString(SettingsPanelActivity.KEY_MEDIA_PACKAGE_NAME); 167 168 // TODO (b/124399577) transform interface to take a context and bundle. 169 mPanel = FeatureFactory.getFactory(activity) 170 .getPanelFeatureProvider() 171 .getPanel(activity, panelType, mediaPackageName); 172 173 if (mPanel == null) { 174 activity.finish(); 175 } 176 177 mMetricsProvider = FeatureFactory.getFactory(activity).getMetricsFeatureProvider(); 178 179 mPanelSlices.setLayoutManager(new LinearLayoutManager((activity))); 180 181 // Add predraw listener to remove the animation and while we wait for Slices to load. 182 mLayoutView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener); 183 184 // Start loading Slices. When finished, the Panel will animate in. 185 loadAllSlices(); 186 187 mTitleView.setText(mPanel.getTitle()); 188 mSeeMoreButton.setOnClickListener(getSeeMoreListener()); 189 mDoneButton.setOnClickListener(getCloseListener()); 190 191 // If getSeeMoreIntent() is null, hide the mSeeMoreButton. 192 if (mPanel.getSeeMoreIntent() == null) { 193 mSeeMoreButton.setVisibility(View.GONE); 194 } 195 196 // Log panel opened. 197 mMetricsProvider.action( 198 0 /* attribution */, 199 SettingsEnums.PAGE_VISIBLE /* opened panel - Action */, 200 mPanel.getMetricsCategory(), 201 callingPackageName, 202 0 /* value */); 203 } 204 loadAllSlices()205 private void loadAllSlices() { 206 mSliceLiveData.clear(); 207 final List<Uri> sliceUris = mPanel.getSlices(); 208 mPanelSlicesLoaderCountdownLatch = new PanelSlicesLoaderCountdownLatch(sliceUris.size()); 209 210 for (Uri uri : sliceUris) { 211 final LiveData<Slice> sliceLiveData = SliceLiveData.fromUri(getActivity(), uri); 212 213 // Add slice first to make it in order. Will remove it later if there's an error. 214 mSliceLiveData.add(sliceLiveData); 215 216 sliceLiveData.observe(getViewLifecycleOwner(), slice -> { 217 // If the Slice has already loaded, do nothing. 218 if (mPanelSlicesLoaderCountdownLatch.isSliceLoaded(uri)) { 219 return; 220 } 221 222 /** 223 * Watching for the {@link Slice} to load. 224 * <p> 225 * If the Slice comes back {@code null} or with the Error attribute, remove the 226 * Slice data from the list, and mark the Slice as loaded. 227 * <p> 228 * If the Slice has come back fully loaded, then mark the Slice as loaded. No 229 * other actions required since we already have the Slice data in the list. 230 * <p> 231 * If the Slice does not match the above condition, we will still want to mark 232 * it as loaded after 250ms timeout to avoid delay showing up the panel for 233 * too long. Since we are still having the Slice data in the list, the Slice 234 * will show up later once it is loaded. 235 */ 236 final SliceMetadata metadata = SliceMetadata.from(getActivity(), slice); 237 if (slice == null || metadata.isErrorSlice()) { 238 mSliceLiveData.remove(sliceLiveData); 239 mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); 240 } else if (metadata.getLoadingState() == SliceMetadata.LOADED_ALL) { 241 mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); 242 } else { 243 Handler handler = new Handler(); 244 handler.postDelayed(() -> { 245 mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); 246 loadPanelWhenReady(); 247 }, DURATION_SLICE_BINDING_TIMEOUT_MS); 248 } 249 250 loadPanelWhenReady(); 251 }); 252 } 253 } 254 255 /** 256 * When all of the Slices have loaded for the first time, then we can setup the 257 * {@link RecyclerView}. 258 * <p> 259 * When the Recyclerview has been laid out, we can begin the animation with the 260 * {@link mOnGlobalLayoutListener}, which calls {@link #animateIn()}. 261 */ loadPanelWhenReady()262 private void loadPanelWhenReady() { 263 if (mPanelSlicesLoaderCountdownLatch.isPanelReadyToLoad()) { 264 mAdapter = new PanelSlicesAdapter( 265 this, mSliceLiveData, mPanel.getMetricsCategory()); 266 mPanelSlices.setAdapter(mAdapter); 267 mPanelSlices.getViewTreeObserver() 268 .addOnGlobalLayoutListener(mOnGlobalLayoutListener); 269 mPanelSlices.setVisibility(View.VISIBLE); 270 271 DividerItemDecoration itemDecoration = new DividerItemDecoration(getActivity()); 272 itemDecoration 273 .setDividerCondition(DividerItemDecoration.DIVIDER_CONDITION_BOTH); 274 mPanelSlices.addItemDecoration(itemDecoration); 275 } 276 } 277 278 /** 279 * Animate a Panel onto the screen. 280 * <p> 281 * Takes the entire panel and animates in from behind the navigation bar. 282 * <p> 283 * Relies on the Panel being having a fixed height to begin the animation. 284 */ animateIn()285 private void animateIn() { 286 final View panelContent = mLayoutView.findViewById(R.id.panel_container); 287 final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView, 288 panelContent.getHeight() /* startY */, 0.0f /* endY */, 289 0.0f /* startAlpha */, 1.0f /* endAlpha */, 290 DURATION_ANIMATE_PANEL_EXPAND_MS); 291 final ValueAnimator animator = new ValueAnimator(); 292 animator.setFloatValues(0.0f, 1.0f); 293 animatorSet.play(animator); 294 animatorSet.start(); 295 // Remove the predraw listeners on the Panel. 296 mLayoutView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener); 297 } 298 299 /** 300 * Build an {@link AnimatorSet} to animate the Panel, {@param parentView} in or out of the 301 * screen, based on the positional parameters {@param startY}, {@param endY}, the parameters 302 * for alpha changes {@param startAlpha}, {@param endAlpha}, and the {@param duration} in 303 * milliseconds. 304 */ 305 @NonNull buildAnimatorSet(@onNull View parentView, float startY, float endY, float startAlpha, float endAlpha, int duration)306 private static AnimatorSet buildAnimatorSet(@NonNull View parentView, float startY, float endY, 307 float startAlpha, float endAlpha, int duration) { 308 final View sheet = parentView.findViewById(R.id.panel_container); 309 final AnimatorSet animatorSet = new AnimatorSet(); 310 animatorSet.setDuration(duration); 311 animatorSet.setInterpolator(new DecelerateInterpolator()); 312 animatorSet.playTogether( 313 ObjectAnimator.ofFloat(sheet, View.TRANSLATION_Y, startY, endY), 314 ObjectAnimator.ofFloat(sheet, View.ALPHA, startAlpha,endAlpha)); 315 return animatorSet; 316 } 317 318 @Override onDestroyView()319 public void onDestroyView() { 320 super.onDestroyView(); 321 322 if (TextUtils.isEmpty(mPanelClosedKey)) { 323 mPanelClosedKey = PanelClosedKeys.KEY_OTHERS; 324 } 325 326 mMetricsProvider.action( 327 0 /* attribution */, 328 SettingsEnums.PAGE_HIDE, 329 mPanel.getMetricsCategory(), 330 mPanelClosedKey, 331 0 /* value */); 332 } 333 334 @VisibleForTesting getSeeMoreListener()335 View.OnClickListener getSeeMoreListener() { 336 return (v) -> { 337 mPanelClosedKey = PanelClosedKeys.KEY_SEE_MORE; 338 final FragmentActivity activity = getActivity(); 339 activity.startActivityForResult(mPanel.getSeeMoreIntent(), 0); 340 activity.finish(); 341 }; 342 } 343 344 @VisibleForTesting getCloseListener()345 View.OnClickListener getCloseListener() { 346 return (v) -> { 347 mPanelClosedKey = PanelClosedKeys.KEY_DONE; 348 getActivity().finish(); 349 }; 350 } 351 } 352