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