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