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.preference.Preference; 37 import androidx.preference.Preference.OnPreferenceClickListener; 38 import androidx.preference.PreferenceFragmentCompat; 39 import androidx.preference.PreferenceGroup; 40 import androidx.preference.PreferenceScreen; 41 42 import com.android.settings.R; 43 import com.android.settings.core.SubSettingLauncher; 44 import com.android.settings.location.LocationSettings; 45 import com.android.settings.utils.LocalClassLoaderContextThemeWrapper; 46 import com.android.settingslib.accounts.AuthenticatorHelper; 47 import com.android.settingslib.core.instrumentation.Instrumentable; 48 49 /** 50 * Class to load the preference screen to be added to the settings page for the specific account 51 * type as specified in the account-authenticator. 52 */ 53 public class AccountTypePreferenceLoader { 54 55 private static final String TAG = "AccountTypePrefLoader"; 56 private static final String ACCOUNT_KEY = "account"; // to pass to auth settings 57 // Action name for the broadcast intent when the Google account preferences page is launching 58 // the location settings. 59 private static final String LAUNCHING_LOCATION_SETTINGS = 60 "com.android.settings.accounts.LAUNCHING_LOCATION_SETTINGS"; 61 62 private AuthenticatorHelper mAuthenticatorHelper; 63 private UserHandle mUserHandle; 64 private PreferenceFragmentCompat mFragment; 65 AccountTypePreferenceLoader(PreferenceFragmentCompat fragment, AuthenticatorHelper authenticatorHelper, UserHandle userHandle)66 public AccountTypePreferenceLoader(PreferenceFragmentCompat fragment, 67 AuthenticatorHelper authenticatorHelper, UserHandle userHandle) { 68 mFragment = fragment; 69 mAuthenticatorHelper = authenticatorHelper; 70 mUserHandle = userHandle; 71 } 72 73 /** 74 * Gets the preferences.xml file associated with a particular account type. 75 * @param accountType the type of account 76 * @return a PreferenceScreen inflated from accountPreferenceId. 77 */ addPreferencesForType(final String accountType, PreferenceScreen parent)78 public PreferenceScreen addPreferencesForType(final String accountType, 79 PreferenceScreen parent) { 80 PreferenceScreen prefs = null; 81 if (mAuthenticatorHelper.containsAccountType(accountType)) { 82 AuthenticatorDescription desc = null; 83 try { 84 desc = mAuthenticatorHelper.getAccountTypeDescription(accountType); 85 if (desc != null && desc.accountPreferencesId != 0) { 86 // Load the context of the target package, then apply the 87 // base Settings theme (no references to local resources) 88 // and create a context theme wrapper so that we get the 89 // correct text colors. Control colors will still be wrong, 90 // but there's not much we can do about it since we can't 91 // reference local color resources. 92 final Context targetCtx = mFragment.getActivity().createPackageContextAsUser( 93 desc.packageName, 0, mUserHandle); 94 final Theme baseTheme = mFragment.getResources().newTheme(); 95 baseTheme.applyStyle(R.style.Theme_SettingsBase, true); 96 final Context themedCtx = 97 new LocalClassLoaderContextThemeWrapper(getClass(), targetCtx, 0); 98 themedCtx.getTheme().setTo(baseTheme); 99 prefs = mFragment.getPreferenceManager().inflateFromResource(themedCtx, 100 desc.accountPreferencesId, parent); 101 } 102 } catch (PackageManager.NameNotFoundException e) { 103 Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); 104 } catch (Resources.NotFoundException e) { 105 Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); 106 } 107 } 108 return prefs; 109 } 110 111 /** 112 * Recursively filters through the preference list provided by GoogleLoginService. 113 * 114 * This method removes all the invalid intent from the list, adds account name as extra into the 115 * intent, and hack the location settings to start it as a fragment. 116 */ updatePreferenceIntents(PreferenceGroup prefs, final String acccountType, Account account)117 public void updatePreferenceIntents(PreferenceGroup prefs, final String acccountType, 118 Account account) { 119 final PackageManager pm = mFragment.getActivity().getPackageManager(); 120 for (int i = 0; i < prefs.getPreferenceCount(); ) { 121 Preference pref = prefs.getPreference(i); 122 if (pref instanceof PreferenceGroup) { 123 updatePreferenceIntents((PreferenceGroup) pref, acccountType, account); 124 } 125 Intent intent = pref.getIntent(); 126 if (intent != null) { 127 // Hack. Launch "Location" as fragment instead of as activity. 128 // 129 // When "Location" is launched as activity via Intent, there's no "Up" button at the 130 // top left, and if there's another running instance of "Location" activity, the 131 // back stack would usually point to some other place so the user won't be able to 132 // go back to the previous page by "back" key. Using fragment is a much easier 133 // solution to those problems. 134 // 135 // If we set Intent to null and assign a fragment to the PreferenceScreen item here, 136 // in order to make it work as expected, we still need to modify the container 137 // PreferenceActivity, override onPreferenceStartFragment() and call 138 // startPreferencePanel() there. In order to inject the title string there, more 139 // dirty further hack is still needed. It's much easier and cleaner to listen to 140 // preference click event here directly. 141 if (TextUtils.equals(intent.getAction(), 142 android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) { 143 // The OnPreferenceClickListener overrides the click event completely. No intent 144 // will get fired. 145 pref.setOnPreferenceClickListener(new FragmentStarter( 146 LocationSettings.class.getName(), R.string.location_settings_title)); 147 } else { 148 ResolveInfo ri = pm.resolveActivityAsUser(intent, 149 PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier()); 150 if (ri == null) { 151 prefs.removePreference(pref); 152 continue; 153 } 154 intent.putExtra(ACCOUNT_KEY, account); 155 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); 156 pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { 157 @Override 158 public boolean onPreferenceClick(Preference preference) { 159 Intent prefIntent = preference.getIntent(); 160 /* 161 * Check the intent to see if it resolves to a exported=false 162 * activity that doesn't share a uid with the authenticator. 163 * 164 * Otherwise the intent is considered unsafe in that it will be 165 * exploiting the fact that settings has system privileges. 166 */ 167 if (isSafeIntent(pm, prefIntent, acccountType)) { 168 // Explicitly set an empty ClipData to ensure that we don't offer to 169 // promote any Uris contained inside for granting purposes 170 prefIntent.setClipData(ClipData.newPlainText(null, null)); 171 mFragment.getActivity().startActivityAsUser( 172 prefIntent, mUserHandle); 173 } else { 174 Log.e(TAG, 175 "Refusing to launch authenticator intent because" 176 + "it exploits Settings permissions: " 177 + prefIntent); 178 } 179 return true; 180 } 181 }); 182 } 183 } 184 i++; 185 } 186 } 187 188 /** 189 * Determines if the supplied Intent is safe. A safe intent is one that is 190 * will launch a exported=true activity or owned by the same uid as the 191 * authenticator supplying the intent. 192 */ isSafeIntent(PackageManager pm, Intent intent, String acccountType)193 private boolean isSafeIntent(PackageManager pm, Intent intent, String acccountType) { 194 AuthenticatorDescription authDesc = 195 mAuthenticatorHelper.getAccountTypeDescription(acccountType); 196 ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mUserHandle.getIdentifier()); 197 if (resolveInfo == null) { 198 return false; 199 } 200 ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo; 201 ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo; 202 try { 203 // Allows to launch only authenticator owned activities. 204 ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0); 205 return resolvedAppInfo.uid == authenticatorAppInf.uid; 206 } catch (NameNotFoundException e) { 207 Log.e(TAG, 208 "Intent considered unsafe due to exception.", 209 e); 210 return false; 211 } 212 } 213 214 /** Listens to a preference click event and starts a fragment */ 215 private class FragmentStarter 216 implements Preference.OnPreferenceClickListener { 217 private final String mClass; 218 private final int mTitleRes; 219 220 /** 221 * @param className the class name of the fragment to be started. 222 * @param title the title resource id of the started preference panel. 223 */ FragmentStarter(String className, int title)224 public FragmentStarter(String className, int title) { 225 mClass = className; 226 mTitleRes = title; 227 } 228 229 @Override onPreferenceClick(Preference preference)230 public boolean onPreferenceClick(Preference preference) { 231 final int metricsCategory = (mFragment instanceof Instrumentable) 232 ? ((Instrumentable) mFragment).getMetricsCategory() 233 : Instrumentable.METRICS_CATEGORY_UNKNOWN; 234 new SubSettingLauncher(preference.getContext()) 235 .setTitleRes(mTitleRes) 236 .setDestination(mClass) 237 .setSourceMetricsCategory(metricsCategory) 238 .launch(); 239 240 // Hack: announce that the Google account preferences page is launching the location 241 // settings 242 if (mClass.equals(LocationSettings.class.getName())) { 243 Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS); 244 mFragment.getActivity().sendBroadcast( 245 intent, android.Manifest.permission.WRITE_SECURE_SETTINGS); 246 } 247 return true; 248 } 249 } 250 251 } 252