1 /*
2 
3  * Copyright (C) 2017 The Android Open Source Project
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.settings.accounts;
19 
20 import android.accounts.Account;
21 import android.accounts.AuthenticatorDescription;
22 import android.content.ClipData;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ActivityInfo;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.content.pm.PackageManager.NameNotFoundException;
29 import android.content.pm.ResolveInfo;
30 import android.content.res.Resources;
31 import android.content.res.Resources.Theme;
32 import android.os.UserHandle;
33 import android.text.TextUtils;
34 import android.util.Log;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.annotation.VisibleForTesting;
39 import androidx.collection.ArraySet;
40 import androidx.preference.Preference;
41 import androidx.preference.Preference.OnPreferenceClickListener;
42 import androidx.preference.PreferenceFragmentCompat;
43 import androidx.preference.PreferenceGroup;
44 import androidx.preference.PreferenceScreen;
45 
46 import com.android.settings.R;
47 import com.android.settings.core.SubSettingLauncher;
48 import com.android.settings.location.LocationSettings;
49 import com.android.settings.utils.LocalClassLoaderContextThemeWrapper;
50 import com.android.settingslib.accounts.AuthenticatorHelper;
51 import com.android.settingslib.core.instrumentation.Instrumentable;
52 
53 import java.util.Set;
54 
55 /**
56  * Class to load the preference screen to be added to the settings page for the specific account
57  * type as specified in the account-authenticator.
58  */
59 public class AccountTypePreferenceLoader {
60 
61     private static final String TAG = "AccountTypePrefLoader";
62     private static final String ACCOUNT_KEY = "account"; // to pass to auth settings
63     // Action name for the broadcast intent when the Google account preferences page is launching
64     // the location settings.
65     private static final String LAUNCHING_LOCATION_SETTINGS =
66         "com.android.settings.accounts.LAUNCHING_LOCATION_SETTINGS";
67 
68     private AuthenticatorHelper mAuthenticatorHelper;
69     private UserHandle mUserHandle;
70     private PreferenceFragmentCompat mFragment;
71 
AccountTypePreferenceLoader(PreferenceFragmentCompat fragment, AuthenticatorHelper authenticatorHelper, UserHandle userHandle)72     public AccountTypePreferenceLoader(PreferenceFragmentCompat fragment,
73             AuthenticatorHelper authenticatorHelper, UserHandle userHandle) {
74         mFragment = fragment;
75         mAuthenticatorHelper = authenticatorHelper;
76         mUserHandle = userHandle;
77     }
78 
79     /**
80      * Gets the preferences.xml file associated with a particular account type.
81      * @param accountType the type of account
82      * @return a PreferenceScreen inflated from accountPreferenceId.
83      */
addPreferencesForType(final String accountType, PreferenceScreen parent)84     public PreferenceScreen addPreferencesForType(final String accountType,
85             PreferenceScreen parent) {
86         PreferenceScreen prefs = null;
87         if (mAuthenticatorHelper.containsAccountType(accountType)) {
88             AuthenticatorDescription desc = null;
89             try {
90                 desc = mAuthenticatorHelper.getAccountTypeDescription(accountType);
91                 if (desc != null && desc.accountPreferencesId != 0) {
92                     Set<String> fragmentAllowList = generateFragmentAllowlist(parent);
93                     // Load the context of the target package, then apply the
94                     // base Settings theme (no references to local resources)
95                     // and create a context theme wrapper so that we get the
96                     // correct text colors. Control colors will still be wrong,
97                     // but there's not much we can do about it since we can't
98                     // reference local color resources.
99                     final Context targetCtx = mFragment.getActivity().createPackageContextAsUser(
100                             desc.packageName, 0, mUserHandle);
101                     final Theme baseTheme = mFragment.getResources().newTheme();
102                     baseTheme.applyStyle(
103                             com.android.settingslib.widget.theme.R.style.Theme_SettingsBase, true);
104                     final Context themedCtx =
105                             new LocalClassLoaderContextThemeWrapper(getClass(), targetCtx, 0);
106                     themedCtx.getTheme().setTo(baseTheme);
107                     prefs = mFragment.getPreferenceManager().inflateFromResource(themedCtx,
108                             desc.accountPreferencesId, parent);
109                     // Ignore Fragments provided dynamically, as these are coming from external
110                     // applications which must not have access to internal Settings' fragments.
111                     // These preferences are rendered into Settings, so they also won't have access
112                     // to their own Fragments, meaning there is no acceptable usage of
113                     // android:fragment here.
114                     filterBlockedFragments(prefs, fragmentAllowList);
115                 }
116             } catch (PackageManager.NameNotFoundException e) {
117                 Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName);
118             } catch (Resources.NotFoundException e) {
119                 Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName);
120             }
121         }
122         return prefs;
123     }
124 
125     /**
126      * Recursively filters through the preference list provided by GoogleLoginService.
127      *
128      * This method removes all the invalid intent from the list, adds account name as extra into the
129      * intent, and hack the location settings to start it as a fragment.
130      */
updatePreferenceIntents(PreferenceGroup prefs, final String acccountType, Account account)131     public void updatePreferenceIntents(PreferenceGroup prefs, final String acccountType,
132             Account account) {
133         final PackageManager pm = mFragment.getActivity().getPackageManager();
134         for (int i = 0; i < prefs.getPreferenceCount(); ) {
135             Preference pref = prefs.getPreference(i);
136             if (pref instanceof PreferenceGroup) {
137                 updatePreferenceIntents((PreferenceGroup) pref, acccountType, account);
138             }
139             Intent intent = pref.getIntent();
140             if (intent != null) {
141                 // Hack. Launch "Location" as fragment instead of as activity.
142                 //
143                 // When "Location" is launched as activity via Intent, there's no "Up" button at the
144                 // top left, and if there's another running instance of "Location" activity, the
145                 // back stack would usually point to some other place so the user won't be able to
146                 // go back to the previous page by "back" key. Using fragment is a much easier
147                 // solution to those problems.
148                 //
149                 // If we set Intent to null and assign a fragment to the PreferenceScreen item here,
150                 // in order to make it work as expected, we still need to modify the container
151                 // PreferenceActivity, override onPreferenceStartFragment() and call
152                 // startPreferencePanel() there. In order to inject the title string there, more
153                 // dirty further hack is still needed. It's much easier and cleaner to listen to
154                 // preference click event here directly.
155                 if (TextUtils.equals(intent.getAction(),
156                         android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) {
157                     // The OnPreferenceClickListener overrides the click event completely. No intent
158                     // will get fired.
159                     pref.setOnPreferenceClickListener(new FragmentStarter(
160                         LocationSettings.class.getName(), R.string.location_settings_title));
161                 } else {
162                     ResolveInfo ri = pm.resolveActivityAsUser(intent,
163                         PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier());
164                     if (ri == null) {
165                         prefs.removePreference(pref);
166                         continue;
167                     }
168                     intent.putExtra(ACCOUNT_KEY, account);
169                     intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
170                     pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
171                         @Override
172                         public boolean onPreferenceClick(Preference preference) {
173                             Intent prefIntent = preference.getIntent();
174                                 /*
175                                  * Check the intent to see if it resolves to a exported=false
176                                  * activity that doesn't share a uid with the authenticator.
177                                  *
178                                  * Otherwise the intent is considered unsafe in that it will be
179                                  * exploiting the fact that settings has system privileges.
180                                  */
181                             if (isSafeIntent(pm, prefIntent, acccountType)) {
182                                 // Explicitly set an empty ClipData to ensure that we don't offer to
183                                 // promote any Uris contained inside for granting purposes
184                                 prefIntent.setClipData(ClipData.newPlainText(null, null));
185                                 mFragment.getActivity().startActivityAsUser(
186                                     prefIntent, mUserHandle);
187                             } else {
188                                 Log.e(TAG,
189                                     "Refusing to launch authenticator intent because"
190                                         + "it exploits Settings permissions: "
191                                         + prefIntent);
192                             }
193                             return true;
194                         }
195                     });
196                 }
197             }
198             i++;
199         }
200     }
201 
202     // Build allowlist from existing Fragments in PreferenceGroup
203     @VisibleForTesting
generateFragmentAllowlist(@ullable PreferenceGroup prefs)204     Set<String> generateFragmentAllowlist(@Nullable PreferenceGroup prefs) {
205         Set<String> fragmentAllowList = new ArraySet<>();
206         if (prefs == null) {
207             return fragmentAllowList;
208         }
209 
210         for (int i = 0; i < prefs.getPreferenceCount(); i++) {
211             Preference pref = prefs.getPreference(i);
212             if (pref instanceof PreferenceGroup) {
213                 fragmentAllowList.addAll(generateFragmentAllowlist((PreferenceGroup) pref));
214             }
215 
216             String fragmentName = pref.getFragment();
217             if (!TextUtils.isEmpty(fragmentName)) {
218                 fragmentAllowList.add(fragmentName);
219             }
220         }
221         return fragmentAllowList;
222     }
223 
224     // Block clicks on any Preference with android:fragment that is not contained in the allowlist
225     @VisibleForTesting
filterBlockedFragments(@ullable PreferenceGroup prefs, @NonNull Set<String> allowedFragments)226     void filterBlockedFragments(@Nullable PreferenceGroup prefs,
227             @NonNull Set<String> allowedFragments) {
228         if (prefs == null) {
229             return;
230         }
231         for (int i = 0; i < prefs.getPreferenceCount(); i++) {
232             Preference pref = prefs.getPreference(i);
233             if (pref instanceof PreferenceGroup) {
234                 filterBlockedFragments((PreferenceGroup) pref, allowedFragments);
235             }
236 
237             String fragmentName = pref.getFragment();
238             if (fragmentName != null && !allowedFragments.contains(fragmentName)) {
239                 pref.setOnPreferenceClickListener(preference -> true);
240             }
241         }
242     }
243 
244     /**
245      * Determines if the supplied Intent is safe. A safe intent is one that is
246      * will launch a exported=true activity or owned by the same uid as the
247      * authenticator supplying the intent.
248      */
isSafeIntent(PackageManager pm, Intent intent, String acccountType)249     private boolean isSafeIntent(PackageManager pm, Intent intent, String acccountType) {
250         AuthenticatorDescription authDesc =
251             mAuthenticatorHelper.getAccountTypeDescription(acccountType);
252         ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mUserHandle.getIdentifier());
253         if (resolveInfo == null) {
254             return false;
255         }
256         ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo;
257         ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo;
258         try {
259             // Allows to launch only authenticator owned activities.
260             ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0);
261             return resolvedAppInfo.uid == authenticatorAppInf.uid;
262         } catch (NameNotFoundException e) {
263             Log.e(TAG,
264                 "Intent considered unsafe due to exception.",
265                 e);
266             return false;
267         }
268     }
269 
270     /** Listens to a preference click event and starts a fragment */
271     private class FragmentStarter
272         implements Preference.OnPreferenceClickListener {
273         private final String mClass;
274         private final int mTitleRes;
275 
276         /**
277          * @param className the class name of the fragment to be started.
278          * @param title the title resource id of the started preference panel.
279          */
FragmentStarter(String className, int title)280         public FragmentStarter(String className, int title) {
281             mClass = className;
282             mTitleRes = title;
283         }
284 
285         @Override
onPreferenceClick(Preference preference)286         public boolean onPreferenceClick(Preference preference) {
287             final int metricsCategory = (mFragment instanceof Instrumentable)
288                     ? ((Instrumentable) mFragment).getMetricsCategory()
289                     : Instrumentable.METRICS_CATEGORY_UNKNOWN;
290             new SubSettingLauncher(preference.getContext())
291                     .setTitleRes(mTitleRes)
292                     .setDestination(mClass)
293                     .setSourceMetricsCategory(metricsCategory)
294                     .launch();
295 
296             // Hack: announce that the Google account preferences page is launching the location
297             // settings
298             if (mClass.equals(LocationSettings.class.getName())) {
299                 Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS);
300                 mFragment.getActivity().sendBroadcast(
301                     intent, android.Manifest.permission.WRITE_SECURE_SETTINGS);
302             }
303             return true;
304         }
305     }
306 
307 }
308