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