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