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
17package com.android.server;
18
19import android.Manifest.permission;
20import android.content.BroadcastReceiver;
21import android.content.ComponentName;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.content.ServiceConnection;
27import android.content.pm.PackageManager;
28import android.net.INetworkScoreCache;
29import android.net.INetworkScoreService;
30import android.net.NetworkScoreManager;
31import android.net.NetworkScorerAppManager;
32import android.net.NetworkScorerAppManager.NetworkScorerAppData;
33import android.net.ScoredNetwork;
34import android.os.Binder;
35import android.os.IBinder;
36import android.os.RemoteException;
37import android.os.UserHandle;
38import android.provider.Settings;
39import android.text.TextUtils;
40import android.util.Log;
41
42import com.android.internal.R;
43import com.android.internal.annotations.GuardedBy;
44import com.android.internal.content.PackageMonitor;
45
46import java.io.FileDescriptor;
47import java.io.PrintWriter;
48import java.util.ArrayList;
49import java.util.HashMap;
50import java.util.HashSet;
51import java.util.List;
52import java.util.Map;
53import java.util.Set;
54
55/**
56 * Backing service for {@link android.net.NetworkScoreManager}.
57 * @hide
58 */
59public class NetworkScoreService extends INetworkScoreService.Stub {
60    private static final String TAG = "NetworkScoreService";
61    private static final boolean DBG = false;
62
63    private final Context mContext;
64    private final Map<Integer, INetworkScoreCache> mScoreCaches;
65    /** Lock used to update mPackageMonitor when scorer package changes occur. */
66    private final Object mPackageMonitorLock = new Object[0];
67
68    @GuardedBy("mPackageMonitorLock")
69    private NetworkScorerPackageMonitor mPackageMonitor;
70    private ScoringServiceConnection mServiceConnection;
71
72    private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
73        @Override
74        public void onReceive(Context context, Intent intent) {
75            final String action = intent.getAction();
76            final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
77            if (DBG) Log.d(TAG, "Received " + action + " for userId " + userId);
78            if (userId == UserHandle.USER_NULL) return;
79
80            if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
81                onUserUnlocked(userId);
82            }
83        }
84    };
85
86    /**
87     * Clears scores when the active scorer package is no longer valid and
88     * manages the service connection.
89     */
90    private class NetworkScorerPackageMonitor extends PackageMonitor {
91        final String mRegisteredPackage;
92
93        private NetworkScorerPackageMonitor(String mRegisteredPackage) {
94            this.mRegisteredPackage = mRegisteredPackage;
95        }
96
97        @Override
98        public void onPackageAdded(String packageName, int uid) {
99            evaluateBinding(packageName, true /* forceUnbind */);
100        }
101
102        @Override
103        public void onPackageRemoved(String packageName, int uid) {
104            evaluateBinding(packageName, true /* forceUnbind */);
105        }
106
107        @Override
108        public void onPackageModified(String packageName) {
109            evaluateBinding(packageName, false /* forceUnbind */);
110        }
111
112        @Override
113        public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
114            if (doit) { // "doit" means the force stop happened instead of just being queried for.
115                for (String packageName : packages) {
116                    evaluateBinding(packageName, true /* forceUnbind */);
117                }
118            }
119            return super.onHandleForceStop(intent, packages, uid, doit);
120        }
121
122        @Override
123        public void onPackageUpdateFinished(String packageName, int uid) {
124            evaluateBinding(packageName, true /* forceUnbind */);
125        }
126
127        private void evaluateBinding(String scorerPackageName, boolean forceUnbind) {
128            if (mRegisteredPackage.equals(scorerPackageName)) {
129                if (DBG) {
130                    Log.d(TAG, "Evaluating binding for: " + scorerPackageName
131                            + ", forceUnbind=" + forceUnbind);
132                }
133                final NetworkScorerAppData activeScorer =
134                        NetworkScorerAppManager.getActiveScorer(mContext);
135                if (activeScorer == null) {
136                    // Package change has invalidated a scorer, this will also unbind any service
137                    // connection.
138                    Log.i(TAG, "Package " + mRegisteredPackage +
139                            " is no longer valid, disabling scoring.");
140                    setScorerInternal(null);
141                } else if (activeScorer.mScoringServiceClassName == null) {
142                    // The scoring service is not available, make sure it's unbound.
143                    unbindFromScoringServiceIfNeeded();
144                } else { // The scoring service changed in some way.
145                    if (forceUnbind) {
146                        unbindFromScoringServiceIfNeeded();
147                    }
148                    bindToScoringServiceIfNeeded(activeScorer);
149                }
150            }
151        }
152    }
153
154    public NetworkScoreService(Context context) {
155        mContext = context;
156        mScoreCaches = new HashMap<>();
157        IntentFilter filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
158        // TODO: Need to update when we support per-user scorers. http://b/23422763
159        mContext.registerReceiverAsUser(
160                mUserIntentReceiver, UserHandle.SYSTEM, filter, null /* broadcastPermission*/,
161                null /* scheduler */);
162    }
163
164    /** Called when the system is ready to run third-party code but before it actually does so. */
165    void systemReady() {
166        if (DBG) Log.d(TAG, "systemReady");
167        ContentResolver cr = mContext.getContentResolver();
168        if (Settings.Global.getInt(cr, Settings.Global.NETWORK_SCORING_PROVISIONED, 0) == 0) {
169            // On first run, we try to initialize the scorer to the one configured at build time.
170            // This will be a no-op if the scorer isn't actually valid.
171            String defaultPackage = mContext.getResources().getString(
172                    R.string.config_defaultNetworkScorerPackageName);
173            if (!TextUtils.isEmpty(defaultPackage)) {
174                NetworkScorerAppManager.setActiveScorer(mContext, defaultPackage);
175            }
176            Settings.Global.putInt(cr, Settings.Global.NETWORK_SCORING_PROVISIONED, 1);
177        }
178
179        registerPackageMonitorIfNeeded();
180    }
181
182    /** Called when the system is ready for us to start third-party code. */
183    void systemRunning() {
184        if (DBG) Log.d(TAG, "systemRunning");
185        bindToScoringServiceIfNeeded();
186    }
187
188    private void onUserUnlocked(int userId) {
189        registerPackageMonitorIfNeeded();
190        bindToScoringServiceIfNeeded();
191    }
192
193    private void registerPackageMonitorIfNeeded() {
194        if (DBG) Log.d(TAG, "registerPackageMonitorIfNeeded");
195        NetworkScorerAppData scorer = NetworkScorerAppManager.getActiveScorer(mContext);
196        synchronized (mPackageMonitorLock) {
197            // Unregister the current monitor if needed.
198            if (mPackageMonitor != null) {
199                if (DBG) {
200                    Log.d(TAG, "Unregistering package monitor for "
201                            + mPackageMonitor.mRegisteredPackage);
202                }
203                mPackageMonitor.unregister();
204                mPackageMonitor = null;
205            }
206
207            // Create and register the monitor if a scorer is active.
208            if (scorer != null) {
209                mPackageMonitor = new NetworkScorerPackageMonitor(scorer.mPackageName);
210                // TODO: Need to update when we support per-user scorers. http://b/23422763
211                mPackageMonitor.register(mContext, null /* thread */, UserHandle.SYSTEM,
212                        false /* externalStorage */);
213                if (DBG) {
214                    Log.d(TAG, "Registered package monitor for "
215                            + mPackageMonitor.mRegisteredPackage);
216                }
217            }
218        }
219    }
220
221    private void bindToScoringServiceIfNeeded() {
222        if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded");
223        NetworkScorerAppData scorerData = NetworkScorerAppManager.getActiveScorer(mContext);
224        bindToScoringServiceIfNeeded(scorerData);
225    }
226
227    private void bindToScoringServiceIfNeeded(NetworkScorerAppData scorerData) {
228        if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded(" + scorerData + ")");
229        if (scorerData != null && scorerData.mScoringServiceClassName != null) {
230            ComponentName componentName =
231                    new ComponentName(scorerData.mPackageName, scorerData.mScoringServiceClassName);
232            // If we're connected to a different component then drop it.
233            if (mServiceConnection != null
234                    && !mServiceConnection.mComponentName.equals(componentName)) {
235                unbindFromScoringServiceIfNeeded();
236            }
237
238            // If we're not connected at all then create a new connection.
239            if (mServiceConnection == null) {
240                mServiceConnection = new ScoringServiceConnection(componentName);
241            }
242
243            // Make sure the connection is connected (idempotent)
244            mServiceConnection.connect(mContext);
245        } else { // otherwise make sure it isn't bound.
246            unbindFromScoringServiceIfNeeded();
247        }
248    }
249
250    private void unbindFromScoringServiceIfNeeded() {
251        if (DBG) Log.d(TAG, "unbindFromScoringServiceIfNeeded");
252        if (mServiceConnection != null) {
253            mServiceConnection.disconnect(mContext);
254        }
255        mServiceConnection = null;
256    }
257
258    @Override
259    public boolean updateScores(ScoredNetwork[] networks) {
260        if (!NetworkScorerAppManager.isCallerActiveScorer(mContext, getCallingUid())) {
261            throw new SecurityException("Caller with UID " + getCallingUid() +
262                    " is not the active scorer.");
263        }
264
265        // Separate networks by type.
266        Map<Integer, List<ScoredNetwork>> networksByType = new HashMap<>();
267        for (ScoredNetwork network : networks) {
268            List<ScoredNetwork> networkList = networksByType.get(network.networkKey.type);
269            if (networkList == null) {
270                networkList = new ArrayList<>();
271                networksByType.put(network.networkKey.type, networkList);
272            }
273            networkList.add(network);
274        }
275
276        // Pass the scores of each type down to the appropriate network scorer.
277        for (Map.Entry<Integer, List<ScoredNetwork>> entry : networksByType.entrySet()) {
278            INetworkScoreCache scoreCache = mScoreCaches.get(entry.getKey());
279            if (scoreCache != null) {
280                try {
281                    scoreCache.updateScores(entry.getValue());
282                } catch (RemoteException e) {
283                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
284                        Log.v(TAG, "Unable to update scores of type " + entry.getKey(), e);
285                    }
286                }
287            } else if (Log.isLoggable(TAG, Log.VERBOSE)) {
288                Log.v(TAG, "No scorer registered for type " + entry.getKey() + ", discarding");
289            }
290        }
291
292        return true;
293    }
294
295    @Override
296    public boolean clearScores() {
297        // Only the active scorer or the system (who can broadcast BROADCAST_NETWORK_PRIVILEGED)
298        // should be allowed to flush all scores.
299        if (NetworkScorerAppManager.isCallerActiveScorer(mContext, getCallingUid()) ||
300                mContext.checkCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED) ==
301                        PackageManager.PERMISSION_GRANTED) {
302            clearInternal();
303            return true;
304        } else {
305            throw new SecurityException(
306                    "Caller is neither the active scorer nor the scorer manager.");
307        }
308    }
309
310    @Override
311    public boolean setActiveScorer(String packageName) {
312        // TODO: For now, since SCORE_NETWORKS requires an app to be privileged, we allow such apps
313        // to directly set the scorer app rather than having to use the consent dialog. The
314        // assumption is that anyone bundling a scorer app with the system is trusted by the OEM to
315        // do the right thing and not enable this feature without explaining it to the user.
316        // In the future, should this API be opened to 3p apps, we will need to lock this down and
317        // figure out another way to streamline the UX.
318
319        // mContext.enforceCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED, TAG);
320        mContext.enforceCallingOrSelfPermission(permission.SCORE_NETWORKS, TAG);
321
322        return setScorerInternal(packageName);
323    }
324
325    @Override
326    public void disableScoring() {
327        // Only the active scorer or the system (who can broadcast BROADCAST_NETWORK_PRIVILEGED)
328        // should be allowed to disable scoring.
329        if (NetworkScorerAppManager.isCallerActiveScorer(mContext, getCallingUid()) ||
330                mContext.checkCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED) ==
331                        PackageManager.PERMISSION_GRANTED) {
332            // The return value is discarded here because at this point, the call should always
333            // succeed. The only reason for failure is if the new package is not a valid scorer, but
334            // we're disabling scoring altogether here.
335            setScorerInternal(null /* packageName */);
336        } else {
337            throw new SecurityException(
338                    "Caller is neither the active scorer nor the scorer manager.");
339        }
340    }
341
342    /** Set the active scorer. Callers are responsible for checking permissions as appropriate. */
343    private boolean setScorerInternal(String packageName) {
344        if (DBG) Log.d(TAG, "setScorerInternal(" + packageName + ")");
345        long token = Binder.clearCallingIdentity();
346        try {
347            unbindFromScoringServiceIfNeeded();
348            // Preemptively clear scores even though the set operation could fail. We do this for
349            // safety as scores should never be compared across apps; in practice, Settings should
350            // only be allowing valid apps to be set as scorers, so failure here should be rare.
351            clearInternal();
352            // Get the scorer that is about to be replaced, if any, so we can notify it directly.
353            NetworkScorerAppData prevScorer = NetworkScorerAppManager.getActiveScorer(mContext);
354            boolean result = NetworkScorerAppManager.setActiveScorer(mContext, packageName);
355            // Unconditionally attempt to bind to the current scorer. If setActiveScorer() failed
356            // then we'll attempt to restore the previous binding (if any), otherwise an attempt
357            // will be made to bind to the new scorer.
358            bindToScoringServiceIfNeeded();
359            if (result) { // new scorer successfully set
360                registerPackageMonitorIfNeeded();
361
362                Intent intent = new Intent(NetworkScoreManager.ACTION_SCORER_CHANGED);
363                if (prevScorer != null) { // Directly notify the old scorer.
364                    intent.setPackage(prevScorer.mPackageName);
365                    // TODO: Need to update when we support per-user scorers. http://b/23422763
366                    mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
367                }
368
369                if (packageName != null) { // Then notify the new scorer
370                    intent.putExtra(NetworkScoreManager.EXTRA_NEW_SCORER, packageName);
371                    intent.setPackage(packageName);
372                    // TODO: Need to update when we support per-user scorers. http://b/23422763
373                    mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
374                }
375            }
376            return result;
377        } finally {
378            Binder.restoreCallingIdentity(token);
379        }
380    }
381
382    /** Clear scores. Callers are responsible for checking permissions as appropriate. */
383    private void clearInternal() {
384        Set<INetworkScoreCache> cachesToClear = getScoreCaches();
385
386        for (INetworkScoreCache scoreCache : cachesToClear) {
387            try {
388                scoreCache.clearScores();
389            } catch (RemoteException e) {
390                if (Log.isLoggable(TAG, Log.VERBOSE)) {
391                    Log.v(TAG, "Unable to clear scores", e);
392                }
393            }
394        }
395    }
396
397    @Override
398    public void registerNetworkScoreCache(int networkType, INetworkScoreCache scoreCache) {
399        mContext.enforceCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED, TAG);
400        synchronized (mScoreCaches) {
401            if (mScoreCaches.containsKey(networkType)) {
402                throw new IllegalArgumentException(
403                        "Score cache already registered for type " + networkType);
404            }
405            mScoreCaches.put(networkType, scoreCache);
406        }
407    }
408
409    @Override
410    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
411        mContext.enforceCallingOrSelfPermission(permission.DUMP, TAG);
412        NetworkScorerAppData currentScorer = NetworkScorerAppManager.getActiveScorer(mContext);
413        if (currentScorer == null) {
414            writer.println("Scoring is disabled.");
415            return;
416        }
417        writer.println("Current scorer: " + currentScorer.mPackageName);
418
419        for (INetworkScoreCache scoreCache : getScoreCaches()) {
420            try {
421                scoreCache.asBinder().dump(fd, args);
422            } catch (RemoteException e) {
423                writer.println("Unable to dump score cache");
424                if (Log.isLoggable(TAG, Log.VERBOSE)) {
425                    Log.v(TAG, "Unable to dump score cache", e);
426                }
427            }
428        }
429        if (mServiceConnection != null) {
430            mServiceConnection.dump(fd, writer, args);
431        } else {
432            writer.println("ScoringServiceConnection: null");
433        }
434        writer.flush();
435    }
436
437    /**
438     * Returns a set of all score caches that are currently active.
439     *
440     * <p>May be used to perform an action on all score caches without potentially strange behavior
441     * if a new scorer is registered during that action's execution.
442     */
443    private Set<INetworkScoreCache> getScoreCaches() {
444        synchronized (mScoreCaches) {
445            return new HashSet<>(mScoreCaches.values());
446        }
447    }
448
449    private static class ScoringServiceConnection implements ServiceConnection {
450        private final ComponentName mComponentName;
451        private boolean mBound = false;
452        private boolean mConnected = false;
453
454        ScoringServiceConnection(ComponentName componentName) {
455            mComponentName = componentName;
456        }
457
458        void connect(Context context) {
459            if (!mBound) {
460                Intent service = new Intent();
461                service.setComponent(mComponentName);
462                mBound = context.bindServiceAsUser(service, this,
463                        Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
464                        UserHandle.SYSTEM);
465                if (!mBound) {
466                    Log.w(TAG, "Bind call failed for " + service);
467                } else {
468                    if (DBG) Log.d(TAG, "ScoringServiceConnection bound.");
469                }
470            }
471        }
472
473        void disconnect(Context context) {
474            try {
475                if (mBound) {
476                    mBound = false;
477                    context.unbindService(this);
478                    if (DBG) Log.d(TAG, "ScoringServiceConnection unbound.");
479                }
480            } catch (RuntimeException e) {
481                Log.e(TAG, "Unbind failed.", e);
482            }
483        }
484
485        @Override
486        public void onServiceConnected(ComponentName name, IBinder service) {
487            if (DBG) Log.d(TAG, "ScoringServiceConnection: " + name.flattenToString());
488            mConnected = true;
489        }
490
491        @Override
492        public void onServiceDisconnected(ComponentName name) {
493            if (DBG) {
494                Log.d(TAG, "ScoringServiceConnection, disconnected: " + name.flattenToString());
495            }
496            mConnected = false;
497        }
498
499        public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
500            writer.println("ScoringServiceConnection: " + mComponentName + ", bound: " + mBound
501                    + ", connected: " + mConnected);
502        }
503    }
504}
505