/* * Copyright (C) 2014 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.server; import android.Manifest.permission; import android.annotation.Nullable; import android.app.AppOpsManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.net.NetworkScoreManager; import android.net.NetworkScorerAppData; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Internal class for discovering and managing the network scorer/recommendation application. * * @hide */ @VisibleForTesting public class NetworkScorerAppManager { private static final String TAG = "NetworkScorerAppManager"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); private final Context mContext; private final SettingsFacade mSettingsFacade; public NetworkScorerAppManager(Context context) { this(context, new SettingsFacade()); } @VisibleForTesting public NetworkScorerAppManager(Context context, SettingsFacade settingsFacade) { mContext = context; mSettingsFacade = settingsFacade; } /** * Returns the list of available scorer apps. The list will be empty if there are * no valid scorers. */ @VisibleForTesting public List<NetworkScorerAppData> getAllValidScorers() { if (VERBOSE) Log.v(TAG, "getAllValidScorers()"); final PackageManager pm = mContext.getPackageManager(); final Intent serviceIntent = new Intent(NetworkScoreManager.ACTION_RECOMMEND_NETWORKS); final List<ResolveInfo> resolveInfos = pm.queryIntentServices(serviceIntent, PackageManager.GET_META_DATA); if (resolveInfos == null || resolveInfos.isEmpty()) { if (DEBUG) Log.d(TAG, "Found 0 Services able to handle " + serviceIntent); return Collections.emptyList(); } List<NetworkScorerAppData> appDataList = new ArrayList<>(); for (int i = 0; i < resolveInfos.size(); i++) { final ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo; if (hasPermissions(serviceInfo.applicationInfo.uid, serviceInfo.packageName)) { if (VERBOSE) { Log.v(TAG, serviceInfo.packageName + " is a valid scorer/recommender."); } final ComponentName serviceComponentName = new ComponentName(serviceInfo.packageName, serviceInfo.name); final String serviceLabel = getRecommendationServiceLabel(serviceInfo, pm); final ComponentName useOpenWifiNetworksActivity = findUseOpenWifiNetworksActivity(serviceInfo); final String networkAvailableNotificationChannelId = getNetworkAvailableNotificationChannelId(serviceInfo); appDataList.add( new NetworkScorerAppData(serviceInfo.applicationInfo.uid, serviceComponentName, serviceLabel, useOpenWifiNetworksActivity, networkAvailableNotificationChannelId)); } else { if (VERBOSE) Log.v(TAG, serviceInfo.packageName + " is NOT a valid scorer/recommender."); } } return appDataList; } @Nullable private String getRecommendationServiceLabel(ServiceInfo serviceInfo, PackageManager pm) { if (serviceInfo.metaData != null) { final String label = serviceInfo.metaData .getString(NetworkScoreManager.RECOMMENDATION_SERVICE_LABEL_META_DATA); if (!TextUtils.isEmpty(label)) { return label; } } CharSequence label = serviceInfo.loadLabel(pm); return label == null ? null : label.toString(); } @Nullable private ComponentName findUseOpenWifiNetworksActivity(ServiceInfo serviceInfo) { if (serviceInfo.metaData == null) { if (DEBUG) { Log.d(TAG, "No metadata found on " + serviceInfo.getComponentName()); } return null; } final String useOpenWifiPackage = serviceInfo.metaData .getString(NetworkScoreManager.USE_OPEN_WIFI_PACKAGE_META_DATA); if (TextUtils.isEmpty(useOpenWifiPackage)) { if (DEBUG) { Log.d(TAG, "No use_open_wifi_package metadata found on " + serviceInfo.getComponentName()); } return null; } final Intent enableUseOpenWifiIntent = new Intent(NetworkScoreManager.ACTION_CUSTOM_ENABLE) .setPackage(useOpenWifiPackage); final ResolveInfo resolveActivityInfo = mContext.getPackageManager() .resolveActivity(enableUseOpenWifiIntent, 0 /* flags */); if (VERBOSE) { Log.d(TAG, "Resolved " + enableUseOpenWifiIntent + " to " + resolveActivityInfo); } if (resolveActivityInfo != null && resolveActivityInfo.activityInfo != null) { return resolveActivityInfo.activityInfo.getComponentName(); } return null; } @Nullable private static String getNetworkAvailableNotificationChannelId(ServiceInfo serviceInfo) { if (serviceInfo.metaData == null) { if (DEBUG) { Log.d(TAG, "No metadata found on " + serviceInfo.getComponentName()); } return null; } return serviceInfo.metaData.getString( NetworkScoreManager.NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID_META_DATA); } /** * Get the application to use for scoring networks. * * @return the scorer app info or null if scoring is disabled (including if no scorer was ever * selected) or if the previously-set scorer is no longer a valid scorer app (e.g. because * it was disabled or uninstalled). */ @Nullable @VisibleForTesting public NetworkScorerAppData getActiveScorer() { final int enabledSetting = getNetworkRecommendationsEnabledSetting(); if (enabledSetting == NetworkScoreManager.RECOMMENDATIONS_ENABLED_FORCED_OFF) { return null; } return getScorer(getNetworkRecommendationsPackage()); } private NetworkScorerAppData getScorer(String packageName) { if (TextUtils.isEmpty(packageName)) { return null; } // Otherwise return the recommendation provider (which may be null). List<NetworkScorerAppData> apps = getAllValidScorers(); for (int i = 0; i < apps.size(); i++) { NetworkScorerAppData app = apps.get(i); if (app.getRecommendationServicePackageName().equals(packageName)) { return app; } } return null; } private boolean hasPermissions(final int uid, final String packageName) { return hasScoreNetworksPermission(packageName) && canAccessLocation(uid, packageName); } private boolean hasScoreNetworksPermission(String packageName) { final PackageManager pm = mContext.getPackageManager(); return pm.checkPermission(permission.SCORE_NETWORKS, packageName) == PackageManager.PERMISSION_GRANTED; } private boolean canAccessLocation(int uid, String packageName) { final PackageManager pm = mContext.getPackageManager(); final AppOpsManager appOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); return isLocationModeEnabled() && pm.checkPermission(permission.ACCESS_COARSE_LOCATION, packageName) == PackageManager.PERMISSION_GRANTED && appOpsManager.noteOp(AppOpsManager.OP_COARSE_LOCATION, uid, packageName) == AppOpsManager.MODE_ALLOWED; } private boolean isLocationModeEnabled() { return mSettingsFacade.getSecureInt(mContext, Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF) != Settings.Secure.LOCATION_MODE_OFF; } /** * Set the specified package as the default scorer application. * * <p>The caller must have permission to write to {@link Settings.Global}. * * @param packageName the packageName of the new scorer to use. If null, scoring will be forced * off, otherwise the scorer will only be set if it is a valid scorer * application. * @return true if the package was a valid scorer (including <code>null</code>) and now * represents the active scorer, false otherwise. */ @VisibleForTesting public boolean setActiveScorer(String packageName) { final String oldPackageName = getNetworkRecommendationsPackage(); if (TextUtils.equals(oldPackageName, packageName)) { // No change. return true; } if (TextUtils.isEmpty(packageName)) { Log.i(TAG, "Network scorer forced off, was: " + oldPackageName); setNetworkRecommendationsPackage(null); setNetworkRecommendationsEnabledSetting( NetworkScoreManager.RECOMMENDATIONS_ENABLED_FORCED_OFF); return true; } // We only make the change if the new package is valid. if (getScorer(packageName) != null) { Log.i(TAG, "Changing network scorer from " + oldPackageName + " to " + packageName); setNetworkRecommendationsPackage(packageName); setNetworkRecommendationsEnabledSetting(NetworkScoreManager.RECOMMENDATIONS_ENABLED_ON); return true; } else { Log.w(TAG, "Requested network scorer is not valid: " + packageName); return false; } } /** * Ensures the {@link Settings.Global#NETWORK_RECOMMENDATIONS_PACKAGE} setting points to a valid * package and {@link Settings.Global#NETWORK_RECOMMENDATIONS_ENABLED} is consistent. * * If {@link Settings.Global#NETWORK_RECOMMENDATIONS_PACKAGE} doesn't point to a valid package * then it will be reverted to the default package specified by * {@link R.string#config_defaultNetworkRecommendationProviderPackage}. If the default package * is no longer valid then {@link Settings.Global#NETWORK_RECOMMENDATIONS_ENABLED} will be set * to <code>0</code> (disabled). */ @VisibleForTesting public void updateState() { final int enabledSetting = getNetworkRecommendationsEnabledSetting(); if (enabledSetting == NetworkScoreManager.RECOMMENDATIONS_ENABLED_FORCED_OFF) { // Don't change anything if it's forced off. if (DEBUG) Log.d(TAG, "Recommendations forced off."); return; } // First, see if the current package is still valid. If so, then we can exit early. final String currentPackageName = getNetworkRecommendationsPackage(); if (getScorer(currentPackageName) != null) { if (VERBOSE) Log.v(TAG, currentPackageName + " is the active scorer."); setNetworkRecommendationsEnabledSetting(NetworkScoreManager.RECOMMENDATIONS_ENABLED_ON); return; } int newEnabledSetting = NetworkScoreManager.RECOMMENDATIONS_ENABLED_OFF; // the active scorer isn't valid, revert to the default if it's different and valid final String defaultPackageName = getDefaultPackageSetting(); if (!TextUtils.equals(currentPackageName, defaultPackageName) && getScorer(defaultPackageName) != null) { if (DEBUG) { Log.d(TAG, "Defaulting the network recommendations app to: " + defaultPackageName); } setNetworkRecommendationsPackage(defaultPackageName); newEnabledSetting = NetworkScoreManager.RECOMMENDATIONS_ENABLED_ON; } setNetworkRecommendationsEnabledSetting(newEnabledSetting); } /** * Migrates the NETWORK_SCORER_APP Setting to the USE_OPEN_WIFI_PACKAGE Setting. */ @VisibleForTesting public void migrateNetworkScorerAppSettingIfNeeded() { final String scorerAppPkgNameSetting = mSettingsFacade.getString(mContext, Settings.Global.NETWORK_SCORER_APP); if (TextUtils.isEmpty(scorerAppPkgNameSetting)) { // Early exit, nothing to do. return; } final NetworkScorerAppData currentAppData = getActiveScorer(); if (currentAppData == null) { // Don't touch anything until we have an active scorer to work with. return; } if (DEBUG) { Log.d(TAG, "Migrating Settings.Global.NETWORK_SCORER_APP " + "(" + scorerAppPkgNameSetting + ")..."); } // If the new (useOpenWifi) Setting isn't set and the old Setting's value matches the // new metadata value then update the new Setting with the old value. Otherwise it's a // mismatch so we shouldn't enable the Setting automatically. final ComponentName enableUseOpenWifiActivity = currentAppData.getEnableUseOpenWifiActivity(); final String useOpenWifiSetting = mSettingsFacade.getString(mContext, Settings.Global.USE_OPEN_WIFI_PACKAGE); if (TextUtils.isEmpty(useOpenWifiSetting) && enableUseOpenWifiActivity != null && scorerAppPkgNameSetting.equals(enableUseOpenWifiActivity.getPackageName())) { mSettingsFacade.putString(mContext, Settings.Global.USE_OPEN_WIFI_PACKAGE, scorerAppPkgNameSetting); if (DEBUG) { Log.d(TAG, "Settings.Global.USE_OPEN_WIFI_PACKAGE set to " + "'" + scorerAppPkgNameSetting + "'."); } } // Clear out the old setting so we don't run through the migration code again. mSettingsFacade.putString(mContext, Settings.Global.NETWORK_SCORER_APP, null); if (DEBUG) { Log.d(TAG, "Settings.Global.NETWORK_SCORER_APP migration complete."); final String setting = mSettingsFacade.getString(mContext, Settings.Global.USE_OPEN_WIFI_PACKAGE); Log.d(TAG, "Settings.Global.USE_OPEN_WIFI_PACKAGE is: '" + setting + "'."); } } private String getDefaultPackageSetting() { return mContext.getResources().getString( R.string.config_defaultNetworkRecommendationProviderPackage); } private String getNetworkRecommendationsPackage() { return mSettingsFacade.getString(mContext, Settings.Global.NETWORK_RECOMMENDATIONS_PACKAGE); } private void setNetworkRecommendationsPackage(String packageName) { mSettingsFacade.putString(mContext, Settings.Global.NETWORK_RECOMMENDATIONS_PACKAGE, packageName); if (VERBOSE) { Log.d(TAG, Settings.Global.NETWORK_RECOMMENDATIONS_PACKAGE + " set to " + packageName); } } private int getNetworkRecommendationsEnabledSetting() { return mSettingsFacade.getInt(mContext, Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, 0); } private void setNetworkRecommendationsEnabledSetting(int value) { mSettingsFacade.putInt(mContext, Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, value); if (VERBOSE) { Log.d(TAG, Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED + " set to " + value); } } /** * Wrapper around Settings to make testing easier. */ public static class SettingsFacade { public boolean putString(Context context, String name, String value) { return Settings.Global.putString(context.getContentResolver(), name, value); } public String getString(Context context, String name) { return Settings.Global.getString(context.getContentResolver(), name); } public boolean putInt(Context context, String name, int value) { return Settings.Global.putInt(context.getContentResolver(), name, value); } public int getInt(Context context, String name, int defaultValue) { return Settings.Global.getInt(context.getContentResolver(), name, defaultValue); } public int getSecureInt(Context context, String name, int defaultValue) { return Settings.Secure.getInt(context.getContentResolver(), name, defaultValue); } } }