• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.car.settings.common;
18 
19 import static android.view.ViewGroup.FOCUS_BEFORE_DESCENDANTS;
20 import static android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS;
21 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
22 
23 import android.car.drivingstate.CarUxRestrictions;
24 import android.car.drivingstate.CarUxRestrictionsManager;
25 import android.car.drivingstate.CarUxRestrictionsManager.OnUxRestrictionsChangedListener;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.ActivityInfo;
29 import android.content.pm.PackageManager;
30 import android.os.Bundle;
31 import android.provider.Settings;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.ViewTreeObserver;
35 import android.view.inputmethod.InputMethodManager;
36 import android.widget.Toast;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 import androidx.fragment.app.DialogFragment;
41 import androidx.fragment.app.Fragment;
42 import androidx.fragment.app.FragmentActivity;
43 import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
44 import androidx.preference.Preference;
45 import androidx.preference.PreferenceFragmentCompat;
46 
47 import com.android.car.apps.common.util.Themes;
48 import com.android.car.settings.R;
49 import com.android.car.settings.common.rotary.SettingsFocusParkingView;
50 import com.android.car.ui.baselayout.Insets;
51 import com.android.car.ui.baselayout.InsetsChangedListener;
52 import com.android.car.ui.core.CarUi;
53 import com.android.car.ui.toolbar.MenuItem;
54 import com.android.car.ui.toolbar.NavButtonMode;
55 import com.android.car.ui.toolbar.ToolbarController;
56 import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin;
57 
58 import java.util.Collections;
59 import java.util.List;
60 
61 /**
62  * Base activity class for car settings, provides a action bar with a back button that goes to
63  * previous activity.
64  */
65 public abstract class BaseCarSettingsActivity extends FragmentActivity implements
66         FragmentHost, OnUxRestrictionsChangedListener, UxRestrictionsProvider,
67         OnBackStackChangedListener, PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
68         InsetsChangedListener {
69 
70     /**
71      * Meta data key for specifying the preference key of the top level menu preference that the
72      * initial activity's fragment falls under. If this is not specified in the activity's
73      * metadata, the top level menu preference will not be highlighted upon activity launch.
74      */
75     public static final String META_DATA_KEY_HEADER_KEY =
76             "com.android.car.settings.TOP_LEVEL_HEADER_KEY";
77 
78     /**
79      * Meta data key for specifying activities that should always be shown in the single pane
80      * configuration. If not specified for the activity, the activity will default to the value
81      * {@link R.bool.config_global_force_single_pane}.
82      */
83     public static final String META_DATA_KEY_SINGLE_PANE = "com.android.car.settings.SINGLE_PANE";
84 
85     private static final Logger LOG = new Logger(BaseCarSettingsActivity.class);
86     private static final int SEARCH_REQUEST_CODE = 501;
87     private static final String KEY_HAS_NEW_INTENT = "key_has_new_intent";
88 
89     private boolean mHasNewIntent = true;
90     private boolean mHasInitialFocus = false;
91 
92     private String mTopLevelHeaderKey;
93     private boolean mIsSinglePane;
94 
95     private ToolbarController mGlobalToolbar;
96     private ToolbarController mMiniToolbar;
97 
98     private CarUxRestrictionsHelper mUxRestrictionsHelper;
99     private ViewGroup mFragmentContainer;
100     private View mRestrictedMessage;
101     // Default to minimum restriction.
102     private CarUxRestrictions mCarUxRestrictions = new CarUxRestrictions.Builder(
103             /* reqOpt= */ true,
104             CarUxRestrictions.UX_RESTRICTIONS_BASELINE,
105             /* timestamp= */ 0
106     ).build();
107 
108     private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener;
109     private final ViewTreeObserver.OnGlobalFocusChangeListener mFocusChangeListener =
110             (oldFocus, newFocus) -> {
111                 if (oldFocus instanceof SettingsFocusParkingView) {
112                     // Focus is manually shifted away from the SettingsFocusParkingView.
113                     // Therefore, the focus should no longer shift upon global layout.
114                     removeGlobalLayoutListener();
115                 }
116                 if (newFocus instanceof SettingsFocusParkingView && mGlobalLayoutListener == null) {
117                     // Attempting to shift focus to the SettingsFocusParkingView without a layout
118                     // listener is not allowed, since it can cause undermined focus behavior
119                     // in these rare edge cases.
120                     requestTopLevelMenuFocus();
121                 }
122 
123                 // This will maintain focus in the content pane if a view goes from
124                 // focusable -> unfocusable.
125                 if (oldFocus == null && mHasInitialFocus) {
126                     requestContentPaneFocus();
127                 } else {
128                     mHasInitialFocus = true;
129                 }
130             };
131 
132     @Override
onCreate(Bundle savedInstanceState)133     protected void onCreate(Bundle savedInstanceState) {
134         super.onCreate(savedInstanceState);
135         getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));
136         if (savedInstanceState != null) {
137             mHasNewIntent = savedInstanceState.getBoolean(KEY_HAS_NEW_INTENT, mHasNewIntent);
138         }
139         populateMetaData();
140         setContentView(R.layout.car_setting_activity);
141         mFragmentContainer = findViewById(R.id.fragment_container);
142 
143         // We do this so that the insets are not automatically sent to the fragments.
144         // The fragments have their own insets handled by the installBaseLayoutAround() method.
145         CarUi.replaceInsetsChangedListenerWith(this, this);
146 
147         setUpToolbars();
148         getSupportFragmentManager().addOnBackStackChangedListener(this);
149         mRestrictedMessage = findViewById(R.id.restricted_message);
150 
151         if (mHasNewIntent) {
152             launchIfDifferent(getInitialFragment());
153             mHasNewIntent = false;
154         } else if (!mIsSinglePane) {
155             updateMiniToolbarState();
156         }
157         mUxRestrictionsHelper = new CarUxRestrictionsHelper(/* context= */ this, /* listener= */
158                 this);
159 
160         if (shouldFocusContentOnLaunch()) {
161             requestContentPaneFocus();
162             mHasInitialFocus = true;
163         } else {
164             requestTopLevelMenuFocus();
165         }
166         setUpFocusChangeListener(true);
167     }
168 
169     @Override
onSaveInstanceState(@onNull Bundle outState)170     protected void onSaveInstanceState(@NonNull Bundle outState) {
171         super.onSaveInstanceState(outState);
172         outState.putBoolean(KEY_HAS_NEW_INTENT, mHasNewIntent);
173     }
174 
175     @Override
onDestroy()176     public void onDestroy() {
177         setUpFocusChangeListener(false);
178         removeGlobalLayoutListener();
179         mUxRestrictionsHelper.destroy();
180         mUxRestrictionsHelper = null;
181         super.onDestroy();
182     }
183 
184     @Override
onBackPressed()185     public void onBackPressed() {
186         super.onBackPressed();
187         hideKeyboard();
188         // If the backstack is empty, finish the activity.
189         if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
190             finish();
191         }
192     }
193 
194     @Override
getIntent()195     public Intent getIntent() {
196         Intent superIntent = super.getIntent();
197         if (mTopLevelHeaderKey != null) {
198             superIntent.putExtra(META_DATA_KEY_HEADER_KEY, mTopLevelHeaderKey);
199         }
200         superIntent.putExtra(META_DATA_KEY_SINGLE_PANE, mIsSinglePane);
201         return superIntent;
202     }
203 
204     @Override
launchFragment(Fragment fragment)205     public void launchFragment(Fragment fragment) {
206         if (fragment instanceof DialogFragment) {
207             throw new IllegalArgumentException(
208                     "cannot launch dialogs with launchFragment() - use showDialog() instead");
209         }
210 
211         if (mIsSinglePane) {
212             Intent intent = SubSettingsActivity.newInstance(/* context= */ this, fragment);
213             startActivity(intent);
214         } else {
215             launchFragmentInternal(fragment);
216         }
217     }
218 
launchFragmentInternal(Fragment fragment)219     protected void launchFragmentInternal(Fragment fragment) {
220         getSupportFragmentManager()
221                 .beginTransaction()
222                 .setCustomAnimations(
223                         Themes.getAttrResourceId(/* context= */ this,
224                                 android.R.attr.fragmentOpenEnterAnimation),
225                         Themes.getAttrResourceId(/* context= */ this,
226                                 android.R.attr.fragmentOpenExitAnimation),
227                         Themes.getAttrResourceId(/* context= */ this,
228                                 android.R.attr.fragmentCloseEnterAnimation),
229                         Themes.getAttrResourceId(/* context= */ this,
230                                 android.R.attr.fragmentCloseExitAnimation))
231                 .replace(R.id.fragment_container, fragment,
232                         Integer.toString(getSupportFragmentManager().getBackStackEntryCount()))
233                 .addToBackStack(null)
234                 .commit();
235     }
236 
237     @Override
goBack()238     public void goBack() {
239         onBackPressed();
240     }
241 
242     @Override
showBlockingMessage()243     public void showBlockingMessage() {
244         Toast.makeText(this, R.string.restricted_while_driving, Toast.LENGTH_SHORT).show();
245     }
246 
247     @Override
getToolbar()248     public ToolbarController getToolbar() {
249         if (mIsSinglePane) {
250             return mGlobalToolbar;
251         }
252         return mMiniToolbar;
253     }
254 
255     @Override
onUxRestrictionsChanged(CarUxRestrictions restrictionInfo)256     public void onUxRestrictionsChanged(CarUxRestrictions restrictionInfo) {
257         mCarUxRestrictions = restrictionInfo;
258 
259         // Update restrictions for current fragment.
260         Fragment currentFragment = getCurrentFragment();
261         if (currentFragment instanceof OnUxRestrictionsChangedListener) {
262             ((OnUxRestrictionsChangedListener) currentFragment)
263                     .onUxRestrictionsChanged(restrictionInfo);
264         }
265         updateBlockingView(currentFragment);
266 
267         if (!mIsSinglePane) {
268             // Update restrictions for top level menu (if present).
269             Fragment topLevelMenu =
270                     getSupportFragmentManager().findFragmentById(R.id.top_level_menu);
271             if (topLevelMenu instanceof CarUxRestrictionsManager.OnUxRestrictionsChangedListener) {
272                 ((CarUxRestrictionsManager.OnUxRestrictionsChangedListener) topLevelMenu)
273                         .onUxRestrictionsChanged(restrictionInfo);
274             }
275         }
276     }
277 
278     @Override
getCarUxRestrictions()279     public CarUxRestrictions getCarUxRestrictions() {
280         return mCarUxRestrictions;
281     }
282 
283     @Override
onBackStackChanged()284     public void onBackStackChanged() {
285         onUxRestrictionsChanged(getCarUxRestrictions());
286         if (!mIsSinglePane) {
287             if (mHasInitialFocus) {
288                 requestContentPaneFocus();
289             }
290             updateMiniToolbarState();
291         }
292     }
293 
294     @Override
onCarUiInsetsChanged(Insets insets)295     public void onCarUiInsetsChanged(Insets insets) {
296         // intentional no-op - insets are handled by the listeners created during toolbar setup
297     }
298 
299     @Override
onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref)300     public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
301         if (pref.getFragment() != null) {
302             Fragment fragment = Fragment.instantiate(/* context= */ this, pref.getFragment(),
303                     pref.getExtras());
304             launchFragment(fragment);
305             return true;
306         }
307         return false;
308     }
309 
310     /**
311      * Gets the fragment to show onCreate. If null, the activity will not perform an initial
312      * fragment transaction.
313      */
314     @Nullable
getInitialFragment()315     protected abstract Fragment getInitialFragment();
316 
getCurrentFragment()317     protected Fragment getCurrentFragment() {
318         return getSupportFragmentManager().findFragmentById(R.id.fragment_container);
319     }
320 
321     /**
322      * Returns whether the content pane should get focus initially when in dual-pane configuration.
323      */
shouldFocusContentOnLaunch()324     protected boolean shouldFocusContentOnLaunch() {
325         return true;
326     }
327 
launchIfDifferent(Fragment newFragment)328     private void launchIfDifferent(Fragment newFragment) {
329         Fragment currentFragment = getCurrentFragment();
330         if ((newFragment != null) && differentFragment(newFragment, currentFragment)) {
331             LOG.d("launchIfDifferent: " + newFragment + " replacing " + currentFragment);
332             launchFragmentInternal(newFragment);
333         }
334     }
335 
336     /**
337      * Returns {code true} if newFragment is different from current fragment.
338      */
differentFragment(Fragment newFragment, Fragment currentFragment)339     private boolean differentFragment(Fragment newFragment, Fragment currentFragment) {
340         return (currentFragment == null)
341                 || (!currentFragment.getClass().equals(newFragment.getClass()));
342     }
343 
hideKeyboard()344     private void hideKeyboard() {
345         InputMethodManager imm = (InputMethodManager) this.getSystemService(
346                 Context.INPUT_METHOD_SERVICE);
347         imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
348     }
349 
updateBlockingView(@ullable Fragment currentFragment)350     private void updateBlockingView(@Nullable Fragment currentFragment) {
351         if (mRestrictedMessage == null) {
352             return;
353         }
354         if (currentFragment instanceof BaseFragment
355                 && !((BaseFragment) currentFragment).canBeShown(mCarUxRestrictions)) {
356             mRestrictedMessage.setVisibility(View.VISIBLE);
357             mFragmentContainer.setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
358             mFragmentContainer.clearFocus();
359             hideKeyboard();
360         } else {
361             mRestrictedMessage.setVisibility(View.GONE);
362             mFragmentContainer.setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
363         }
364     }
365 
populateMetaData()366     private void populateMetaData() {
367         try {
368             ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
369                     PackageManager.GET_META_DATA);
370             if (ai == null || ai.metaData == null) {
371                 mIsSinglePane = getResources().getBoolean(R.bool.config_global_force_single_pane);
372                 return;
373             }
374             mTopLevelHeaderKey = ai.metaData.getString(META_DATA_KEY_HEADER_KEY);
375             mIsSinglePane = ai.metaData.getBoolean(META_DATA_KEY_SINGLE_PANE,
376                     getResources().getBoolean(R.bool.config_global_force_single_pane));
377         } catch (PackageManager.NameNotFoundException e) {
378             LOG.w("Unable to find package", e);
379         }
380     }
381 
setUpToolbars()382     private void setUpToolbars() {
383         View globalToolbarWrappedView = mIsSinglePane ? findViewById(
384                 R.id.fragment_container_wrapper) : findViewById(R.id.top_level_menu);
385         mGlobalToolbar = CarUi.installBaseLayoutAround(
386                 globalToolbarWrappedView,
387                 insets -> globalToolbarWrappedView.setPadding(
388                         insets.getLeft(), insets.getTop(), insets.getRight(),
389                         insets.getBottom()), /* hasToolbar= */ true);
390         if (mIsSinglePane) {
391             mGlobalToolbar.setNavButtonMode(NavButtonMode.BACK);
392             findViewById(R.id.top_level_menu_container).setVisibility(View.GONE);
393             findViewById(R.id.top_level_divider).setVisibility(View.GONE);
394             return;
395         }
396         mMiniToolbar = CarUi.installBaseLayoutAround(
397                 findViewById(R.id.fragment_container_wrapper),
398                 insets -> findViewById(R.id.fragment_container_wrapper).setPadding(
399                         insets.getLeft(), insets.getTop(), insets.getRight(),
400                         insets.getBottom()), /* hasToolbar= */ true);
401 
402         MenuItem searchButton = new MenuItem.Builder(this)
403                 .setToSearch()
404                 .setOnClickListener(i -> onSearchButtonClicked())
405                 .setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD)
406                 .setId(R.id.toolbar_menu_item_0)
407                 .build();
408         List<MenuItem> items = Collections.singletonList(searchButton);
409 
410         mGlobalToolbar.setTitle(R.string.settings_label);
411         mGlobalToolbar.setNavButtonMode(NavButtonMode.DISABLED);
412         mGlobalToolbar.setLogo(R.drawable.ic_launcher_settings);
413         mGlobalToolbar.setMenuItems(items);
414     }
415 
updateMiniToolbarState()416     private void updateMiniToolbarState() {
417         if (mMiniToolbar == null) {
418             return;
419         }
420         if (getSupportFragmentManager().getBackStackEntryCount() > 1 || !isTaskRoot()) {
421             mMiniToolbar.setNavButtonMode(NavButtonMode.BACK);
422         } else {
423             mMiniToolbar.setNavButtonMode(NavButtonMode.DISABLED);
424         }
425     }
426 
setUpFocusChangeListener(boolean enable)427     private void setUpFocusChangeListener(boolean enable) {
428         if (mIsSinglePane) {
429             // The focus change listener is only needed with two panes.
430             return;
431         }
432         ViewTreeObserver observer = findViewById(
433                 R.id.car_settings_activity_wrapper).getViewTreeObserver();
434         if (enable) {
435             observer.addOnGlobalFocusChangeListener(mFocusChangeListener);
436         } else {
437             observer.removeOnGlobalFocusChangeListener(mFocusChangeListener);
438         }
439     }
440 
requestTopLevelMenuFocus()441     private void requestTopLevelMenuFocus() {
442         if (mIsSinglePane) {
443             return;
444         }
445         Fragment topLevelMenu = getSupportFragmentManager().findFragmentById(R.id.top_level_menu);
446         if (topLevelMenu == null) {
447             return;
448         }
449         View fragmentView = topLevelMenu.getView();
450         if (fragmentView == null) {
451             return;
452         }
453         View focusArea = fragmentView.findViewById(R.id.settings_car_ui_focus_area);
454         if (focusArea == null) {
455             return;
456         }
457         removeGlobalLayoutListener();
458         mGlobalLayoutListener = () -> {
459             if (focusArea.isInTouchMode() || focusArea.hasFocus()) {
460                 return;
461             }
462             focusArea.performAccessibilityAction(ACTION_FOCUS, /* arguments= */ null);
463             removeGlobalLayoutListener();
464         };
465         fragmentView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
466     }
467 
requestContentPaneFocus()468     private void requestContentPaneFocus() {
469         if (mIsSinglePane) {
470             return;
471         }
472         if (getCurrentFragment() == null) {
473             return;
474         }
475         View fragmentView = getCurrentFragment().getView();
476         if (fragmentView == null) {
477             return;
478         }
479         removeGlobalLayoutListener();
480         if (fragmentView.isInTouchMode()) {
481             mHasInitialFocus = false;
482             return;
483         }
484         View focusArea = fragmentView.findViewById(R.id.settings_car_ui_focus_area);
485 
486         if (focusArea == null) {
487             focusArea = fragmentView.findViewById(R.id.settings_content_focus_area);
488             if (focusArea == null) {
489                 return;
490             }
491         }
492         removeGlobalLayoutListener();
493         View finalFocusArea = focusArea; // required to be effectively final for inner class access
494         mGlobalLayoutListener = () -> {
495             if (finalFocusArea.isInTouchMode() || finalFocusArea.hasFocus()) {
496                 return;
497             }
498             boolean success = finalFocusArea.performAccessibilityAction(
499                     ACTION_FOCUS, /* arguments= */ null);
500             if (success) {
501                 removeGlobalLayoutListener();
502             } else {
503                 findViewById(
504                         R.id.settings_focus_parking_view).performAccessibilityAction(
505                         ACTION_FOCUS, /* arguments= */ null);
506             }
507         };
508         fragmentView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
509     }
510 
removeGlobalLayoutListener()511     private void removeGlobalLayoutListener() {
512         if (mGlobalLayoutListener == null) {
513             return;
514         }
515 
516         // Check content pane
517         Fragment contentFragment = getCurrentFragment();
518         if (contentFragment != null && contentFragment.getView() != null) {
519             contentFragment.getView().getViewTreeObserver()
520                     .removeOnGlobalLayoutListener(mGlobalLayoutListener);
521         }
522 
523         // Check top level menu
524         Fragment topLevelMenu = getSupportFragmentManager().findFragmentById(R.id.top_level_menu);
525         if (topLevelMenu != null && topLevelMenu.getView() != null) {
526             topLevelMenu.getView().getViewTreeObserver()
527                     .removeOnGlobalLayoutListener(mGlobalLayoutListener);
528         }
529 
530         mGlobalLayoutListener = null;
531     }
532 
onSearchButtonClicked()533     private void onSearchButtonClicked() {
534         Intent intent = new Intent(Settings.ACTION_APP_SEARCH_SETTINGS)
535                 .setPackage(getSettingsIntelligencePkgName());
536         if (intent.resolveActivity(getPackageManager()) == null) {
537             return;
538         }
539         startActivityForResult(intent, SEARCH_REQUEST_CODE);
540     }
541 
getSettingsIntelligencePkgName()542     private String getSettingsIntelligencePkgName() {
543         return getString(R.string.config_settingsintelligence_package_name);
544     }
545 }
546