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