Home | History | Annotate | Download | only in server
      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