1 /* 2 * Copyright (C) 2018 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.settings.homepage; 18 19 import static android.provider.Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY; 20 import static android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY; 21 import static android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI; 22 23 import android.animation.LayoutTransition; 24 import android.app.ActivityManager; 25 import android.app.settings.SettingsEnums; 26 import android.content.ComponentName; 27 import android.content.Intent; 28 import android.content.res.Configuration; 29 import android.os.Bundle; 30 import android.text.TextUtils; 31 import android.util.ArraySet; 32 import android.util.FeatureFlagUtils; 33 import android.util.Log; 34 import android.view.View; 35 import android.view.Window; 36 import android.view.WindowManager; 37 import android.widget.FrameLayout; 38 import android.widget.ImageView; 39 import android.widget.Toolbar; 40 41 import androidx.fragment.app.Fragment; 42 import androidx.fragment.app.FragmentActivity; 43 import androidx.fragment.app.FragmentManager; 44 import androidx.fragment.app.FragmentTransaction; 45 import androidx.window.embedding.SplitRule; 46 47 import com.android.settings.R; 48 import com.android.settings.Settings; 49 import com.android.settings.SettingsActivity; 50 import com.android.settings.SettingsApplication; 51 import com.android.settings.accounts.AvatarViewMixin; 52 import com.android.settings.activityembedding.ActivityEmbeddingRulesController; 53 import com.android.settings.activityembedding.ActivityEmbeddingUtils; 54 import com.android.settings.core.CategoryMixin; 55 import com.android.settings.core.FeatureFlags; 56 import com.android.settings.homepage.contextualcards.ContextualCardsFragment; 57 import com.android.settings.overlay.FeatureFactory; 58 import com.android.settingslib.Utils; 59 import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin; 60 61 import java.net.URISyntaxException; 62 import java.util.Set; 63 64 /** Settings homepage activity */ 65 public class SettingsHomepageActivity extends FragmentActivity implements 66 CategoryMixin.CategoryHandler { 67 68 private static final String TAG = "SettingsHomepageActivity"; 69 70 // Additional extra of Settings#ACTION_SETTINGS_LARGE_SCREEN_DEEP_LINK. 71 // Put true value to the intent when startActivity for a deep link intent from this Activity. 72 public static final String EXTRA_IS_FROM_SETTINGS_HOMEPAGE = "is_from_settings_homepage"; 73 74 // Additional extra of Settings#ACTION_SETTINGS_LARGE_SCREEN_DEEP_LINK. 75 // Set & get Uri of the Intent separately to prevent failure of Intent#ParseUri. 76 public static final String EXTRA_SETTINGS_LARGE_SCREEN_DEEP_LINK_INTENT_DATA = 77 "settings_large_screen_deep_link_intent_data"; 78 79 static final int DEFAULT_HIGHLIGHT_MENU_KEY = R.string.menu_key_network; 80 private static final long HOMEPAGE_LOADING_TIMEOUT_MS = 300; 81 82 private TopLevelSettings mMainFragment; 83 private View mHomepageView; 84 private View mSuggestionView; 85 private View mTwoPaneSuggestionView; 86 private CategoryMixin mCategoryMixin; 87 private Set<HomepageLoadedListener> mLoadedListeners; 88 private boolean mIsEmbeddingActivityEnabled; 89 private boolean mIsTwoPaneLastTime; 90 91 /** A listener receiving homepage loaded events. */ 92 public interface HomepageLoadedListener { 93 /** Called when the homepage is loaded. */ onHomepageLoaded()94 void onHomepageLoaded(); 95 } 96 97 private interface FragmentBuilder<T extends Fragment> { build()98 T build(); 99 } 100 101 /** 102 * Try to add a {@link HomepageLoadedListener}. If homepage is already loaded, the listener 103 * will not be notified. 104 * 105 * @return Whether the listener is added. 106 */ addHomepageLoadedListener(HomepageLoadedListener listener)107 public boolean addHomepageLoadedListener(HomepageLoadedListener listener) { 108 if (mHomepageView == null) { 109 return false; 110 } else { 111 if (!mLoadedListeners.contains(listener)) { 112 mLoadedListeners.add(listener); 113 } 114 return true; 115 } 116 } 117 118 /** 119 * Shows the homepage and shows/hides the suggestion together. Only allows to be executed once 120 * to avoid the flicker caused by the suggestion suddenly appearing/disappearing. 121 */ showHomepageWithSuggestion(boolean showSuggestion)122 public void showHomepageWithSuggestion(boolean showSuggestion) { 123 if (mHomepageView == null) { 124 return; 125 } 126 Log.i(TAG, "showHomepageWithSuggestion: " + showSuggestion); 127 final View homepageView = mHomepageView; 128 mSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE); 129 mTwoPaneSuggestionView.setVisibility(showSuggestion ? View.VISIBLE : View.GONE); 130 mHomepageView = null; 131 132 mLoadedListeners.forEach(listener -> listener.onHomepageLoaded()); 133 mLoadedListeners.clear(); 134 homepageView.setVisibility(View.VISIBLE); 135 } 136 137 /** Returns the main content fragment */ getMainFragment()138 public TopLevelSettings getMainFragment() { 139 return mMainFragment; 140 } 141 142 @Override getCategoryMixin()143 public CategoryMixin getCategoryMixin() { 144 return mCategoryMixin; 145 } 146 147 @Override onCreate(Bundle savedInstanceState)148 protected void onCreate(Bundle savedInstanceState) { 149 super.onCreate(savedInstanceState); 150 setContentView(R.layout.settings_homepage_container); 151 mIsEmbeddingActivityEnabled = ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this); 152 mIsTwoPaneLastTime = ActivityEmbeddingUtils.isTwoPaneResolution(this); 153 154 final View appBar = findViewById(R.id.app_bar_container); 155 appBar.setMinimumHeight(getSearchBoxHeight()); 156 initHomepageContainer(); 157 updateHomepageAppBar(); 158 updateHomepageBackground(); 159 mLoadedListeners = new ArraySet<>(); 160 161 initSearchBarView(); 162 163 getLifecycle().addObserver(new HideNonSystemOverlayMixin(this)); 164 mCategoryMixin = new CategoryMixin(this); 165 getLifecycle().addObserver(mCategoryMixin); 166 167 final String highlightMenuKey = getHighlightMenuKey(); 168 // Only allow features on high ram devices. 169 if (!getSystemService(ActivityManager.class).isLowRamDevice()) { 170 initAvatarView(); 171 final boolean scrollNeeded = mIsEmbeddingActivityEnabled 172 && !TextUtils.equals(getString(DEFAULT_HIGHLIGHT_MENU_KEY), highlightMenuKey); 173 showSuggestionFragment(scrollNeeded); 174 if (FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) { 175 showFragment(() -> new ContextualCardsFragment(), R.id.contextual_cards_content); 176 } 177 } 178 mMainFragment = showFragment(() -> { 179 final TopLevelSettings fragment = new TopLevelSettings(); 180 fragment.getArguments().putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, 181 highlightMenuKey); 182 return fragment; 183 }, R.id.main_content); 184 185 ((FrameLayout) findViewById(R.id.main_content)) 186 .getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); 187 188 // Launch the intent from deep link for large screen devices. 189 launchDeepLinkIntentToRight(); 190 } 191 192 @Override onStart()193 protected void onStart() { 194 ((SettingsApplication) getApplication()).setHomeActivity(this); 195 super.onStart(); 196 } 197 198 @Override onNewIntent(Intent intent)199 protected void onNewIntent(Intent intent) { 200 super.onNewIntent(intent); 201 202 // When it's large screen 2-pane and Settings app is in the background, receiving an Intent 203 // will not recreate this activity. Update the intent for this case. 204 setIntent(intent); 205 reloadHighlightMenuKey(); 206 if (isFinishing()) { 207 return; 208 } 209 // Launch the intent from deep link for large screen devices. 210 launchDeepLinkIntentToRight(); 211 } 212 213 @Override onConfigurationChanged(Configuration newConfig)214 public void onConfigurationChanged(Configuration newConfig) { 215 super.onConfigurationChanged(newConfig); 216 final boolean isTwoPane = ActivityEmbeddingUtils.isTwoPaneResolution(this); 217 if (mIsTwoPaneLastTime != isTwoPane) { 218 mIsTwoPaneLastTime = isTwoPane; 219 updateHomepageAppBar(); 220 updateHomepageBackground(); 221 } 222 } 223 initSearchBarView()224 private void initSearchBarView() { 225 final Toolbar toolbar = findViewById(R.id.search_action_bar); 226 FeatureFactory.getFactory(this).getSearchFeatureProvider() 227 .initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE); 228 229 if (mIsEmbeddingActivityEnabled) { 230 final Toolbar toolbarTwoPaneVersion = findViewById(R.id.search_action_bar_two_pane); 231 FeatureFactory.getFactory(this).getSearchFeatureProvider() 232 .initSearchToolbar(this /* activity */, toolbarTwoPaneVersion, 233 SettingsEnums.SETTINGS_HOMEPAGE); 234 } 235 } 236 initAvatarView()237 private void initAvatarView() { 238 final ImageView avatarView = findViewById(R.id.account_avatar); 239 final ImageView avatarTwoPaneView = findViewById(R.id.account_avatar_two_pane_version); 240 if (AvatarViewMixin.isAvatarSupported(this)) { 241 avatarView.setVisibility(View.VISIBLE); 242 getLifecycle().addObserver(new AvatarViewMixin(this, avatarView)); 243 244 if (mIsEmbeddingActivityEnabled) { 245 avatarTwoPaneView.setVisibility(View.VISIBLE); 246 getLifecycle().addObserver(new AvatarViewMixin(this, avatarTwoPaneView)); 247 } 248 } 249 } 250 updateHomepageBackground()251 private void updateHomepageBackground() { 252 if (!mIsEmbeddingActivityEnabled) { 253 return; 254 } 255 256 final Window window = getWindow(); 257 final int color = ActivityEmbeddingUtils.isTwoPaneResolution(this) 258 ? Utils.getColorAttrDefaultColor(this, com.android.internal.R.attr.colorSurface) 259 : Utils.getColorAttrDefaultColor(this, android.R.attr.colorBackground); 260 261 window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 262 // Update status bar color 263 window.setStatusBarColor(color); 264 // Update content background. 265 findViewById(R.id.settings_homepage_container).setBackgroundColor(color); 266 } 267 showSuggestionFragment(boolean scrollNeeded)268 private void showSuggestionFragment(boolean scrollNeeded) { 269 final Class<? extends Fragment> fragmentClass = FeatureFactory.getFactory(this) 270 .getSuggestionFeatureProvider(this).getContextualSuggestionFragment(); 271 if (fragmentClass == null) { 272 return; 273 } 274 275 mSuggestionView = findViewById(R.id.suggestion_content); 276 mTwoPaneSuggestionView = findViewById(R.id.two_pane_suggestion_content); 277 mHomepageView = findViewById(R.id.settings_homepage_container); 278 // Hide the homepage for preparing the suggestion. If scrolling is needed, the list views 279 // should be initialized in the invisible homepage view to prevent a scroll flicker. 280 mHomepageView.setVisibility(scrollNeeded ? View.INVISIBLE : View.GONE); 281 // Schedule a timer to show the homepage and hide the suggestion on timeout. 282 mHomepageView.postDelayed(() -> showHomepageWithSuggestion(false), 283 HOMEPAGE_LOADING_TIMEOUT_MS); 284 final FragmentBuilder<?> fragmentBuilder = () -> { 285 try { 286 return fragmentClass.getConstructor().newInstance(); 287 } catch (Exception e) { 288 Log.w(TAG, "Cannot show fragment", e); 289 } 290 return null; 291 }; 292 showFragment(fragmentBuilder, R.id.suggestion_content); 293 if (mIsEmbeddingActivityEnabled) { 294 showFragment(fragmentBuilder, R.id.two_pane_suggestion_content); 295 } 296 } 297 showFragment(FragmentBuilder<T> fragmentBuilder, int id)298 private <T extends Fragment> T showFragment(FragmentBuilder<T> fragmentBuilder, int id) { 299 final FragmentManager fragmentManager = getSupportFragmentManager(); 300 final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); 301 T showFragment = (T) fragmentManager.findFragmentById(id); 302 303 if (showFragment == null) { 304 showFragment = fragmentBuilder.build(); 305 fragmentTransaction.add(id, showFragment); 306 } else { 307 fragmentTransaction.show(showFragment); 308 } 309 fragmentTransaction.commit(); 310 return showFragment; 311 } 312 launchDeepLinkIntentToRight()313 private void launchDeepLinkIntentToRight() { 314 if (!mIsEmbeddingActivityEnabled) { 315 return; 316 } 317 318 final Intent intent = getIntent(); 319 if (intent == null || !TextUtils.equals(intent.getAction(), 320 ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY)) { 321 return; 322 } 323 324 if (!(this instanceof DeepLinkHomepageActivity 325 || this instanceof SliceDeepLinkHomepageActivity)) { 326 Log.e(TAG, "Not a deep link component"); 327 finish(); 328 return; 329 } 330 331 final String intentUriString = intent.getStringExtra( 332 EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI); 333 if (TextUtils.isEmpty(intentUriString)) { 334 Log.e(TAG, "No EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI to deep link"); 335 finish(); 336 return; 337 } 338 339 final Intent targetIntent; 340 try { 341 targetIntent = Intent.parseUri(intentUriString, Intent.URI_INTENT_SCHEME); 342 } catch (URISyntaxException e) { 343 Log.e(TAG, "Failed to parse deep link intent: " + e); 344 finish(); 345 return; 346 } 347 348 final ComponentName targetComponentName = targetIntent.resolveActivity(getPackageManager()); 349 if (targetComponentName == null) { 350 Log.e(TAG, "No valid target for the deep link intent: " + targetIntent); 351 finish(); 352 return; 353 } 354 targetIntent.setComponent(targetComponentName); 355 356 // To prevent launchDeepLinkIntentToRight again for configuration change. 357 intent.setAction(null); 358 359 targetIntent.setFlags(targetIntent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK); 360 targetIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 361 362 // Sender of intent may want to send intent extra data to the destination of targetIntent. 363 targetIntent.replaceExtras(intent); 364 365 targetIntent.putExtra(EXTRA_IS_FROM_SETTINGS_HOMEPAGE, true); 366 targetIntent.putExtra(SettingsActivity.EXTRA_IS_FROM_SLICE, false); 367 368 targetIntent.setData(intent.getParcelableExtra( 369 SettingsHomepageActivity.EXTRA_SETTINGS_LARGE_SCREEN_DEEP_LINK_INTENT_DATA)); 370 371 // Set 2-pane pair rule for the deep link page. 372 ActivityEmbeddingRulesController.registerTwoPanePairRule(this, 373 new ComponentName(getApplicationContext(), getClass()), 374 targetComponentName, 375 targetIntent.getAction(), 376 SplitRule.FINISH_ALWAYS, 377 SplitRule.FINISH_ALWAYS, 378 true /* clearTop */); 379 ActivityEmbeddingRulesController.registerTwoPanePairRule(this, 380 new ComponentName(getApplicationContext(), Settings.class), 381 targetComponentName, 382 targetIntent.getAction(), 383 SplitRule.FINISH_ALWAYS, 384 SplitRule.FINISH_ALWAYS, 385 true /* clearTop */); 386 startActivity(targetIntent); 387 } 388 getHighlightMenuKey()389 private String getHighlightMenuKey() { 390 final Intent intent = getIntent(); 391 if (intent != null && TextUtils.equals(intent.getAction(), 392 ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY)) { 393 final String menuKey = intent.getStringExtra( 394 EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY); 395 if (!TextUtils.isEmpty(menuKey)) { 396 return menuKey; 397 } 398 } 399 return getString(DEFAULT_HIGHLIGHT_MENU_KEY); 400 } 401 reloadHighlightMenuKey()402 private void reloadHighlightMenuKey() { 403 mMainFragment.getArguments().putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, 404 getHighlightMenuKey()); 405 mMainFragment.reloadHighlightMenuKey(); 406 } 407 initHomepageContainer()408 private void initHomepageContainer() { 409 final View view = findViewById(R.id.homepage_container); 410 // Prevent inner RecyclerView gets focus and invokes scrolling. 411 view.setFocusableInTouchMode(true); 412 view.requestFocus(); 413 } 414 updateHomepageAppBar()415 private void updateHomepageAppBar() { 416 if (!mIsEmbeddingActivityEnabled) { 417 return; 418 } 419 if (ActivityEmbeddingUtils.isTwoPaneResolution(this)) { 420 findViewById(R.id.homepage_app_bar_regular_phone_view).setVisibility(View.GONE); 421 findViewById(R.id.homepage_app_bar_two_pane_view).setVisibility(View.VISIBLE); 422 } else { 423 findViewById(R.id.homepage_app_bar_regular_phone_view).setVisibility(View.VISIBLE); 424 findViewById(R.id.homepage_app_bar_two_pane_view).setVisibility(View.GONE); 425 } 426 } 427 getSearchBoxHeight()428 private int getSearchBoxHeight() { 429 final int searchBarHeight = getResources().getDimensionPixelSize(R.dimen.search_bar_height); 430 final int searchBarMargin = getResources().getDimensionPixelSize(R.dimen.search_bar_margin); 431 return searchBarHeight + searchBarMargin * 2; 432 } 433 } 434