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