• 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.Menu;
37 import android.view.MenuInflater;
38 import android.view.MenuItem;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.accessibility.AccessibilityEvent;
43 import android.widget.TextView;
44 
45 import androidx.annotation.NonNull;
46 import androidx.leanback.preference.LeanbackPreferenceFragmentCompat;
47 import androidx.lifecycle.LifecycleOwner;
48 import androidx.lifecycle.ViewModelProvider;
49 import androidx.preference.Preference;
50 import androidx.preference.PreferenceGroup;
51 import androidx.preference.PreferenceGroupAdapter;
52 import androidx.preference.PreferenceScreen;
53 import androidx.preference.PreferenceViewHolder;
54 import androidx.recyclerview.widget.RecyclerView;
55 
56 import com.android.settingslib.core.lifecycle.Lifecycle;
57 import com.android.tv.settings.library.overlay.FlavorUtils;
58 import com.android.tv.settings.util.SettingsPreferenceUtil;
59 import com.android.tv.settings.widget.SettingsViewModel;
60 import com.android.tv.settings.widget.TsPreference;
61 import com.android.tv.twopanelsettings.TwoPanelSettingsFragment;
62 
63 import java.util.Collections;
64 
65 /**
66  * A {@link LeanbackPreferenceFragmentCompat} that has hooks to observe fragment lifecycle events
67  * and allow for instrumentation.
68  */
69 public abstract class SettingsPreferenceFragment extends LeanbackPreferenceFragmentCompat
70         implements LifecycleOwner,
71         TwoPanelSettingsFragment.PreviewableComponentCallback {
72     private final Lifecycle mLifecycle = new Lifecycle(this);
73 
74     // Rename getLifecycle() to getSettingsLifecycle() as androidx Fragment has already implemented
75     // getLifecycle(), overriding here would cause unexpected crash in framework.
76     @NonNull
getSettingsLifecycle()77     public Lifecycle getSettingsLifecycle() {
78         return mLifecycle;
79     }
80 
SettingsPreferenceFragment()81     public SettingsPreferenceFragment() {
82     }
83 
84     @CallSuper
85     @Override
onAttach(Context context)86     public void onAttach(Context context) {
87         super.onAttach(context);
88         mLifecycle.onAttach(context);
89     }
90 
91     @CallSuper
92     @Override
onCreate(Bundle savedInstanceState)93     public void onCreate(Bundle savedInstanceState) {
94         mLifecycle.onCreate(savedInstanceState);
95         mLifecycle.handleLifecycleEvent(ON_CREATE);
96         super.onCreate(savedInstanceState);
97         if (getCallbackFragment() != null
98                 && !(getCallbackFragment() instanceof TwoPanelSettingsFragment)) {
99             logPageFocused(getPageId(), true);
100         }
101     }
102 
103     // While the default of relying on text language to determine gravity works well in general,
104     // some page titles (e.g., SSID as Wifi details page title) are dynamic and can be in different
105     // languages. This can cause some complex gravity issues. For example, Wifi details page in RTL
106     // showing an English SSID title would by default align the title to the left, which is
107     // incorrectly considered as START in RTL.
108     // We explicitly set the title gravity to RIGHT in RTL cases to remedy this issue.
109     @Override
onViewCreated(View view, Bundle savedInstanceState)110     public void onViewCreated(View view, Bundle savedInstanceState) {
111         super.onViewCreated(view, savedInstanceState);
112         // Set list view listeners after the fragment view is created
113         if (getCallbackFragment() instanceof TwoPanelSettingsFragment) {
114             TwoPanelSettingsFragment parentFragment =
115                     (TwoPanelSettingsFragment) getCallbackFragment();
116             parentFragment.addListenerForFragment(this);
117         }
118         if (view != null) {
119             TextView titleView = view.findViewById(R.id.decor_title);
120             // We rely on getResources().getConfiguration().getLayoutDirection() instead of
121             // view.isLayoutRtl() as the latter could return false in some complex scenarios even if
122             // it is RTL.
123             if (titleView != null
124                     && getResources().getConfiguration().getLayoutDirection()
125                     == View.LAYOUT_DIRECTION_RTL) {
126                 titleView.setGravity(Gravity.RIGHT);
127             }
128             if (FlavorUtils.isTwoPanel(getContext())) {
129                 ViewGroup decor = view.findViewById(R.id.decor_title_container);
130                 if (decor != null) {
131                     decor.setOutlineProvider(null);
132                 }
133             } else {
134                 // We only want to set the title in this location for one-panel settings.
135                 // TwoPanelSettings behavior is handled moveToPanel in TwoPanelSettingsFragment
136                 // since we only want the active/main panel to announce its title.
137                 // For some reason, setAccessibiltyPaneTitle interferes with the initial a11y focus
138                 // of this screen.
139                 if (getActivity().getWindow() != null) {
140                     getActivity().getWindow().setTitle(getPreferenceScreen().getTitle());
141                     view.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
142                 }
143 
144                 // Only the one-panel settings should be set as unrestricted keep-clear areas
145                 // because they are a side panel, so the PiP can be moved next to it.
146                 view.addOnLayoutChangeListener((v, l, t, r, b, oldL, oldT, oldR, oldB) -> {
147                     view.setUnrestrictedPreferKeepClearRects(
148                             Collections.singletonList(new Rect(0, 0, r - l, b - t)));
149                 });
150 
151             }
152             removeAnimationClipping(view);
153         }
154         SettingsViewModel settingsViewModel = new ViewModelProvider(this.getActivity(),
155                 ViewModelProvider.AndroidViewModelFactory.getInstance(
156                         this.getActivity().getApplication())).get(SettingsViewModel.class);
157         iteratePreferenceAndSetObserver(settingsViewModel, getPreferenceScreen());
158     }
159 
iteratePreferenceAndSetObserver(SettingsViewModel viewModel, PreferenceGroup preferenceGroup)160     private void iteratePreferenceAndSetObserver(SettingsViewModel viewModel,
161             PreferenceGroup preferenceGroup) {
162         for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
163             Preference pref = preferenceGroup.getPreference(i);
164             if (pref instanceof TsPreference
165                     && ((TsPreference) pref).updatableFromGoogleSettings()) {
166                 viewModel.getVisibilityLiveData(
167                         SettingsPreferenceUtil.getCompoundKey(this, pref))
168                         .observe(getViewLifecycleOwner(), (Boolean b) -> pref.setVisible(b));
169             }
170             if (pref instanceof PreferenceGroup) {
171                 iteratePreferenceAndSetObserver(viewModel, (PreferenceGroup) pref);
172             }
173         }
174     }
175 
removeAnimationClipping(View v)176     protected void removeAnimationClipping(View v) {
177         if (v instanceof ViewGroup) {
178             ((ViewGroup) v).setClipChildren(false);
179             ((ViewGroup) v).setClipToPadding(false);
180             for (int index = 0; index < ((ViewGroup) v).getChildCount(); index++) {
181                 View child = ((ViewGroup) v).getChildAt(index);
182                 removeAnimationClipping(child);
183             }
184         }
185     }
186 
187     @Override
onCreateAdapter(PreferenceScreen preferenceScreen)188     protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
189         return new PreferenceGroupAdapter(preferenceScreen) {
190             @Override
191             @NonNull
192             public PreferenceViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
193                     int viewType) {
194                 PreferenceViewHolder vh = super.onCreateViewHolder(parent, viewType);
195                 if (FlavorUtils.isTwoPanel(getContext())) {
196                     vh.itemView.setStateListAnimator(AnimatorInflater.loadStateListAnimator(
197                             getContext(), R.animator.preference));
198                 }
199                 vh.itemView.setOnTouchListener((v, e) -> {
200                     if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
201                         vh.itemView.requestFocus();
202                         v.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
203                                 KeyEvent.KEYCODE_DPAD_CENTER));
204                         return true;
205                     } else if (e.getActionMasked() == MotionEvent.ACTION_UP) {
206                         v.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
207                                 KeyEvent.KEYCODE_DPAD_CENTER));
208                         return true;
209                     }
210                     return false;
211                 });
212                 vh.itemView.setFocusable(true);
213                 vh.itemView.setFocusableInTouchMode(true);
214                 return vh;
215             }
216         };
217     }
218 
219     @Override
220     public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
221         mLifecycle.setPreferenceScreen(preferenceScreen);
222         super.setPreferenceScreen(preferenceScreen);
223     }
224 
225     @CallSuper
226     @Override
227     public void onSaveInstanceState(Bundle outState) {
228         super.onSaveInstanceState(outState);
229         mLifecycle.onSaveInstanceState(outState);
230     }
231 
232     @CallSuper
233     @Override
234     public void onStart() {
235         mLifecycle.handleLifecycleEvent(ON_START);
236         super.onStart();
237     }
238 
239     @CallSuper
240     @Override
241     public void onResume() {
242         super.onResume();
243         mLifecycle.handleLifecycleEvent(ON_RESUME);
244     }
245 
246     // This should only be invoked if the parent Fragment is TwoPanelSettingsFragment.
247     @CallSuper
248     @Override
249     public void onArriveAtMainPanel(boolean forward) {
250         logPageFocused(getPageId(), forward);
251     }
252 
253     @CallSuper
254     @Override
255     public void onPause() {
256         mLifecycle.handleLifecycleEvent(ON_PAUSE);
257         super.onPause();
258     }
259 
260     @CallSuper
261     @Override
262     public void onStop() {
263         mLifecycle.handleLifecycleEvent(ON_STOP);
264         super.onStop();
265     }
266 
267     @CallSuper
268     @Override
269     public void onDestroy() {
270         mLifecycle.handleLifecycleEvent(ON_DESTROY);
271         super.onDestroy();
272     }
273 
274     @Override
275     public void onDestroyView() {
276         super.onDestroyView();
277         if (getCallbackFragment() instanceof TwoPanelSettingsFragment) {
278             TwoPanelSettingsFragment parentFragment =
279                     (TwoPanelSettingsFragment) getCallbackFragment();
280             parentFragment.removeListenerForFragment(this);
281         }
282     }
283 
284     @CallSuper
285     @Override
286     public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
287         mLifecycle.onCreateOptionsMenu(menu, inflater);
288         super.onCreateOptionsMenu(menu, inflater);
289     }
290 
291     @CallSuper
292     @Override
293     public void onPrepareOptionsMenu(final Menu menu) {
294         mLifecycle.onPrepareOptionsMenu(menu);
295         super.onPrepareOptionsMenu(menu);
296     }
297 
298     @CallSuper
299     @Override
300     public boolean onOptionsItemSelected(final MenuItem menuItem) {
301         boolean lifecycleHandled = mLifecycle.onOptionsItemSelected(menuItem);
302         if (!lifecycleHandled) {
303             return super.onOptionsItemSelected(menuItem);
304         }
305         return lifecycleHandled;
306     }
307 
308     /** Subclasses should override this to use their own PageId for statsd logging. */
309     protected int getPageId() {
310         return TvSettingsEnums.PAGE_CLASSIC_DEFAULT;
311     }
312 }
313