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