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