• 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.LayoutParams.MATCH_PARENT;
20 
21 import static com.android.car.settings.deeplink.DeepLinkHomepageActivity.EXTRA_TARGET_SECONDARY_CONTAINER;
22 import static com.android.car.settings.deeplink.DeepLinkHomepageActivity.convertToDeepLinkHomepageIntent;
23 
24 import android.car.drivingstate.CarUxRestrictions;
25 import android.car.drivingstate.CarUxRestrictionsManager.OnUxRestrictionsChangedListener;
26 import android.content.ComponentName;
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.view.View;
32 import android.view.inputmethod.InputMethodManager;
33 import android.widget.FrameLayout;
34 import android.widget.Toast;
35 
36 import androidx.annotation.Nullable;
37 import androidx.fragment.app.DialogFragment;
38 import androidx.fragment.app.Fragment;
39 import androidx.fragment.app.FragmentActivity;
40 import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
41 import androidx.preference.Preference;
42 import androidx.preference.PreferenceFragmentCompat;
43 
44 import com.android.car.apps.common.util.Themes;
45 import com.android.car.settings.R;
46 import com.android.car.settings.activityembedding.ActivityEmbeddingUtils;
47 import com.android.car.ui.baselayout.Insets;
48 import com.android.car.ui.baselayout.InsetsChangedListener;
49 import com.android.car.ui.core.CarUi;
50 import com.android.car.ui.toolbar.NavButtonMode;
51 import com.android.car.ui.toolbar.ToolbarController;
52 import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin;
53 
54 /**
55  * Base activity class for car settings, provides a action bar with a back button that goes to
56  * previous activity.
57  */
58 public abstract class BaseCarSettingsActivity extends FragmentActivity implements
59         FragmentHost, OnUxRestrictionsChangedListener, UxRestrictionsProvider,
60         OnBackStackChangedListener, PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
61         InsetsChangedListener {
62 
63     /**
64      * Meta data key for specifying the preference key of the top level menu preference that the
65      * initial activity's fragment falls under. If this is not specified in the activity's
66      * metadata, the top level menu preference will not be highlighted upon activity launch.
67      */
68     public static final String META_DATA_KEY_HEADER_KEY =
69             "com.android.car.settings.TOP_LEVEL_HEADER_KEY";
70 
71     /**
72      * Meta data key for specifying activities that should always be shown in the single pane
73      * configuration. If not specified for the activity, the activity will default to the value
74      * {@link R.bool.config_global_force_single_pane}.
75      */
76     public static final String META_DATA_KEY_SINGLE_PANE = "com.android.car.settings.SINGLE_PANE";
77 
78     private static final Logger LOG = new Logger(BaseCarSettingsActivity.class);
79 
80     private String mTopLevelHeaderKey;
81     private boolean mIsSinglePane;
82 
83     private ToolbarController mToolbar;
84 
85     private CarUxRestrictionsHelper mUxRestrictionsHelper;
86     private View mRestrictedMessage;
87     // Default to minimum restriction.
88     private CarUxRestrictions mCarUxRestrictions = new CarUxRestrictions.Builder(
89             /* reqOpt= */ true,
90             CarUxRestrictions.UX_RESTRICTIONS_BASELINE,
91             /* timestamp= */ 0
92     ).build();
93 
94     @Override
onCreate(Bundle savedInstanceState)95     protected void onCreate(Bundle savedInstanceState) {
96         super.onCreate(savedInstanceState);
97         populateMetaData();
98         // When dual-pane is enabled, all activity-filter Intents into Settings should be relaunched
99         // into the secondary container with the exception of HomepageActivity.
100         // For any instance of BaseCarSettingsActivity, if its start-up Intent meets the conditions
101         // for deep link, trampoline it and restart the activity on the secondary container.
102         if (shouldUseSecondaryPaneForActivity()) {
103             startActivity(convertToDeepLinkHomepageIntent(getIntent()));
104             finish();
105             return;
106         }
107         getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));
108         setContentView(this instanceof CarSettingActivities.HomepageActivity
109                 ? R.layout.homepage_activity : R.layout.car_setting_activity);
110 
111         // We do this so that the insets are not automatically sent to the fragments.
112         // The fragments have their own insets handled by the installBaseLayoutAround() method.
113         CarUi.replaceInsetsChangedListenerWith(this, this);
114 
115         setUpToolbarAndDivider();
116         getSupportFragmentManager().addOnBackStackChangedListener(this);
117         mRestrictedMessage = findViewById(R.id.restricted_message);
118         mUxRestrictionsHelper = new CarUxRestrictionsHelper(/* context= */ this, /* listener= */
119                 this);
120 
121         handleNewIntent(getIntent());
122     }
123 
124     @Override
onNewIntent(Intent intent)125     protected void onNewIntent(Intent intent) {
126         super.onNewIntent(intent);
127         handleNewIntent(intent);
128     }
129 
130     /**
131      * Handles when an intent being processed by this class, and should be called every time a new
132      * {@code Intent} is received by this Activity, including during {@link #onCreate(Bundle)}
133      * when this Activity first starts, and during subsequent calls to {@link #onNewIntent(Intent)}.
134      */
handleNewIntent(Intent intent)135     protected void handleNewIntent(Intent intent) {
136         launchIfDifferent(getInitialFragment());
137     }
138 
shouldUseSecondaryPaneForActivity()139     private boolean shouldUseSecondaryPaneForActivity() {
140         if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this)) {
141             return false;
142         }
143         // Homepage and deeplink activity should never be hosted on the secondary pane.
144         if (this instanceof CarSettingActivities.HomepageActivity) {
145             return false;
146         }
147         // All deeplink intents are received via intent-filter so getAction must not be null.
148         // Only starts trampoline for deep link intents. Should return false for all the cases that
149         // CarSettings app starts a SubSettingsActivity.
150         if (getIntent().getAction() == null) {
151             return false;
152         }
153         // If the activity's launch mode is "singleInstance", it can't be embedded in Settings since
154         // it will always be created in a new task.
155         ActivityInfo info = getIntent().resolveActivityInfo(getPackageManager(),
156                 PackageManager.MATCH_DEFAULT_ONLY);
157         if (info.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
158             return false;
159         }
160         // If the activity metadata is configured to be single pane, it should be directly shown.
161         info = getActivityInfo(getPackageManager(), getComponentName());
162         if (info != null && info.metaData != null
163                 && info.metaData.getBoolean(META_DATA_KEY_SINGLE_PANE, false)) {
164             return false;
165         }
166         // This intent has already been restarted as deeplink intent, or was launched by another
167         // activity already embedded on the secondary pane.
168         if (getIntent().getBooleanExtra(EXTRA_TARGET_SECONDARY_CONTAINER, false)) {
169             return false;
170         }
171         return true;
172     }
173 
populateMetaData()174     private void populateMetaData() {
175         ActivityInfo ai = getActivityInfo(getPackageManager(), getComponentName());
176         mIsSinglePane = !ActivityEmbeddingUtils.isEmbeddingSplitActivated(this);
177         if (ai != null && ai.metaData != null) {
178             setTopLevelHeaderKey(ai.metaData.getString(META_DATA_KEY_HEADER_KEY));
179             mIsSinglePane = ai.metaData.getBoolean(META_DATA_KEY_SINGLE_PANE, mIsSinglePane);
180         }
181     }
182 
getTopLevelHeaderKey()183     protected String getTopLevelHeaderKey() {
184         return mTopLevelHeaderKey;
185     }
186 
setTopLevelHeaderKey(@ullable String key)187     protected void setTopLevelHeaderKey(@Nullable String key) {
188         mTopLevelHeaderKey = key;
189     }
190 
launchIfDifferent(Fragment newFragment)191     private void launchIfDifferent(Fragment newFragment) {
192         Fragment currentFragment = getCurrentFragment();
193         if ((newFragment != null) && differentFragment(newFragment, currentFragment)) {
194             updateFragmentContainer(newFragment);
195         }
196     }
197 
differentFragment(Fragment newFragment, Fragment currentFragment)198     private boolean differentFragment(Fragment newFragment, Fragment currentFragment) {
199         return (currentFragment == null)
200                 || (!currentFragment.getClass().equals(newFragment.getClass()));
201     }
202 
203     @Override
onDestroy()204     public void onDestroy() {
205         if (mUxRestrictionsHelper != null) {
206             mUxRestrictionsHelper.destroy();
207             mUxRestrictionsHelper = null;
208         }
209         super.onDestroy();
210     }
211 
212     @Override
onBackPressed()213     public void onBackPressed() {
214         super.onBackPressed();
215         hideKeyboard();
216         // If the backstack is empty, finish the activity.
217         if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
218             finish();
219         }
220     }
221 
222     @Override
getIntent()223     public Intent getIntent() {
224         Intent superIntent = super.getIntent();
225         if (mTopLevelHeaderKey != null) {
226             superIntent.putExtra(META_DATA_KEY_HEADER_KEY, mTopLevelHeaderKey);
227         }
228         superIntent.putExtra(META_DATA_KEY_SINGLE_PANE, mIsSinglePane);
229         return superIntent;
230     }
231 
232     @Override
launchFragment(Fragment fragment)233     public void launchFragment(Fragment fragment) {
234         if (fragment instanceof DialogFragment) {
235             throw new IllegalArgumentException(
236                     "cannot launch dialogs with launchFragment() - use showDialog() instead");
237         }
238         if (mIsSinglePane || this instanceof SubSettingsActivity) {
239             updateFragmentContainer(fragment);
240         } else {
241             Intent intent = SubSettingsActivity.newInstance(this, fragment);
242             setIntent(intent);
243             startActivity(intent);
244         }
245     }
246 
updateFragmentContainer(Fragment fragment)247     protected void updateFragmentContainer(Fragment fragment) {
248         getSupportFragmentManager()
249                 .beginTransaction()
250                 .setCustomAnimations(
251                         Themes.getAttrResourceId(/* context= */ this,
252                                 android.R.attr.fragmentOpenEnterAnimation),
253                         Themes.getAttrResourceId(/* context= */ this,
254                                 android.R.attr.fragmentOpenExitAnimation),
255                         Themes.getAttrResourceId(/* context= */ this,
256                                 android.R.attr.fragmentCloseEnterAnimation),
257                         Themes.getAttrResourceId(/* context= */ this,
258                                 android.R.attr.fragmentCloseExitAnimation))
259                 .replace(getFragmentContainerId(), fragment,
260                         Integer.toString(getSupportFragmentManager().getBackStackEntryCount()))
261                 .addToBackStack(null)
262                 .commit();
263     }
264 
265     @Override
goBack()266     public void goBack() {
267         onBackPressed();
268     }
269 
270     @Override
showBlockingMessage()271     public void showBlockingMessage() {
272         Toast.makeText(this, R.string.restricted_while_driving, Toast.LENGTH_SHORT).show();
273     }
274 
275     @Override
getToolbar()276     public ToolbarController getToolbar() {
277         return mToolbar;
278     }
279 
280     @Override
onUxRestrictionsChanged(CarUxRestrictions restrictionInfo)281     public void onUxRestrictionsChanged(CarUxRestrictions restrictionInfo) {
282         mCarUxRestrictions = restrictionInfo;
283 
284         // Update restrictions for current fragment.
285         Fragment currentFragment = getCurrentFragment();
286         if (currentFragment instanceof OnUxRestrictionsChangedListener) {
287             ((OnUxRestrictionsChangedListener) currentFragment)
288                     .onUxRestrictionsChanged(restrictionInfo);
289         }
290         updateBlockingView(currentFragment);
291     }
292 
293     @Override
getCarUxRestrictions()294     public CarUxRestrictions getCarUxRestrictions() {
295         return mCarUxRestrictions;
296     }
297 
298     @Override
onBackStackChanged()299     public void onBackStackChanged() {
300         onUxRestrictionsChanged(getCarUxRestrictions());
301     }
302 
303     @Override
onCarUiInsetsChanged(Insets insets)304     public void onCarUiInsetsChanged(Insets insets) {
305         // intentional no-op - insets are handled by the listeners created during toolbar setup
306     }
307 
308     @Override
onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref)309     public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
310         if (pref.getFragment() != null) {
311             Fragment fragment = Fragment.instantiate(/* context= */ this, pref.getFragment(),
312                     pref.getExtras());
313             launchFragment(fragment);
314             return true;
315         }
316         return false;
317     }
318 
319     /**
320      * Gets the fragment to show onCreate. If null, the activity will not perform an initial
321      * fragment transaction.
322      */
323     @Nullable
getInitialFragment()324     protected abstract Fragment getInitialFragment();
325 
getCurrentFragment()326     protected Fragment getCurrentFragment() {
327         return getSupportFragmentManager().findFragmentById(getFragmentContainerId());
328     }
329 
getFragmentContainerId()330     private int getFragmentContainerId() {
331         return this instanceof CarSettingActivities.HomepageActivity
332                 ? R.id.top_level_menu_container : R.id.fragment_container;
333     }
334 
335 
updateBlockingView(@ullable Fragment currentFragment)336     private void updateBlockingView(@Nullable Fragment currentFragment) {
337         if (mRestrictedMessage == null) {
338             return;
339         }
340         if (currentFragment instanceof BaseFragment
341                 && !((BaseFragment) currentFragment).canBeShown(mCarUxRestrictions)) {
342             mRestrictedMessage.setVisibility(View.VISIBLE);
343             hideKeyboard();
344         } else {
345             mRestrictedMessage.setVisibility(View.GONE);
346         }
347     }
348 
hideKeyboard()349     private void hideKeyboard() {
350         InputMethodManager imm = getSystemService(InputMethodManager.class);
351         imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
352     }
353 
setUpToolbarAndDivider()354     private void setUpToolbarAndDivider() {
355         boolean isHomepageActivity = this instanceof CarSettingActivities.HomepageActivity;
356         if (isHomepageActivity && !ActivityEmbeddingUtils.isEmbeddingSplitActivated(this)) {
357             findViewById(R.id.top_level_menu_container).setLayoutParams(
358                     new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
359             findViewById(R.id.top_level_divider).setVisibility(View.GONE);
360         }
361         View globalToolbarWrappedView = findViewById(isHomepageActivity
362                 ? R.id.top_level_menu_container : R.id.fragment_container_wrapper);
363         mToolbar = CarUi.installBaseLayoutAround(
364                 globalToolbarWrappedView,
365                 insets -> globalToolbarWrappedView.setPadding(
366                         insets.getLeft(), insets.getTop(), insets.getRight(),
367                         insets.getBottom()), /* hasToolbar= */ true);
368         mToolbar.setNavButtonMode(NavButtonMode.BACK);
369     }
370 
371     /**
372      * Returns the ActivityInfo of the given componentName.
373      */
374     @Nullable
getActivityInfo(PackageManager pm, ComponentName componentName)375     public ActivityInfo getActivityInfo(PackageManager pm, ComponentName componentName) {
376         try {
377             return pm.getActivityInfo(componentName, PackageManager.GET_META_DATA);
378         } catch (PackageManager.NameNotFoundException e) {
379             LOG.w("Unable to find package", e);
380         }
381         return null;
382     }
383 }
384