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