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