• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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