| /* |
| |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.settings.accounts; |
| |
| import android.accounts.Account; |
| import android.accounts.AuthenticatorDescription; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ActivityInfo; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.content.pm.ResolveInfo; |
| import android.content.res.Resources; |
| import android.content.res.Resources.Theme; |
| import android.os.UserHandle; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import androidx.preference.Preference; |
| import androidx.preference.Preference.OnPreferenceClickListener; |
| import androidx.preference.PreferenceFragmentCompat; |
| import androidx.preference.PreferenceGroup; |
| import androidx.preference.PreferenceScreen; |
| |
| import com.android.settings.R; |
| import com.android.settings.core.SubSettingLauncher; |
| import com.android.settings.location.LocationSettings; |
| import com.android.settings.utils.LocalClassLoaderContextThemeWrapper; |
| import com.android.settingslib.accounts.AuthenticatorHelper; |
| import com.android.settingslib.core.instrumentation.Instrumentable; |
| |
| /** |
| * Class to load the preference screen to be added to the settings page for the specific account |
| * type as specified in the account-authenticator. |
| */ |
| public class AccountTypePreferenceLoader { |
| |
| private static final String TAG = "AccountTypePrefLoader"; |
| private static final String ACCOUNT_KEY = "account"; // to pass to auth settings |
| // Action name for the broadcast intent when the Google account preferences page is launching |
| // the location settings. |
| private static final String LAUNCHING_LOCATION_SETTINGS = |
| "com.android.settings.accounts.LAUNCHING_LOCATION_SETTINGS"; |
| |
| |
| private AuthenticatorHelper mAuthenticatorHelper; |
| private UserHandle mUserHandle; |
| private PreferenceFragmentCompat mFragment; |
| |
| public AccountTypePreferenceLoader(PreferenceFragmentCompat fragment, |
| AuthenticatorHelper authenticatorHelper, UserHandle userHandle) { |
| mFragment = fragment; |
| mAuthenticatorHelper = authenticatorHelper; |
| mUserHandle = userHandle; |
| } |
| |
| /** |
| * Gets the preferences.xml file associated with a particular account type. |
| * @param accountType the type of account |
| * @return a PreferenceScreen inflated from accountPreferenceId. |
| */ |
| public PreferenceScreen addPreferencesForType(final String accountType, |
| PreferenceScreen parent) { |
| PreferenceScreen prefs = null; |
| if (mAuthenticatorHelper.containsAccountType(accountType)) { |
| AuthenticatorDescription desc = null; |
| try { |
| desc = mAuthenticatorHelper.getAccountTypeDescription(accountType); |
| if (desc != null && desc.accountPreferencesId != 0) { |
| // Load the context of the target package, then apply the |
| // base Settings theme (no references to local resources) |
| // and create a context theme wrapper so that we get the |
| // correct text colors. Control colors will still be wrong, |
| // but there's not much we can do about it since we can't |
| // reference local color resources. |
| final Context targetCtx = mFragment.getActivity().createPackageContextAsUser( |
| desc.packageName, 0, mUserHandle); |
| final Theme baseTheme = mFragment.getResources().newTheme(); |
| baseTheme.applyStyle(R.style.Theme_SettingsBase, true); |
| final Context themedCtx = |
| new LocalClassLoaderContextThemeWrapper(getClass(), targetCtx, 0); |
| themedCtx.getTheme().setTo(baseTheme); |
| prefs = mFragment.getPreferenceManager().inflateFromResource(themedCtx, |
| desc.accountPreferencesId, parent); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); |
| } catch (Resources.NotFoundException e) { |
| Log.w(TAG, "Couldn't load preferences.xml file from " + desc.packageName); |
| } |
| } |
| return prefs; |
| } |
| |
| /** |
| * Recursively filters through the preference list provided by GoogleLoginService. |
| * |
| * This method removes all the invalid intent from the list, adds account name as extra into the |
| * intent, and hack the location settings to start it as a fragment. |
| */ |
| public void updatePreferenceIntents(PreferenceGroup prefs, final String acccountType, |
| Account account) { |
| final PackageManager pm = mFragment.getActivity().getPackageManager(); |
| for (int i = 0; i < prefs.getPreferenceCount(); ) { |
| Preference pref = prefs.getPreference(i); |
| if (pref instanceof PreferenceGroup) { |
| updatePreferenceIntents((PreferenceGroup) pref, acccountType, account); |
| } |
| Intent intent = pref.getIntent(); |
| if (intent != null) { |
| // Hack. Launch "Location" as fragment instead of as activity. |
| // |
| // When "Location" is launched as activity via Intent, there's no "Up" button at the |
| // top left, and if there's another running instance of "Location" activity, the |
| // back stack would usually point to some other place so the user won't be able to |
| // go back to the previous page by "back" key. Using fragment is a much easier |
| // solution to those problems. |
| // |
| // If we set Intent to null and assign a fragment to the PreferenceScreen item here, |
| // in order to make it work as expected, we still need to modify the container |
| // PreferenceActivity, override onPreferenceStartFragment() and call |
| // startPreferencePanel() there. In order to inject the title string there, more |
| // dirty further hack is still needed. It's much easier and cleaner to listen to |
| // preference click event here directly. |
| if (TextUtils.equals(intent.getAction(), |
| android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) { |
| // The OnPreferenceClickListener overrides the click event completely. No intent |
| // will get fired. |
| pref.setOnPreferenceClickListener(new FragmentStarter( |
| LocationSettings.class.getName(), R.string.location_settings_title)); |
| } else { |
| ResolveInfo ri = pm.resolveActivityAsUser(intent, |
| PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier()); |
| if (ri == null) { |
| prefs.removePreference(pref); |
| continue; |
| } |
| intent.putExtra(ACCOUNT_KEY, account); |
| intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); |
| pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { |
| @Override |
| public boolean onPreferenceClick(Preference preference) { |
| Intent prefIntent = preference.getIntent(); |
| /* |
| * Check the intent to see if it resolves to a exported=false |
| * activity that doesn't share a uid with the authenticator. |
| * |
| * Otherwise the intent is considered unsafe in that it will be |
| * exploiting the fact that settings has system privileges. |
| */ |
| if (isSafeIntent(pm, prefIntent, acccountType)) { |
| mFragment.getActivity().startActivityAsUser( |
| prefIntent, mUserHandle); |
| } else { |
| Log.e(TAG, |
| "Refusing to launch authenticator intent because" |
| + "it exploits Settings permissions: " |
| + prefIntent); |
| } |
| return true; |
| } |
| }); |
| } |
| } |
| i++; |
| } |
| } |
| |
| /** |
| * Determines if the supplied Intent is safe. A safe intent is one that is |
| * will launch a exported=true activity or owned by the same uid as the |
| * authenticator supplying the intent. |
| */ |
| private boolean isSafeIntent(PackageManager pm, Intent intent, String acccountType) { |
| AuthenticatorDescription authDesc = |
| mAuthenticatorHelper.getAccountTypeDescription(acccountType); |
| ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mUserHandle.getIdentifier()); |
| if (resolveInfo == null) { |
| return false; |
| } |
| ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo; |
| ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo; |
| try { |
| // Allows to launch only authenticator owned activities. |
| ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0); |
| return resolvedAppInfo.uid == authenticatorAppInf.uid; |
| } catch (NameNotFoundException e) { |
| Log.e(TAG, |
| "Intent considered unsafe due to exception.", |
| e); |
| return false; |
| } |
| } |
| |
| /** Listens to a preference click event and starts a fragment */ |
| private class FragmentStarter |
| implements Preference.OnPreferenceClickListener { |
| private final String mClass; |
| private final int mTitleRes; |
| |
| /** |
| * @param className the class name of the fragment to be started. |
| * @param title the title resource id of the started preference panel. |
| */ |
| public FragmentStarter(String className, int title) { |
| mClass = className; |
| mTitleRes = title; |
| } |
| |
| @Override |
| public boolean onPreferenceClick(Preference preference) { |
| final int metricsCategory = (mFragment instanceof Instrumentable) |
| ? ((Instrumentable) mFragment).getMetricsCategory() |
| : Instrumentable.METRICS_CATEGORY_UNKNOWN; |
| new SubSettingLauncher(preference.getContext()) |
| .setTitleRes(mTitleRes) |
| .setDestination(mClass) |
| .setSourceMetricsCategory(metricsCategory) |
| .launch(); |
| |
| // Hack: announce that the Google account preferences page is launching the location |
| // settings |
| if (mClass.equals(LocationSettings.class.getName())) { |
| Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS); |
| mFragment.getActivity().sendBroadcast( |
| intent, android.Manifest.permission.WRITE_SECURE_SETTINGS); |
| } |
| return true; |
| } |
| } |
| |
| } |