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