1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.server; 18 19 import android.Manifest.permission; 20 import android.annotation.Nullable; 21 import android.content.ComponentName; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.PermissionChecker; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ResolveInfo; 28 import android.content.pm.ServiceInfo; 29 import android.net.NetworkScoreManager; 30 import android.net.NetworkScorerAppData; 31 import android.provider.Settings; 32 import android.text.TextUtils; 33 import android.util.Log; 34 35 import com.android.internal.R; 36 import com.android.internal.annotations.VisibleForTesting; 37 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.List; 41 42 /** 43 * Internal class for discovering and managing the network scorer/recommendation application. 44 * 45 * @hide 46 */ 47 @VisibleForTesting 48 public class NetworkScorerAppManager { 49 private static final String TAG = "NetworkScorerAppManager"; 50 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 51 private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); 52 private final Context mContext; 53 private final SettingsFacade mSettingsFacade; 54 NetworkScorerAppManager(Context context)55 public NetworkScorerAppManager(Context context) { 56 this(context, new SettingsFacade()); 57 } 58 59 @VisibleForTesting NetworkScorerAppManager(Context context, SettingsFacade settingsFacade)60 public NetworkScorerAppManager(Context context, SettingsFacade settingsFacade) { 61 mContext = context; 62 mSettingsFacade = settingsFacade; 63 } 64 65 /** 66 * Returns the list of available scorer apps. The list will be empty if there are 67 * no valid scorers. 68 */ 69 @VisibleForTesting getAllValidScorers()70 public List<NetworkScorerAppData> getAllValidScorers() { 71 if (VERBOSE) Log.v(TAG, "getAllValidScorers()"); 72 final PackageManager pm = mContext.getPackageManager(); 73 final Intent serviceIntent = new Intent(NetworkScoreManager.ACTION_RECOMMEND_NETWORKS); 74 final List<ResolveInfo> resolveInfos = 75 pm.queryIntentServices(serviceIntent, PackageManager.GET_META_DATA); 76 if (resolveInfos == null || resolveInfos.isEmpty()) { 77 if (DEBUG) Log.d(TAG, "Found 0 Services able to handle " + serviceIntent); 78 return Collections.emptyList(); 79 } 80 81 List<NetworkScorerAppData> appDataList = new ArrayList<>(); 82 for (int i = 0; i < resolveInfos.size(); i++) { 83 final ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo; 84 if (hasPermissions(serviceInfo.applicationInfo.uid, serviceInfo.packageName)) { 85 if (VERBOSE) { 86 Log.v(TAG, serviceInfo.packageName + " is a valid scorer/recommender."); 87 } 88 final ComponentName serviceComponentName = 89 new ComponentName(serviceInfo.packageName, serviceInfo.name); 90 final String serviceLabel = getRecommendationServiceLabel(serviceInfo, pm); 91 final ComponentName useOpenWifiNetworksActivity = 92 findUseOpenWifiNetworksActivity(serviceInfo); 93 final String networkAvailableNotificationChannelId = 94 getNetworkAvailableNotificationChannelId(serviceInfo); 95 appDataList.add( 96 new NetworkScorerAppData(serviceInfo.applicationInfo.uid, 97 serviceComponentName, serviceLabel, useOpenWifiNetworksActivity, 98 networkAvailableNotificationChannelId)); 99 } else { 100 if (VERBOSE) Log.v(TAG, serviceInfo.packageName 101 + " is NOT a valid scorer/recommender."); 102 } 103 } 104 105 return appDataList; 106 } 107 108 @Nullable getRecommendationServiceLabel(ServiceInfo serviceInfo, PackageManager pm)109 private String getRecommendationServiceLabel(ServiceInfo serviceInfo, PackageManager pm) { 110 if (serviceInfo.metaData != null) { 111 final String label = serviceInfo.metaData 112 .getString(NetworkScoreManager.RECOMMENDATION_SERVICE_LABEL_META_DATA); 113 if (!TextUtils.isEmpty(label)) { 114 return label; 115 } 116 } 117 CharSequence label = serviceInfo.loadLabel(pm); 118 return label == null ? null : label.toString(); 119 } 120 121 @Nullable findUseOpenWifiNetworksActivity(ServiceInfo serviceInfo)122 private ComponentName findUseOpenWifiNetworksActivity(ServiceInfo serviceInfo) { 123 if (serviceInfo.metaData == null) { 124 if (DEBUG) { 125 Log.d(TAG, "No metadata found on " + serviceInfo.getComponentName()); 126 } 127 return null; 128 } 129 final String useOpenWifiPackage = serviceInfo.metaData 130 .getString(NetworkScoreManager.USE_OPEN_WIFI_PACKAGE_META_DATA); 131 if (TextUtils.isEmpty(useOpenWifiPackage)) { 132 if (DEBUG) { 133 Log.d(TAG, "No use_open_wifi_package metadata found on " 134 + serviceInfo.getComponentName()); 135 } 136 return null; 137 } 138 final Intent enableUseOpenWifiIntent = new Intent(NetworkScoreManager.ACTION_CUSTOM_ENABLE) 139 .setPackage(useOpenWifiPackage); 140 final ResolveInfo resolveActivityInfo = mContext.getPackageManager() 141 .resolveActivity(enableUseOpenWifiIntent, 0 /* flags */); 142 if (VERBOSE) { 143 Log.d(TAG, "Resolved " + enableUseOpenWifiIntent + " to " + resolveActivityInfo); 144 } 145 146 if (resolveActivityInfo != null && resolveActivityInfo.activityInfo != null) { 147 return resolveActivityInfo.activityInfo.getComponentName(); 148 } 149 150 return null; 151 } 152 153 @Nullable getNetworkAvailableNotificationChannelId(ServiceInfo serviceInfo)154 private static String getNetworkAvailableNotificationChannelId(ServiceInfo serviceInfo) { 155 if (serviceInfo.metaData == null) { 156 if (DEBUG) { 157 Log.d(TAG, "No metadata found on " + serviceInfo.getComponentName()); 158 } 159 return null; 160 } 161 162 return serviceInfo.metaData.getString( 163 NetworkScoreManager.NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID_META_DATA); 164 } 165 166 167 /** 168 * Get the application to use for scoring networks. 169 * 170 * @return the scorer app info or null if scoring is disabled (including if no scorer was ever 171 * selected) or if the previously-set scorer is no longer a valid scorer app (e.g. because 172 * it was disabled or uninstalled). 173 */ 174 @Nullable 175 @VisibleForTesting getActiveScorer()176 public NetworkScorerAppData getActiveScorer() { 177 final int enabledSetting = getNetworkRecommendationsEnabledSetting(); 178 if (enabledSetting == NetworkScoreManager.RECOMMENDATIONS_ENABLED_FORCED_OFF) { 179 return null; 180 } 181 182 return getScorer(getNetworkRecommendationsPackage()); 183 } 184 getScorer(String packageName)185 private NetworkScorerAppData getScorer(String packageName) { 186 if (TextUtils.isEmpty(packageName)) { 187 return null; 188 } 189 190 // Otherwise return the recommendation provider (which may be null). 191 List<NetworkScorerAppData> apps = getAllValidScorers(); 192 for (int i = 0; i < apps.size(); i++) { 193 NetworkScorerAppData app = apps.get(i); 194 if (app.getRecommendationServicePackageName().equals(packageName)) { 195 return app; 196 } 197 } 198 199 return null; 200 } 201 hasPermissions(final int uid, final String packageName)202 private boolean hasPermissions(final int uid, final String packageName) { 203 return hasScoreNetworksPermission(packageName) 204 && canAccessLocation(uid, packageName); 205 } 206 hasScoreNetworksPermission(String packageName)207 private boolean hasScoreNetworksPermission(String packageName) { 208 final PackageManager pm = mContext.getPackageManager(); 209 return pm.checkPermission(permission.SCORE_NETWORKS, packageName) 210 == PackageManager.PERMISSION_GRANTED; 211 } 212 canAccessLocation(int uid, String packageName)213 private boolean canAccessLocation(int uid, String packageName) { 214 return isLocationModeEnabled() && PermissionChecker.checkPermissionForPreflight(mContext, 215 permission.ACCESS_COARSE_LOCATION, PermissionChecker.PID_UNKNOWN, uid, packageName) 216 == PermissionChecker.PERMISSION_GRANTED; 217 } 218 isLocationModeEnabled()219 private boolean isLocationModeEnabled() { 220 return mSettingsFacade.getSecureInt(mContext, Settings.Secure.LOCATION_MODE, 221 Settings.Secure.LOCATION_MODE_OFF) != Settings.Secure.LOCATION_MODE_OFF; 222 } 223 224 /** 225 * Set the specified package as the default scorer application. 226 * 227 * <p>The caller must have permission to write to {@link Settings.Global}. 228 * 229 * @param packageName the packageName of the new scorer to use. If null, scoring will be forced 230 * off, otherwise the scorer will only be set if it is a valid scorer 231 * application. 232 * @return true if the package was a valid scorer (including <code>null</code>) and now 233 * represents the active scorer, false otherwise. 234 */ 235 @VisibleForTesting setActiveScorer(String packageName)236 public boolean setActiveScorer(String packageName) { 237 final String oldPackageName = getNetworkRecommendationsPackage(); 238 239 if (TextUtils.equals(oldPackageName, packageName)) { 240 // No change. 241 return true; 242 } 243 244 if (TextUtils.isEmpty(packageName)) { 245 Log.i(TAG, "Network scorer forced off, was: " + oldPackageName); 246 setNetworkRecommendationsPackage(null); 247 setNetworkRecommendationsEnabledSetting( 248 NetworkScoreManager.RECOMMENDATIONS_ENABLED_FORCED_OFF); 249 return true; 250 } 251 252 // We only make the change if the new package is valid. 253 if (getScorer(packageName) != null) { 254 Log.i(TAG, "Changing network scorer from " + oldPackageName + " to " + packageName); 255 setNetworkRecommendationsPackage(packageName); 256 setNetworkRecommendationsEnabledSetting(NetworkScoreManager.RECOMMENDATIONS_ENABLED_ON); 257 return true; 258 } else { 259 Log.w(TAG, "Requested network scorer is not valid: " + packageName); 260 return false; 261 } 262 } 263 264 /** 265 * Ensures the {@link Settings.Global#NETWORK_RECOMMENDATIONS_PACKAGE} setting points to a valid 266 * package and {@link Settings.Global#NETWORK_RECOMMENDATIONS_ENABLED} is consistent. 267 * 268 * If {@link Settings.Global#NETWORK_RECOMMENDATIONS_PACKAGE} doesn't point to a valid package 269 * then it will be reverted to the default package specified by 270 * {@link R.string#config_defaultNetworkRecommendationProviderPackage}. If the default package 271 * is no longer valid then {@link Settings.Global#NETWORK_RECOMMENDATIONS_ENABLED} will be set 272 * to <code>0</code> (disabled). 273 */ 274 @VisibleForTesting updateState()275 public void updateState() { 276 final int enabledSetting = getNetworkRecommendationsEnabledSetting(); 277 if (enabledSetting == NetworkScoreManager.RECOMMENDATIONS_ENABLED_FORCED_OFF) { 278 // Don't change anything if it's forced off. 279 if (DEBUG) Log.d(TAG, "Recommendations forced off."); 280 return; 281 } 282 283 // First, see if the current package is still valid. If so, then we can exit early. 284 final String currentPackageName = getNetworkRecommendationsPackage(); 285 if (getScorer(currentPackageName) != null) { 286 if (VERBOSE) Log.v(TAG, currentPackageName + " is the active scorer."); 287 setNetworkRecommendationsEnabledSetting(NetworkScoreManager.RECOMMENDATIONS_ENABLED_ON); 288 return; 289 } 290 291 int newEnabledSetting = NetworkScoreManager.RECOMMENDATIONS_ENABLED_OFF; 292 // the active scorer isn't valid, revert to the default if it's different and valid 293 final String defaultPackageName = getDefaultPackageSetting(); 294 if (!TextUtils.equals(currentPackageName, defaultPackageName) 295 && getScorer(defaultPackageName) != null) { 296 if (DEBUG) { 297 Log.d(TAG, "Defaulting the network recommendations app to: " 298 + defaultPackageName); 299 } 300 setNetworkRecommendationsPackage(defaultPackageName); 301 newEnabledSetting = NetworkScoreManager.RECOMMENDATIONS_ENABLED_ON; 302 } 303 304 setNetworkRecommendationsEnabledSetting(newEnabledSetting); 305 } 306 307 /** 308 * Migrates the NETWORK_SCORER_APP Setting to the USE_OPEN_WIFI_PACKAGE Setting. 309 */ 310 @VisibleForTesting migrateNetworkScorerAppSettingIfNeeded()311 public void migrateNetworkScorerAppSettingIfNeeded() { 312 final String scorerAppPkgNameSetting = 313 mSettingsFacade.getString(mContext, Settings.Global.NETWORK_SCORER_APP); 314 if (TextUtils.isEmpty(scorerAppPkgNameSetting)) { 315 // Early exit, nothing to do. 316 return; 317 } 318 319 final NetworkScorerAppData currentAppData = getActiveScorer(); 320 if (currentAppData == null) { 321 // Don't touch anything until we have an active scorer to work with. 322 return; 323 } 324 325 if (DEBUG) { 326 Log.d(TAG, "Migrating Settings.Global.NETWORK_SCORER_APP " 327 + "(" + scorerAppPkgNameSetting + ")..."); 328 } 329 330 // If the new (useOpenWifi) Setting isn't set and the old Setting's value matches the 331 // new metadata value then update the new Setting with the old value. Otherwise it's a 332 // mismatch so we shouldn't enable the Setting automatically. 333 final ComponentName enableUseOpenWifiActivity = 334 currentAppData.getEnableUseOpenWifiActivity(); 335 final String useOpenWifiSetting = 336 mSettingsFacade.getString(mContext, Settings.Global.USE_OPEN_WIFI_PACKAGE); 337 if (TextUtils.isEmpty(useOpenWifiSetting) 338 && enableUseOpenWifiActivity != null 339 && scorerAppPkgNameSetting.equals(enableUseOpenWifiActivity.getPackageName())) { 340 mSettingsFacade.putString(mContext, Settings.Global.USE_OPEN_WIFI_PACKAGE, 341 scorerAppPkgNameSetting); 342 if (DEBUG) { 343 Log.d(TAG, "Settings.Global.USE_OPEN_WIFI_PACKAGE set to " 344 + "'" + scorerAppPkgNameSetting + "'."); 345 } 346 } 347 348 // Clear out the old setting so we don't run through the migration code again. 349 mSettingsFacade.putString(mContext, Settings.Global.NETWORK_SCORER_APP, null); 350 if (DEBUG) { 351 Log.d(TAG, "Settings.Global.NETWORK_SCORER_APP migration complete."); 352 final String setting = 353 mSettingsFacade.getString(mContext, Settings.Global.USE_OPEN_WIFI_PACKAGE); 354 Log.d(TAG, "Settings.Global.USE_OPEN_WIFI_PACKAGE is: '" + setting + "'."); 355 } 356 } 357 getDefaultPackageSetting()358 private String getDefaultPackageSetting() { 359 return mContext.getResources().getString( 360 R.string.config_defaultNetworkRecommendationProviderPackage); 361 } 362 getNetworkRecommendationsPackage()363 private String getNetworkRecommendationsPackage() { 364 return mSettingsFacade.getString(mContext, Settings.Global.NETWORK_RECOMMENDATIONS_PACKAGE); 365 } 366 setNetworkRecommendationsPackage(String packageName)367 private void setNetworkRecommendationsPackage(String packageName) { 368 mSettingsFacade.putString(mContext, 369 Settings.Global.NETWORK_RECOMMENDATIONS_PACKAGE, packageName); 370 if (VERBOSE) { 371 Log.d(TAG, Settings.Global.NETWORK_RECOMMENDATIONS_PACKAGE + " set to " + packageName); 372 } 373 } 374 getNetworkRecommendationsEnabledSetting()375 private int getNetworkRecommendationsEnabledSetting() { 376 return mSettingsFacade.getInt(mContext, Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, 0); 377 } 378 setNetworkRecommendationsEnabledSetting(int value)379 private void setNetworkRecommendationsEnabledSetting(int value) { 380 mSettingsFacade.putInt(mContext, 381 Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, value); 382 if (VERBOSE) { 383 Log.d(TAG, Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED + " set to " + value); 384 } 385 } 386 387 /** 388 * Wrapper around Settings to make testing easier. 389 */ 390 public static class SettingsFacade { putString(Context context, String name, String value)391 public boolean putString(Context context, String name, String value) { 392 return Settings.Global.putString(context.getContentResolver(), name, value); 393 } 394 getString(Context context, String name)395 public String getString(Context context, String name) { 396 return Settings.Global.getString(context.getContentResolver(), name); 397 } 398 putInt(Context context, String name, int value)399 public boolean putInt(Context context, String name, int value) { 400 return Settings.Global.putInt(context.getContentResolver(), name, value); 401 } 402 getInt(Context context, String name, int defaultValue)403 public int getInt(Context context, String name, int defaultValue) { 404 return Settings.Global.getInt(context.getContentResolver(), name, defaultValue); 405 } 406 getSecureInt(Context context, String name, int defaultValue)407 public int getSecureInt(Context context, String name, int defaultValue) { 408 final ContentResolver cr = context.getContentResolver(); 409 return Settings.Secure.getIntForUser(cr, name, defaultValue, cr.getUserId()); 410 } 411 } 412 } 413