• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.tv.settings;
18 
19 import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
20 import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
21 import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
22 import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
23 import static androidx.lifecycle.Lifecycle.Event.ON_START;
24 import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
25 
26 import static com.android.tv.settings.util.InstrumentationUtils.logPageFocused;
27 
28 import android.animation.AnimatorInflater;
29 import android.annotation.CallSuper;
30 import android.app.tvsettings.TvSettingsEnums;
31 import android.content.Context;
32 import android.graphics.Rect;
33 import android.os.Bundle;
34 import android.view.Gravity;
35 import android.view.KeyEvent;
36 import android.view.LayoutInflater;
37 import android.view.Menu;
38 import android.view.MenuInflater;
39 import android.view.MenuItem;
40 import android.view.MotionEvent;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.view.accessibility.AccessibilityEvent;
44 import android.widget.TextView;
45 
46 import androidx.annotation.NonNull;
47 import androidx.annotation.Nullable;
48 import androidx.leanback.preference.LeanbackPreferenceFragmentCompat;
49 import androidx.lifecycle.LifecycleOwner;
50 import androidx.lifecycle.ViewModelProvider;
51 import androidx.preference.Preference;
52 import androidx.preference.PreferenceGroup;
53 import androidx.preference.PreferenceGroupAdapter;
54 import androidx.preference.PreferenceScreen;
55 import androidx.preference.PreferenceViewHolder;
56 import androidx.recyclerview.widget.RecyclerView;
57 
58 import com.android.settingslib.core.lifecycle.Lifecycle;
59 import com.android.tv.settings.library.instrumentation.InstrumentedPreferenceFragment;
60 import com.android.tv.settings.overlay.FlavorUtils;
61 import com.android.tv.settings.util.SettingsPreferenceUtil;
62 import com.android.tv.settings.widget.SettingsViewModel;
63 import com.android.tv.settings.widget.TsPreference;
64 import com.android.tv.twopanelsettings.TwoPanelSettingsFragment;
65 
66 import java.util.Collections;
67 
68 /**
69  * A {@link LeanbackPreferenceFragmentCompat} that has hooks to observe fragment lifecycle events
70  * and allow for instrumentation.
71  */
72 public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceFragment
73         implements LifecycleOwner,
74         TwoPanelSettingsFragment.PreviewableComponentCallback {
75     private static final int PROGRESS_BAR_DELAY_MS=500;
76     private final Lifecycle mLifecycle = new Lifecycle(this);
77 
78     // Rename getLifecycle() to getSettingsLifecycle() as androidx Fragment has already implemented
79     // getLifecycle(), overriding here would cause unexpected crash in framework.
80     @NonNull
getSettingsLifecycle()81     public Lifecycle getSettingsLifecycle() {
82         return mLifecycle;
83     }
84 
SettingsPreferenceFragment()85     public SettingsPreferenceFragment() {
86     }
87 
88     @CallSuper
89     @Override
onAttach(Context context)90     public void onAttach(Context context) {
91         super.onAttach(context);
92         mLifecycle.onAttach(context);
93     }
94 
95     @CallSuper
96     @Override
onCreate(Bundle savedInstanceState)97     public void onCreate(Bundle savedInstanceState) {
98         mLifecycle.onCreate(savedInstanceState);
99         mLifecycle.handleLifecycleEvent(ON_CREATE);
100         super.onCreate(savedInstanceState);
101         if (getCallbackFragment() != null
102                 && !(getCallbackFragment() instanceof TwoPanelSettingsFragment)) {
103             logPageFocused(getPageId(), true);
104         }
105     }
106 
107     @NonNull
108     @Override
onCreateView(@onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)109     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
110             Bundle savedInstanceState) {
111         View view = super.onCreateView(inflater, container, savedInstanceState);
112         LayoutInflater themedInflater = LayoutInflater.from(view.getContext());
113         ViewGroup newContainer = (ViewGroup)  themedInflater.inflate(
114                 R.layout.tv_settings_progress_bar, container, false);
115         newContainer.addView(view);
116         return newContainer;
117     }
118 
showProgressBar(boolean toShow)119     public void showProgressBar(boolean toShow) {
120         if (getView() == null) {
121             return;
122         }
123 
124         View progressBar = requireView().requireViewById(R.id.tv_settings_progress_bar);
125         if (toShow) {
126             progressBar.bringToFront();
127             progressBar.setAlpha(0f);
128             progressBar.setVisibility(View.VISIBLE);
129             progressBar.animate().alpha(1f).setStartDelay(PROGRESS_BAR_DELAY_MS)
130                     .setDuration(getResources().getInteger(android.R.integer.config_shortAnimTime));
131         } else {
132             progressBar.setVisibility(View.GONE);
133         }
134     }
135 
136     // While the default of relying on text language to determine gravity works well in general,
137     // some page titles (e.g., SSID as Wifi details page title) are dynamic and can be in different
138     // languages. This can cause some complex gravity issues. For example, Wifi details page in RTL
139     // showing an English SSID title would by default align the title to the left, which is
140     // incorrectly considered as START in RTL.
141     // We explicitly set the title gravity to RIGHT in RTL cases to remedy this issue.
142     @Override
onViewCreated(View view, Bundle savedInstanceState)143     public void onViewCreated(View view, Bundle savedInstanceState) {
144         super.onViewCreated(view, savedInstanceState);
145         if (view != null) {
146             TextView titleView = view.findViewById(R.id.decor_title);
147             // We rely on getResources().getConfiguration().getLayoutDirection() instead of
148             // view.isLayoutRtl() as the latter could return false in some complex scenarios even if
149             // it is RTL.
150             if (titleView != null
151                     && getResources().getConfiguration().getLayoutDirection()
152                     == View.LAYOUT_DIRECTION_RTL) {
153                 titleView.setGravity(Gravity.RIGHT);
154             }
155             if (FlavorUtils.isTwoPanel(getContext())) {
156                 ViewGroup decor = view.findViewById(R.id.decor_title_container);
157                 if (decor != null) {
158                     decor.setOutlineProvider(null);
159                 }
160                 // Title is set in LeanbackPreferenceCompat parent class
161                 // Subtitle is set here because base leanbackpreference doesn't have a summary.
162                 CharSequence summary = getPreferenceScreen().getSummary();
163                 final TextView decorSummary =
164                     view == null ? null : (TextView) view.findViewById(R.id.DecorSubtitleId);
165                 if (summary != null && decorSummary != null) {
166                       decorSummary.setText(summary);
167                       decorSummary.setVisibility(View.VISIBLE);
168                 }
169             } else {
170                 // We only want to set the title in this location for one-panel settings.
171                 // TwoPanelSettings behavior is handled moveToPanel in TwoPanelSettingsFragment
172                 // since we only want the active/main panel to announce its title.
173                 // For some reason, setAccessibiltyPaneTitle interferes with the initial a11y focus
174                 // of this screen.
175                 if (getActivity().getWindow() != null) {
176                     getActivity().getWindow().setTitle(getPreferenceScreen().getTitle());
177                     view.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
178                 }
179 
180                 // Only the one-panel settings should be set as unrestricted keep-clear areas
181                 // because they are a side panel, so the PiP can be moved next to it.
182                 view.addOnLayoutChangeListener((v, l, t, r, b, oldL, oldT, oldR, oldB) -> {
183                     view.setUnrestrictedPreferKeepClearRects(
184                             Collections.singletonList(new Rect(0, 0, r - l, b - t)));
185                 });
186 
187             }
188             // For Two Panel Settings determine clipping per-view
189             if (!FlavorUtils.isTwoPanel(getContext())) {
190                 removeAnimationClipping(view);
191             }
192         }
193         SettingsViewModel settingsViewModel = new ViewModelProvider(this.getActivity(),
194                 ViewModelProvider.AndroidViewModelFactory.getInstance(
195                         this.getActivity().getApplication())).get(SettingsViewModel.class);
196         iteratePreferenceAndSetObserver(settingsViewModel, getPreferenceScreen());
197     }
198 
iteratePreferenceAndSetObserver(SettingsViewModel viewModel, PreferenceGroup preferenceGroup)199     private void iteratePreferenceAndSetObserver(SettingsViewModel viewModel,
200             PreferenceGroup preferenceGroup) {
201         for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
202             Preference pref = preferenceGroup.getPreference(i);
203             if (pref instanceof TsPreference
204                     && ((TsPreference) pref).updatableFromGoogleSettings()) {
205                 viewModel.getVisibilityLiveData(
206                         SettingsPreferenceUtil.getCompoundKey(this, pref))
207                         .observe(getViewLifecycleOwner(), (Boolean b) -> pref.setVisible(b));
208             }
209             if (pref instanceof PreferenceGroup) {
210                 iteratePreferenceAndSetObserver(viewModel, (PreferenceGroup) pref);
211             }
212         }
213     }
214 
removeAnimationClipping(View v)215     protected void removeAnimationClipping(View v) {
216         if (v instanceof ViewGroup) {
217             ((ViewGroup) v).setClipChildren(false);
218             ((ViewGroup) v).setClipToPadding(false);
219             for (int index = 0; index < ((ViewGroup) v).getChildCount(); index++) {
220                 View child = ((ViewGroup) v).getChildAt(index);
221                 removeAnimationClipping(child);
222             }
223         }
224     }
225 
226     @Override
onCreateAdapter(PreferenceScreen preferenceScreen)227     protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
228         return new PreferenceGroupAdapter(preferenceScreen) {
229             @Override
230             @NonNull
231             public PreferenceViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
232                     int viewType) {
233                 PreferenceViewHolder vh = super.onCreateViewHolder(parent, viewType);
234                 if (FlavorUtils.isTwoPanel(getContext())) {
235                     vh.itemView.setStateListAnimator(AnimatorInflater.loadStateListAnimator(
236                             getContext(), R.animator.preference));
237                 }
238                 vh.itemView.setOnTouchListener((v, e) -> {
239                     if (e.getActionMasked() == MotionEvent.ACTION_DOWN
240                             && isPrimaryKey(e.getButtonState())) {
241                         vh.itemView.requestFocus();
242                         v.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
243                                 KeyEvent.KEYCODE_DPAD_CENTER));
244                         return true;
245                     } else if (e.getActionMasked() == MotionEvent.ACTION_UP
246                             && isPrimaryKey(e.getButtonState())) {
247                         v.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
248                                 KeyEvent.KEYCODE_DPAD_CENTER));
249                         return true;
250                     }
251                     return false;
252                 });
253                 vh.itemView.setFocusable(true);
254                 vh.itemView.setFocusableInTouchMode(true);
255                 return vh;
256             }
257         };
258     }
259 
260     @Override
261     public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
262         mLifecycle.setPreferenceScreen(preferenceScreen);
263         super.setPreferenceScreen(preferenceScreen);
264     }
265 
266     @CallSuper
267     @Override
268     public void onSaveInstanceState(Bundle outState) {
269         super.onSaveInstanceState(outState);
270         mLifecycle.onSaveInstanceState(outState);
271     }
272 
273     @CallSuper
274     @Override
275     public void onStart() {
276         mLifecycle.handleLifecycleEvent(ON_START);
277         super.onStart();
278     }
279 
280     @CallSuper
281     @Override
282     public void onResume() {
283         super.onResume();
284         mLifecycle.handleLifecycleEvent(ON_RESUME);
285         if (getCallbackFragment() instanceof TwoPanelSettingsFragment) {
286             TwoPanelSettingsFragment parentFragment =
287                     (TwoPanelSettingsFragment) getCallbackFragment();
288             parentFragment.addListenerForFragment(this);
289         }
290     }
291 
292     // This should only be invoked if the parent Fragment is TwoPanelSettingsFragment.
293     @CallSuper
294     @Override
295     public void onArriveAtMainPanel(boolean forward) {
296         logPageFocused(getPageId(), forward);
297     }
298 
299     @CallSuper
300     @Override
301     public void onPause() {
302         mLifecycle.handleLifecycleEvent(ON_PAUSE);
303         super.onPause();
304         if (getCallbackFragment() instanceof TwoPanelSettingsFragment) {
305             TwoPanelSettingsFragment parentFragment =
306                     (TwoPanelSettingsFragment) getCallbackFragment();
307             parentFragment.removeListenerForFragment(this);
308         }
309     }
310 
311     @CallSuper
312     @Override
313     public void onStop() {
314         mLifecycle.handleLifecycleEvent(ON_STOP);
315         super.onStop();
316     }
317 
318     @CallSuper
319     @Override
320     public void onDestroy() {
321         mLifecycle.handleLifecycleEvent(ON_DESTROY);
322         super.onDestroy();
323     }
324 
325     @CallSuper
326     @Override
327     public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
328         mLifecycle.onCreateOptionsMenu(menu, inflater);
329         super.onCreateOptionsMenu(menu, inflater);
330     }
331 
332     @CallSuper
333     @Override
334     public void onPrepareOptionsMenu(final Menu menu) {
335         mLifecycle.onPrepareOptionsMenu(menu);
336         super.onPrepareOptionsMenu(menu);
337     }
338 
339     @CallSuper
340     @Override
341     public boolean onOptionsItemSelected(final MenuItem menuItem) {
342         boolean lifecycleHandled = mLifecycle.onOptionsItemSelected(menuItem);
343         if (!lifecycleHandled) {
344             return super.onOptionsItemSelected(menuItem);
345         }
346         return lifecycleHandled;
347     }
348 
349     /** Subclasses should override this to use their own PageId for statsd logging. */
350     protected int getPageId() {
351         return TvSettingsEnums.PAGE_CLASSIC_DEFAULT;
352     }
353 
354     // check if such motion event should translate to key event DPAD_CENTER
355     private boolean isPrimaryKey(int buttonState) {
356         return buttonState == MotionEvent.BUTTON_PRIMARY
357                 || buttonState == MotionEvent.BUTTON_STYLUS_PRIMARY
358                 || buttonState == 0;  // motion events which creates by UI Automator
359     }
360 }
361