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.support.v14.preference.PreferenceFragment; 33 import android.support.v7.preference.Preference; 34 import android.support.v7.preference.Preference.OnPreferenceClickListener; 35 import android.support.v7.preference.PreferenceGroup; 36 import android.support.v7.preference.PreferenceScreen; 37 import android.text.TextUtils; 38 import android.util.Log; 39 40 import com.android.settings.R; 41 import com.android.settings.core.SubSettingLauncher; 42 import com.android.settings.location.LocationSettings; 43 import com.android.settings.utils.LocalClassLoaderContextThemeWrapper; 44 import com.android.settingslib.accounts.AuthenticatorHelper; 45 import com.android.settingslib.core.instrumentation.Instrumentable; 46 47 /** 48 * Class to load the preference screen to be added to the settings page for the specific account 49 * type as specified in the account-authenticator. 50 */ 51 public class AccountTypePreferenceLoader { 52 53 private static final String TAG = "AccountTypePrefLoader"; 54 private static final String ACCOUNT_KEY = "account"; // to pass to auth settings 55 // Action name for the broadcast intent when the Google account preferences page is launching 56 // the location settings. 57 private static final String LAUNCHING_LOCATION_SETTINGS = 58 "com.android.settings.accounts.LAUNCHING_LOCATION_SETTINGS"; 59 60 61 private AuthenticatorHelper mAuthenticatorHelper; 62 private UserHandle mUserHandle; 63 private PreferenceFragment mFragment; 64 65 public AccountTypePreferenceLoader(PreferenceFragment 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 */ 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 */ 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 */ 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 if (resolvedActivityInfo.exported) { 200 if (resolvedActivityInfo.permission == null) { 201 return true; // exported activity without permission. 202 } else if (pm.checkPermission(resolvedActivityInfo.permission, 203 authDesc.packageName) == PackageManager.PERMISSION_GRANTED) { 204 return true; 205 } 206 } 207 ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0); 208 return resolvedAppInfo.uid == authenticatorAppInf.uid; 209 } catch (NameNotFoundException e) { 210 Log.e(TAG, 211 "Intent considered unsafe due to exception.", 212 e); 213 return false; 214 } 215 } 216 217 /** Listens to a preference click event and starts a fragment */ 218 private class FragmentStarter 219 implements Preference.OnPreferenceClickListener { 220 private final String mClass; 221 private final int mTitleRes; 222 223 /** 224 * @param className the class name of the fragment to be started. 225 * @param title the title resource id of the started preference panel. 226 */ 227 public FragmentStarter(String className, int title) { 228 mClass = className; 229 mTitleRes = title; 230 } 231 232 @Override 233 public boolean onPreferenceClick(Preference preference) { 234 final int metricsCategory = (mFragment instanceof Instrumentable) 235 ? ((Instrumentable) mFragment).getMetricsCategory() 236 : Instrumentable.METRICS_CATEGORY_UNKNOWN; 237 new SubSettingLauncher(preference.getContext()) 238 .setTitle(mTitleRes) 239 .setDestination(mClass) 240 .setSourceMetricsCategory(metricsCategory) 241 .launch(); 242 243 // Hack: announce that the Google account preferences page is launching the location 244 // settings 245 if (mClass.equals(LocationSettings.class.getName())) { 246 Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS); 247 mFragment.getActivity().sendBroadcast( 248 intent, android.Manifest.permission.WRITE_SECURE_SETTINGS); 249 } 250 return true; 251 } 252 } 253 254 } 255