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