1 /*
2  * Copyright (C) 2022 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 package com.android.permissioncontroller.safetycenter.ui;
17 
18 import static android.content.Intent.ACTION_SAFETY_CENTER;
19 import static android.content.Intent.FLAG_ACTIVITY_FORWARD_RESULT;
20 import static android.os.Build.VERSION_CODES.TIRAMISU;
21 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
22 import static android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCES_GROUP_ID;
23 
24 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION;
25 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_CLICKED;
26 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.EXTRA_SETTINGS_FRAGMENT_ARGS_KEY;
27 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PERSONAL_PROFILE_SUFFIX;
28 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PRIVACY_SOURCES_GROUP_ID;
29 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PRIVATE_PROFILE_SUFFIX;
30 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.WORK_PROFILE_SUFFIX;
31 
32 import android.app.ActionBar;
33 import android.content.Intent;
34 import android.content.res.Configuration;
35 import android.os.Bundle;
36 import android.provider.Settings;
37 import android.safetycenter.SafetyCenterManager;
38 import android.safetycenter.config.SafetyCenterConfig;
39 import android.safetycenter.config.SafetySource;
40 import android.safetycenter.config.SafetySourcesGroup;
41 import android.text.TextUtils;
42 import android.util.Log;
43 import android.view.View;
44 
45 import androidx.annotation.Nullable;
46 import androidx.annotation.RequiresApi;
47 import androidx.core.graphics.Insets;
48 import androidx.core.view.ViewCompat;
49 import androidx.core.view.WindowInsetsCompat;
50 import androidx.fragment.app.Fragment;
51 import androidx.lifecycle.Lifecycle;
52 import androidx.lifecycle.LifecycleEventObserver;
53 import androidx.lifecycle.LifecycleOwner;
54 import androidx.preference.PreferenceFragmentCompat;
55 
56 import com.android.permissioncontroller.Constants;
57 import com.android.permissioncontroller.PermissionControllerStatsLog;
58 import com.android.permissioncontroller.R;
59 import com.android.permissioncontroller.permission.utils.Utils;
60 import com.android.permissioncontroller.safetycenter.ui.model.PrivacyControlsViewModel.Pref;
61 import com.android.settingslib.activityembedding.ActivityEmbeddingUtils;
62 import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity;
63 
64 import java.util.List;
65 import java.util.Objects;
66 
67 /** Entry-point activity for SafetyCenter. */
68 @RequiresApi(TIRAMISU)
69 public final class SafetyCenterActivity extends CollapsingToolbarBaseActivity {
70 
71     private static final String TAG = SafetyCenterActivity.class.getSimpleName();
72     private static final String PRIVACY_CONTROLS_ACTION = "android.settings.PRIVACY_CONTROLS";
73     private static final String MENU_KEY_SAFETY_CENTER = "top_level_safety_center";
74     private static final String EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI =
75             "android.provider.extra.SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI";
76     private static final String EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY =
77             "android.provider.extra.SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY";
78     private static final String EXTRA_PREVENT_TRAMPOLINE_TO_SETTINGS =
79             "com.android.permissioncontroller.safetycenter.extra.PREVENT_TRAMPOLINE_TO_SETTINGS";
80 
81     private SafetyCenterManager mSafetyCenterManager;
82 
83     @Override
onCreate(Bundle savedInstanceState)84     public void onCreate(Bundle savedInstanceState) {
85         super.onCreate(savedInstanceState);
86         mSafetyCenterManager = getSystemService(SafetyCenterManager.class);
87 
88         if (maybeRedirectIfDisabled()) {
89             return;
90         }
91 
92         if (maybeRedirectIntoTwoPaneSettings()) {
93             return;
94         }
95 
96         Fragment frag;
97         final boolean maybeOpenSubpage =
98                 SafetyCenterUiFlags.getShowSubpages()
99                         && getIntent().getAction().equals(ACTION_SAFETY_CENTER);
100         if (maybeOpenSubpage && getIntent().hasExtra(EXTRA_SAFETY_SOURCES_GROUP_ID)) {
101             String groupId = getIntent().getStringExtra(EXTRA_SAFETY_SOURCES_GROUP_ID);
102             frag = openRelevantSubpage(groupId);
103         } else if (maybeOpenSubpage && getIntent().hasExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY)) {
104             String preferenceKey = getIntent().getStringExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY);
105             String groupId = getParentGroupId(preferenceKey);
106             frag = openRelevantSubpage(groupId);
107         } else if (getIntent().getAction().equals(PRIVACY_CONTROLS_ACTION)) {
108             setTitle(R.string.privacy_controls_title);
109             frag = PrivacyControlsFragment.newInstance();
110         } else {
111             frag = openHomepage();
112         }
113 
114         if (savedInstanceState == null) {
115             getSupportFragmentManager()
116                     .beginTransaction()
117                     .add(com.android.settingslib.collapsingtoolbar.R.id.content_frame, frag)
118                     .commitNow();
119         }
120 
121         configureHomeButton();
122 
123         frag.getLifecycle()
124                 .addObserver(
125                         new LifecycleEventObserver() {
126                             @Override
127                             public void onStateChanged(
128                                     LifecycleOwner unused, Lifecycle.Event event) {
129                                 if (event != Lifecycle.Event.ON_START) {
130                                     return;
131                                 }
132                                 View listView = getListView(frag);
133                                 if (listView == null) {
134                                     return;
135                                 }
136                                 int paddingBottom = listView.getPaddingBottom();
137                                 ViewCompat.setOnApplyWindowInsetsListener(
138                                         listView,
139                                         (v, windowInsets) -> {
140                                             Insets insets =
141                                                     windowInsets.getInsets(
142                                                             WindowInsetsCompat.Type.systemBars());
143                                             v.setPadding(
144                                                     v.getPaddingLeft(),
145                                                     v.getPaddingTop(),
146                                                     v.getPaddingRight(),
147                                                     paddingBottom + insets.bottom);
148                                             return WindowInsetsCompat.CONSUMED;
149                                         });
150                             }
151                         });
152     }
153 
154     @Override
onStart()155     protected void onStart() {
156         super.onStart();
157         maybeRedirectIfDisabled();
158     }
159 
160     @Override
onConfigurationChanged(Configuration newConfig)161     public void onConfigurationChanged(Configuration newConfig) {
162         // We don't set configChanges, but small screen size changes may still be delivered here.
163         super.onConfigurationChanged(newConfig);
164         configureHomeButton();
165     }
166 
167     /** Decide whether a home/back button should be shown or not. */
configureHomeButton()168     private void configureHomeButton() {
169         ActionBar actionBar = getActionBar();
170         Fragment frag =
171                 getSupportFragmentManager()
172                         .findFragmentById(
173                                 com.android.settingslib.collapsingtoolbar.R.id.content_frame);
174         if (actionBar == null || frag == null) {
175             return;
176         }
177 
178         // Only the homepage can be considered a "second layer" page as it's the only one that
179         // can be reached from the Settings menu. The other pages are only reachable using
180         // a direct intent (e.g. notification, "first layer") and/or by navigating within Safety
181         // Center ("third layer").
182         // Note that the homepage can also be a "first layer" page, but that would only happen
183         // if the activity is not embedded.
184         boolean isSecondLayerPage = frag instanceof SafetyCenterScrollWrapperFragment;
185         if (ActivityEmbeddingUtils.shouldHideNavigateUpButton(this, isSecondLayerPage)) {
186             actionBar.setDisplayHomeAsUpEnabled(false);
187             actionBar.setHomeButtonEnabled(false);
188         }
189     }
190 
maybeRedirectIfDisabled()191     private boolean maybeRedirectIfDisabled() {
192         if (mSafetyCenterManager == null || !mSafetyCenterManager.isSafetyCenterEnabled()) {
193             Log.w(TAG, "Safety Center disabled, redirecting to settings page");
194             startActivity(
195                     new Intent(getActionToRedirectWhenDisabled())
196                             .addFlags(FLAG_ACTIVITY_FORWARD_RESULT));
197             finish();
198             return true;
199         }
200         return false;
201     }
202 
getActionToRedirectWhenDisabled()203     private String getActionToRedirectWhenDisabled() {
204         boolean isPrivacyControls =
205                 TextUtils.equals(getIntent().getAction(), PRIVACY_CONTROLS_ACTION);
206         if (isPrivacyControls) {
207             return Settings.ACTION_PRIVACY_SETTINGS;
208         }
209         return Settings.ACTION_SETTINGS;
210     }
211 
maybeRedirectIntoTwoPaneSettings()212     private boolean maybeRedirectIntoTwoPaneSettings() {
213         return shouldUseTwoPaneSettings() && tryRedirectTwoPaneSettings();
214     }
215 
shouldUseTwoPaneSettings()216     private boolean shouldUseTwoPaneSettings() {
217         if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this)) {
218             return false;
219         }
220 
221         Bundle extras = getIntent().getExtras();
222         if (extras != null && extras.getBoolean(EXTRA_PREVENT_TRAMPOLINE_TO_SETTINGS, false)) {
223             return false;
224         }
225 
226         return isTaskRoot() && !ActivityEmbeddingUtils.isActivityEmbedded(this);
227     }
228 
229     /** Return {@code true} if the redirection was attempted. */
tryRedirectTwoPaneSettings()230     private boolean tryRedirectTwoPaneSettings() {
231         Intent twoPaneIntent = getTwoPaneIntent();
232         if (twoPaneIntent == null) {
233             return false;
234         }
235 
236         Log.i(TAG, "Safety Center restarting in Settings two-pane layout");
237         startActivity(twoPaneIntent);
238         finishAndRemoveTask();
239         return true;
240     }
241 
242     @Nullable
getTwoPaneIntent()243     private Intent getTwoPaneIntent() {
244         Intent twoPaneIntent = ActivityEmbeddingUtils.buildEmbeddingActivityBaseIntent(this);
245         if (twoPaneIntent == null) {
246             return null;
247         }
248 
249         twoPaneIntent.putExtras(getIntent());
250         twoPaneIntent.putExtra(
251                 EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI,
252                 getIntent().toUri(Intent.URI_INTENT_SCHEME));
253         twoPaneIntent.putExtra(
254                 EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY, MENU_KEY_SAFETY_CENTER);
255         return twoPaneIntent;
256     }
257 
logPrivacySourceMetric()258     private void logPrivacySourceMetric() {
259         Intent intent = getIntent();
260         if (intent != null && intent.hasExtra(Constants.EXTRA_PRIVACY_SOURCE)) {
261             int privacySource = intent.getIntExtra(Constants.EXTRA_PRIVACY_SOURCE, -1);
262             int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
263             long sessionId =
264                     intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID);
265             Log.i(
266                     TAG,
267                     "privacy source notification metric, source "
268                             + privacySource
269                             + " uid "
270                             + uid
271                             + " sessionId "
272                             + sessionId);
273             PermissionControllerStatsLog.write(
274                     PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
275                     privacySource,
276                     uid,
277                     PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_CLICKED,
278                     sessionId);
279         }
280     }
281 
openHomepage()282     private Fragment openHomepage() {
283         logPrivacySourceMetric();
284         setTitle(getString(R.string.safety_center_dashboard_page_title));
285         return new SafetyCenterScrollWrapperFragment();
286     }
287 
288     @RequiresApi(UPSIDE_DOWN_CAKE)
openRelevantSubpage(String groupId)289     private Fragment openRelevantSubpage(String groupId) {
290         if (groupId.isEmpty()) {
291             return openHomepage();
292         }
293 
294         long sessionId = Utils.getOrGenerateSessionId(getIntent());
295         if (Objects.equals(groupId, PRIVACY_SOURCES_GROUP_ID)) {
296             logPrivacySourceMetric();
297             return PrivacySubpageFragment.newInstance(sessionId);
298         }
299 
300         return SafetyCenterSubpageFragment.newInstance(sessionId, groupId);
301     }
302 
303     @RequiresApi(UPSIDE_DOWN_CAKE)
getParentGroupId(String preferenceKey)304     private String getParentGroupId(String preferenceKey) {
305         if (Pref.findByKey(preferenceKey) != null) {
306             return PRIVACY_SOURCES_GROUP_ID;
307         }
308 
309         SafetyCenterConfig safetyCenterConfig = mSafetyCenterManager.getSafetyCenterConfig();
310         String[] splitKey;
311         if (preferenceKey.endsWith(PERSONAL_PROFILE_SUFFIX)) {
312             splitKey = preferenceKey.split("_" + PERSONAL_PROFILE_SUFFIX);
313         } else if (preferenceKey.endsWith(WORK_PROFILE_SUFFIX)) {
314             splitKey = preferenceKey.split("_" + WORK_PROFILE_SUFFIX);
315         } else if (preferenceKey.endsWith(PRIVATE_PROFILE_SUFFIX)) {
316             splitKey = preferenceKey.split("_" + PRIVATE_PROFILE_SUFFIX);
317         } else {
318             return "";
319         }
320 
321         if (safetyCenterConfig == null || splitKey.length == 0) {
322             return "";
323         }
324 
325         List<SafetySourcesGroup> groups = safetyCenterConfig.getSafetySourcesGroups();
326         for (SafetySourcesGroup group : groups) {
327             if (group.getType() != SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_STATEFUL) {
328                 // Hidden and static groups are not opened in a subpage.
329                 continue;
330             }
331             for (SafetySource source : group.getSafetySources()) {
332                 if (Objects.equals(source.getId(), splitKey[0])) {
333                     return group.getId();
334                 }
335             }
336         }
337         return "";
338     }
339 
340     @Nullable
getListView(Fragment fragment)341     private View getListView(Fragment fragment) {
342         if (fragment instanceof PreferenceFragmentCompat) {
343             return ((PreferenceFragmentCompat) fragment).getListView();
344         }
345         if (fragment instanceof SafetyCenterScrollWrapperFragment) {
346             Fragment dashboardFragment =
347                     fragment.getChildFragmentManager().findFragmentById(R.id.fragment_container);
348             if (dashboardFragment instanceof PreferenceFragmentCompat) {
349                 return ((PreferenceFragmentCompat) dashboardFragment).getListView();
350             }
351         }
352         return null;
353     }
354 }
355