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