• 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                     newFocus.clearFocus();
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         }
164         setUpFocusChangeListener(true);
165     }
166 
167     @Override
onSaveInstanceState(@onNull Bundle outState)168     protected void onSaveInstanceState(@NonNull Bundle outState) {
169         super.onSaveInstanceState(outState);
170         outState.putBoolean(KEY_HAS_NEW_INTENT, mHasNewIntent);
171     }
172 
173     @Override
onDestroy()174     public void onDestroy() {
175         setUpFocusChangeListener(false);
176         removeGlobalLayoutListener();
177         mUxRestrictionsHelper.destroy();
178         mUxRestrictionsHelper = null;
179         super.onDestroy();
180     }
181 
182     @Override
onBackPressed()183     public void onBackPressed() {
184         super.onBackPressed();
185         hideKeyboard();
186         // If the backstack is empty, finish the activity.
187         if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
188             finish();
189         }
190     }
191 
192     @Override
getIntent()193     public Intent getIntent() {
194         Intent superIntent = super.getIntent();
195         if (mTopLevelHeaderKey != null) {
196             superIntent.putExtra(META_DATA_KEY_HEADER_KEY, mTopLevelHeaderKey);
197         }
198         superIntent.putExtra(META_DATA_KEY_SINGLE_PANE, mIsSinglePane);
199         return superIntent;
200     }
201 
202     @Override
launchFragment(Fragment fragment)203     public void launchFragment(Fragment fragment) {
204         if (fragment instanceof DialogFragment) {
205             throw new IllegalArgumentException(
206                     "cannot launch dialogs with launchFragment() - use showDialog() instead");
207         }
208 
209         if (mIsSinglePane) {
210             Intent intent = SubSettingsActivity.newInstance(/* context= */ this, fragment);
211             startActivity(intent);
212         } else {
213             launchFragmentInternal(fragment);
214         }
215     }
216 
launchFragmentInternal(Fragment fragment)217     private void launchFragmentInternal(Fragment fragment) {
218         getSupportFragmentManager()
219                 .beginTransaction()
220                 .setCustomAnimations(
221                         Themes.getAttrResourceId(/* context= */ this,
222                                 android.R.attr.fragmentOpenEnterAnimation),
223                         Themes.getAttrResourceId(/* context= */ this,
224                                 android.R.attr.fragmentOpenExitAnimation),
225                         Themes.getAttrResourceId(/* context= */ this,
226                                 android.R.attr.fragmentCloseEnterAnimation),
227                         Themes.getAttrResourceId(/* context= */ this,
228                                 android.R.attr.fragmentCloseExitAnimation))
229                 .replace(R.id.fragment_container, fragment,
230                         Integer.toString(getSupportFragmentManager().getBackStackEntryCount()))
231                 .addToBackStack(null)
232                 .commit();
233     }
234 
235     @Override
goBack()236     public void goBack() {
237         onBackPressed();
238     }
239 
240     @Override
showBlockingMessage()241     public void showBlockingMessage() {
242         Toast.makeText(this, R.string.restricted_while_driving, Toast.LENGTH_SHORT).show();
243     }
244 
245     @Override
getToolbar()246     public ToolbarController getToolbar() {
247         if (mIsSinglePane) {
248             return mGlobalToolbar;
249         }
250         return mMiniToolbar;
251     }
252 
253     @Override
onUxRestrictionsChanged(CarUxRestrictions restrictionInfo)254     public void onUxRestrictionsChanged(CarUxRestrictions restrictionInfo) {
255         mCarUxRestrictions = restrictionInfo;
256 
257         // Update restrictions for current fragment.
258         Fragment currentFragment = getCurrentFragment();
259         if (currentFragment instanceof OnUxRestrictionsChangedListener) {
260             ((OnUxRestrictionsChangedListener) currentFragment)
261                     .onUxRestrictionsChanged(restrictionInfo);
262         }
263         updateBlockingView(currentFragment);
264 
265         if (!mIsSinglePane) {
266             // Update restrictions for top level menu (if present).
267             Fragment topLevelMenu =
268                     getSupportFragmentManager().findFragmentById(R.id.top_level_menu);
269             if (topLevelMenu instanceof CarUxRestrictionsManager.OnUxRestrictionsChangedListener) {
270                 ((CarUxRestrictionsManager.OnUxRestrictionsChangedListener) topLevelMenu)
271                         .onUxRestrictionsChanged(restrictionInfo);
272             }
273         }
274     }
275 
276     @Override
getCarUxRestrictions()277     public CarUxRestrictions getCarUxRestrictions() {
278         return mCarUxRestrictions;
279     }
280 
281     @Override
onBackStackChanged()282     public void onBackStackChanged() {
283         onUxRestrictionsChanged(getCarUxRestrictions());
284         if (!mIsSinglePane) {
285             if (mHasInitialFocus) {
286                 requestContentPaneFocus();
287             }
288             updateMiniToolbarState();
289         }
290     }
291 
292     @Override
onCarUiInsetsChanged(Insets insets)293     public void onCarUiInsetsChanged(Insets insets) {
294         // intentional no-op - insets are handled by the listeners created during toolbar setup
295     }
296 
297     @Override
onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref)298     public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
299         if (pref.getFragment() != null) {
300             Fragment fragment = Fragment.instantiate(/* context= */ this, pref.getFragment(),
301                     pref.getExtras());
302             launchFragment(fragment);
303             return true;
304         }
305         return false;
306     }
307 
308     /**
309      * Gets the fragment to show onCreate. If null, the activity will not perform an initial
310      * fragment transaction.
311      */
312     @Nullable
getInitialFragment()313     protected abstract Fragment getInitialFragment();
314 
getCurrentFragment()315     protected Fragment getCurrentFragment() {
316         return getSupportFragmentManager().findFragmentById(R.id.fragment_container);
317     }
318 
319     /**
320      * Returns whether the content pane should get focus initially when in dual-pane configuration.
321      */
shouldFocusContentOnLaunch()322     protected boolean shouldFocusContentOnLaunch() {
323         return true;
324     }
325 
launchIfDifferent(Fragment newFragment)326     private void launchIfDifferent(Fragment newFragment) {
327         Fragment currentFragment = getCurrentFragment();
328         if ((newFragment != null) && differentFragment(newFragment, currentFragment)) {
329             LOG.d("launchIfDifferent: " + newFragment + " replacing " + currentFragment);
330             launchFragmentInternal(newFragment);
331         }
332     }
333 
334     /**
335      * Returns {code true} if newFragment is different from current fragment.
336      */
differentFragment(Fragment newFragment, Fragment currentFragment)337     private boolean differentFragment(Fragment newFragment, Fragment currentFragment) {
338         return (currentFragment == null)
339                 || (!currentFragment.getClass().equals(newFragment.getClass()));
340     }
341 
hideKeyboard()342     private void hideKeyboard() {
343         InputMethodManager imm = (InputMethodManager) this.getSystemService(
344                 Context.INPUT_METHOD_SERVICE);
345         imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
346     }
347 
updateBlockingView(@ullable Fragment currentFragment)348     private void updateBlockingView(@Nullable Fragment currentFragment) {
349         if (mRestrictedMessage == null) {
350             return;
351         }
352         if (currentFragment instanceof BaseFragment
353                 && !((BaseFragment) currentFragment).canBeShown(mCarUxRestrictions)) {
354             mRestrictedMessage.setVisibility(View.VISIBLE);
355             mFragmentContainer.setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);
356             mFragmentContainer.clearFocus();
357             hideKeyboard();
358         } else {
359             mRestrictedMessage.setVisibility(View.GONE);
360             mFragmentContainer.setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
361         }
362     }
363 
populateMetaData()364     private void populateMetaData() {
365         try {
366             ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
367                     PackageManager.GET_META_DATA);
368             if (ai == null || ai.metaData == null) {
369                 mIsSinglePane = getResources().getBoolean(R.bool.config_global_force_single_pane);
370                 return;
371             }
372             mTopLevelHeaderKey = ai.metaData.getString(META_DATA_KEY_HEADER_KEY);
373             mIsSinglePane = ai.metaData.getBoolean(META_DATA_KEY_SINGLE_PANE,
374                     getResources().getBoolean(R.bool.config_global_force_single_pane));
375         } catch (PackageManager.NameNotFoundException e) {
376             LOG.w("Unable to find package", e);
377         }
378     }
379 
setUpToolbars()380     private void setUpToolbars() {
381         View globalToolbarWrappedView = mIsSinglePane ? findViewById(
382                 R.id.fragment_container_wrapper) : findViewById(R.id.top_level_menu);
383         mGlobalToolbar = CarUi.installBaseLayoutAround(
384                 globalToolbarWrappedView,
385                 insets -> globalToolbarWrappedView.setPadding(
386                         insets.getLeft(), insets.getTop(), insets.getRight(),
387                         insets.getBottom()), /* hasToolbar= */ true);
388         if (mIsSinglePane) {
389             mGlobalToolbar.setNavButtonMode(NavButtonMode.BACK);
390             findViewById(R.id.top_level_menu_container).setVisibility(View.GONE);
391             findViewById(R.id.top_level_divider).setVisibility(View.GONE);
392             return;
393         }
394         mMiniToolbar = CarUi.installBaseLayoutAround(
395                 findViewById(R.id.fragment_container_wrapper),
396                 insets -> findViewById(R.id.fragment_container_wrapper).setPadding(
397                         insets.getLeft(), insets.getTop(), insets.getRight(),
398                         insets.getBottom()), /* hasToolbar= */ true);
399 
400         MenuItem searchButton = new MenuItem.Builder(this)
401                 .setToSearch()
402                 .setOnClickListener(i -> onSearchButtonClicked())
403                 .setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD)
404                 .setId(R.id.toolbar_menu_item_0)
405                 .build();
406         List<MenuItem> items = Collections.singletonList(searchButton);
407 
408         mGlobalToolbar.setTitle(R.string.settings_label);
409         mGlobalToolbar.setNavButtonMode(NavButtonMode.DISABLED);
410         mGlobalToolbar.setLogo(R.drawable.ic_launcher_settings);
411         mGlobalToolbar.setMenuItems(items);
412     }
413 
updateMiniToolbarState()414     private void updateMiniToolbarState() {
415         if (mMiniToolbar == null) {
416             return;
417         }
418         if (getSupportFragmentManager().getBackStackEntryCount() > 1 || !isTaskRoot()) {
419             mMiniToolbar.setNavButtonMode(NavButtonMode.BACK);
420         } else {
421             mMiniToolbar.setNavButtonMode(NavButtonMode.DISABLED);
422         }
423     }
424 
setUpFocusChangeListener(boolean enable)425     private void setUpFocusChangeListener(boolean enable) {
426         if (mIsSinglePane) {
427             // The focus change listener is only needed with two panes.
428             return;
429         }
430         ViewTreeObserver observer = findViewById(
431                 R.id.car_settings_activity_wrapper).getViewTreeObserver();
432         if (enable) {
433             observer.addOnGlobalFocusChangeListener(mFocusChangeListener);
434         } else {
435             observer.removeOnGlobalFocusChangeListener(mFocusChangeListener);
436         }
437     }
438 
requestContentPaneFocus()439     private void requestContentPaneFocus() {
440         if (mIsSinglePane) {
441             return;
442         }
443         if (getCurrentFragment() == null) {
444             return;
445         }
446         View fragmentView = getCurrentFragment().getView();
447         if (fragmentView == null) {
448             return;
449         }
450         removeGlobalLayoutListener();
451         if (fragmentView.isInTouchMode()) {
452             mHasInitialFocus = false;
453             return;
454         }
455         View focusArea = fragmentView.findViewById(R.id.settings_car_ui_focus_area);
456 
457         if (focusArea == null) {
458             focusArea = fragmentView.findViewById(R.id.settings_content_focus_area);
459             if (focusArea == null) {
460                 return;
461             }
462         }
463         View finalFocusArea = focusArea; // required to be effectively final for inner class access
464         mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
465             @Override
466             public void onGlobalLayout() {
467                 boolean success = finalFocusArea.performAccessibilityAction(
468                         ACTION_FOCUS, /* arguments= */ null);
469                 if (success) {
470                     removeGlobalLayoutListener();
471                 } else {
472                     findViewById(
473                             R.id.settings_focus_parking_view).performAccessibilityAction(
474                             ACTION_FOCUS, /* arguments= */ null);
475                 }
476             }
477         };
478         fragmentView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
479     }
480 
removeGlobalLayoutListener()481     private void removeGlobalLayoutListener() {
482         if (mGlobalLayoutListener == null) {
483             return;
484         }
485         if (getCurrentFragment() == null) {
486             return;
487         }
488         View fragmentView = getCurrentFragment().getView();
489         if (fragmentView == null) {
490             return;
491         }
492         fragmentView.getViewTreeObserver()
493                 .removeOnGlobalLayoutListener(mGlobalLayoutListener);
494         mGlobalLayoutListener = null;
495     }
496 
onSearchButtonClicked()497     private void onSearchButtonClicked() {
498         Intent intent = new Intent(Settings.ACTION_APP_SEARCH_SETTINGS)
499                 .setPackage(getSettingsIntelligencePkgName());
500         if (intent.resolveActivity(getPackageManager()) == null) {
501             return;
502         }
503         startActivityForResult(intent, SEARCH_REQUEST_CODE);
504     }
505 
getSettingsIntelligencePkgName()506     private String getSettingsIntelligencePkgName() {
507         return getString(R.string.config_settingsintelligence_package_name);
508     }
509 }
510