Doris Ling | 13ac884 | 2017-02-10 14:22:58 -0800 | [diff] [blame] | 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; |
Carsten Hauge | f35cb6b | 2014-07-07 09:36:07 +0200 | [diff] [blame] | 32 | import android.text.TextUtils; |
Doris Ling | 13ac884 | 2017-02-10 14:22:58 -0800 | [diff] [blame] | 33 | import android.util.Log; |
| 34 | |
Fan Zhang | 23f8d59 | 2018-08-28 15:11:40 -0700 | [diff] [blame] | 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 | |
Doris Ling | 13ac884 | 2017-02-10 14:22:58 -0800 | [diff] [blame] | 41 | import com.android.settings.R; |
Fan Zhang | 7cf99f5 | 2018-02-16 10:37:37 -0800 | [diff] [blame] | 42 | import com.android.settings.core.SubSettingLauncher; |
Doris Ling | 13ac884 | 2017-02-10 14:22:58 -0800 | [diff] [blame] | 43 | import com.android.settings.location.LocationSettings; |
| 44 | import com.android.settings.utils.LocalClassLoaderContextThemeWrapper; |
| 45 | import com.android.settingslib.accounts.AuthenticatorHelper; |
Fan Zhang | 7cf99f5 | 2018-02-16 10:37:37 -0800 | [diff] [blame] | 46 | import com.android.settingslib.core.instrumentation.Instrumentable; |
Doris Ling | 13ac884 | 2017-02-10 14:22:58 -0800 | [diff] [blame] | 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 | |
| 62 | private AuthenticatorHelper mAuthenticatorHelper; |
| 63 | private UserHandle mUserHandle; |
tmfang | 27c84de | 2018-06-28 11:39:05 +0800 | [diff] [blame] | 64 | private PreferenceFragmentCompat mFragment; |
Doris Ling | 13ac884 | 2017-02-10 14:22:58 -0800 | [diff] [blame] | 65 | |
tmfang | 27c84de | 2018-06-28 11:39:05 +0800 | [diff] [blame] | 66 | public AccountTypePreferenceLoader(PreferenceFragmentCompat fragment, |
Doris Ling | 13ac884 | 2017-02-10 14:22:58 -0800 | [diff] [blame] | 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 | */ |
| 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 | */ |
| 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. |
Carsten Hauge | f35cb6b | 2014-07-07 09:36:07 +0200 | [diff] [blame] | 141 | if (TextUtils.equals(intent.getAction(), |
| 142 | android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) { |
Doris Ling | 13ac884 | 2017-02-10 14:22:58 -0800 | [diff] [blame] | 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 | mFragment.getActivity().startActivityAsUser( |
| 169 | prefIntent, mUserHandle); |
| 170 | } else { |
| 171 | Log.e(TAG, |
| 172 | "Refusing to launch authenticator intent because" |
| 173 | + "it exploits Settings permissions: " |
| 174 | + prefIntent); |
| 175 | } |
| 176 | return true; |
| 177 | } |
| 178 | }); |
| 179 | } |
| 180 | } |
| 181 | i++; |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * Determines if the supplied Intent is safe. A safe intent is one that is |
| 187 | * will launch a exported=true activity or owned by the same uid as the |
| 188 | * authenticator supplying the intent. |
| 189 | */ |
| 190 | private boolean isSafeIntent(PackageManager pm, Intent intent, String acccountType) { |
| 191 | AuthenticatorDescription authDesc = |
| 192 | mAuthenticatorHelper.getAccountTypeDescription(acccountType); |
| 193 | ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mUserHandle.getIdentifier()); |
| 194 | if (resolveInfo == null) { |
| 195 | return false; |
| 196 | } |
| 197 | ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo; |
| 198 | ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo; |
| 199 | try { |
| 200 | if (resolvedActivityInfo.exported) { |
| 201 | if (resolvedActivityInfo.permission == null) { |
| 202 | return true; // exported activity without permission. |
| 203 | } else if (pm.checkPermission(resolvedActivityInfo.permission, |
| 204 | authDesc.packageName) == PackageManager.PERMISSION_GRANTED) { |
| 205 | return true; |
| 206 | } |
| 207 | } |
| 208 | ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0); |
| 209 | return resolvedAppInfo.uid == authenticatorAppInf.uid; |
| 210 | } catch (NameNotFoundException e) { |
| 211 | Log.e(TAG, |
| 212 | "Intent considered unsafe due to exception.", |
| 213 | e); |
| 214 | return false; |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | /** Listens to a preference click event and starts a fragment */ |
| 219 | private class FragmentStarter |
| 220 | implements Preference.OnPreferenceClickListener { |
| 221 | private final String mClass; |
| 222 | private final int mTitleRes; |
| 223 | |
| 224 | /** |
| 225 | * @param className the class name of the fragment to be started. |
| 226 | * @param title the title resource id of the started preference panel. |
| 227 | */ |
| 228 | public FragmentStarter(String className, int title) { |
| 229 | mClass = className; |
| 230 | mTitleRes = title; |
| 231 | } |
| 232 | |
| 233 | @Override |
| 234 | public boolean onPreferenceClick(Preference preference) { |
Fan Zhang | 7cf99f5 | 2018-02-16 10:37:37 -0800 | [diff] [blame] | 235 | final int metricsCategory = (mFragment instanceof Instrumentable) |
| 236 | ? ((Instrumentable) mFragment).getMetricsCategory() |
| 237 | : Instrumentable.METRICS_CATEGORY_UNKNOWN; |
| 238 | new SubSettingLauncher(preference.getContext()) |
hjchangliao | e86eec0 | 2018-05-02 13:01:07 +0800 | [diff] [blame] | 239 | .setTitleRes(mTitleRes) |
Fan Zhang | 7cf99f5 | 2018-02-16 10:37:37 -0800 | [diff] [blame] | 240 | .setDestination(mClass) |
| 241 | .setSourceMetricsCategory(metricsCategory) |
| 242 | .launch(); |
| 243 | |
Doris Ling | 13ac884 | 2017-02-10 14:22:58 -0800 | [diff] [blame] | 244 | // Hack: announce that the Google account preferences page is launching the location |
| 245 | // settings |
| 246 | if (mClass.equals(LocationSettings.class.getName())) { |
| 247 | Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS); |
| 248 | mFragment.getActivity().sendBroadcast( |
| 249 | intent, android.Manifest.permission.WRITE_SECURE_SETTINGS); |
| 250 | } |
| 251 | return true; |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | } |