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