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