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