1 /*
2  * Copyright (C) 2022 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.wifi;
18 
19 import static android.net.wifi.WifiManager.PnoScanResultsCallback.REGISTER_PNO_CALLBACK_RESOURCE_BUSY;
20 import static android.net.wifi.WifiManager.PnoScanResultsCallback.REMOVE_PNO_CALLBACK_RESULTS_DELIVERED;
21 import static android.net.wifi.WifiManager.PnoScanResultsCallback.REMOVE_PNO_CALLBACK_UNREGISTERED;
22 
23 import android.annotation.NonNull;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.net.wifi.IPnoScanResultsCallback;
27 import android.net.wifi.ScanResult;
28 import android.net.wifi.WifiManager;
29 import android.net.wifi.WifiSsid;
30 import android.os.Handler;
31 import android.os.IBinder;
32 import android.os.RemoteException;
33 import android.os.UserHandle;
34 import android.util.ArraySet;
35 import android.util.Log;
36 
37 import java.io.FileDescriptor;
38 import java.io.PrintWriter;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.Collections;
42 import java.util.List;
43 import java.util.NoSuchElementException;
44 import java.util.Set;
45 import java.util.stream.Collectors;
46 
47 /**
48  * Manages PNO scan requests from apps.
49  * This class is not thread safe and is expected to be run on a single thread.
50  */
51 public class ExternalPnoScanRequestManager implements IBinder.DeathRecipient {
52     private static final String TAG = "ExternalPnoScanRequestManager";
53     private ExternalPnoScanRequest mCurrentRequest;
54     private final Handler mHandler;
55     private int mCurrentRequestOnPnoNetworkFoundCount = 0;
56     private Context mContext;
57     private boolean mVerboseLoggingEnabled = false;
58 
59     /**
60      * Creates a ExternalPnoScanRequestManager.
61      * @param handler to run binder death callback.
62      * @param context of the wifi service.
63      */
ExternalPnoScanRequestManager(Handler handler, Context context)64     public ExternalPnoScanRequestManager(Handler handler, Context context) {
65         mHandler = handler;
66         mContext = context;
67     }
68 
69     /**
70      * Returns a copy of the current SSIDs being requested for PNO scan.
71      */
getExternalPnoScanSsids()72     public Set<String> getExternalPnoScanSsids() {
73         return mCurrentRequest == null ? Collections.EMPTY_SET
74                 : new ArraySet<>(mCurrentRequest.mSsidStrings);
75     }
76 
77     /**
78      * Returns a copy of the current frequencies being requested for PNO scan.
79      */
getExternalPnoScanFrequencies()80     public Set<Integer> getExternalPnoScanFrequencies() {
81         return mCurrentRequest == null ? Collections.EMPTY_SET
82                 : new ArraySet<>(mCurrentRequest.mFrequencies);
83     }
84 
85     /**
86      * Enables verbose logging.
87      */
enableVerboseLogging(boolean enabled)88     public void enableVerboseLogging(boolean enabled) {
89         mVerboseLoggingEnabled = enabled;
90     }
91 
92     /**
93      * Sets the request. This will fail if there's already a request set.
94      */
setRequest(int uid, @NonNull String packageName, @NonNull IBinder binder, @NonNull IPnoScanResultsCallback callback, @NonNull List<WifiSsid> ssids, @NonNull int[] frequencies)95     public boolean setRequest(int uid, @NonNull String packageName, @NonNull IBinder binder,
96             @NonNull IPnoScanResultsCallback callback,
97             @NonNull List<WifiSsid> ssids, @NonNull int[] frequencies) {
98         if (mCurrentRequest != null && uid != mCurrentRequest.mUid) {
99             try {
100                 callback.onRegisterFailed(REGISTER_PNO_CALLBACK_RESOURCE_BUSY);
101             } catch (RemoteException e) {
102                 Log.e(TAG, "RemoteException failed to trigger onRegisterFailed for callback="
103                         + callback);
104             }
105             return false;
106         }
107         ExternalPnoScanRequest request = new ExternalPnoScanRequestManager.ExternalPnoScanRequest(
108                 uid, packageName, binder, callback, ssids, frequencies);
109         try {
110             request.mBinder.linkToDeath(this, 0);
111         } catch (RemoteException e) {
112             Log.e(TAG, "mBinder.linkToDeath failed: " + e.getMessage());
113             return false;
114         }
115         try {
116             request.mCallback.onRegisterSuccess();
117         } catch (RemoteException e) {
118             Log.e(TAG, "Failed to register request due to remote exception:" + e.getMessage());
119             return false;
120         }
121         removeCurrentRequest();
122         if (mVerboseLoggingEnabled) {
123             Log.i(TAG, "Successfully set external PNO scan request:" + request);
124         }
125         mCurrentRequest = request;
126         return true;
127     }
128 
removeCurrentRequest()129     private void removeCurrentRequest() {
130         if (mCurrentRequest != null) {
131             try {
132                 mCurrentRequest.mBinder.unlinkToDeath(this, 0);
133                 if (mVerboseLoggingEnabled) {
134                     Log.i(TAG, "mBinder.unlinkToDeath on request:" + mCurrentRequest);
135                 }
136             } catch (NoSuchElementException e) {
137                 Log.e(TAG, "Encountered remote exception in unlinkToDeath=" + e.getMessage());
138             }
139         }
140         mCurrentRequest = null;
141         mCurrentRequestOnPnoNetworkFoundCount = 0;
142     }
143 
144     /**
145      * Removes the requests. Will fail if the remover's uid and packageName does not match with the
146      * creator's uid and packageName.
147      */
removeRequest(int uid)148     public boolean removeRequest(int uid) {
149         if (mCurrentRequest == null || uid != mCurrentRequest.mUid) {
150             return false;
151         }
152 
153         try {
154             mCurrentRequest.mCallback.onRemoved(REMOVE_PNO_CALLBACK_UNREGISTERED);
155         } catch (RemoteException e) {
156             Log.e(TAG, "Encountered remote exception in onRemoved=" + e.getMessage());
157         }
158         removeCurrentRequest();
159         return true;
160     }
161 
162     /**
163      * Triggered when networks are found. Any results matching the external request will be
164      * sent to the callback.
165      * @param scanDetails the list of scanDetails received by the framework
166      */
onScanResultsAvailable(List<ScanDetail> scanDetails)167     public void onScanResultsAvailable(List<ScanDetail> scanDetails) {
168         if (mCurrentRequest == null) {
169             return;
170         }
171         mCurrentRequestOnPnoNetworkFoundCount++;
172         List<ScanResult> requestedResults = new ArrayList<>();
173         for (ScanDetail scanDetail : scanDetails) {
174             if (mCurrentRequest.mSsidStrings.contains(
175                     scanDetail.getScanResult().getWifiSsid().toString())) {
176                 requestedResults.add(scanDetail.getScanResult());
177             }
178         }
179         if (requestedResults.isEmpty()) {
180             return;
181         }
182 
183         // requested PNO SSIDs found. Send results and then remove request.
184         if (mVerboseLoggingEnabled) {
185             Log.i(TAG, "On network found for request:" + mCurrentRequest);
186         }
187         sendScanResultAvailableBroadcastToPackage(mCurrentRequest.mPackageName);
188         try {
189             mCurrentRequest.mCallback.onScanResultsAvailable(requestedResults);
190             mCurrentRequest.mCallback.onRemoved(REMOVE_PNO_CALLBACK_RESULTS_DELIVERED);
191         } catch (RemoteException e) {
192             Log.e(TAG, "Failed to send PNO results via callback due to remote exception="
193                     + e.getMessage());
194         }
195         removeCurrentRequest();
196     }
197 
sendScanResultAvailableBroadcastToPackage(String packageName)198     private void sendScanResultAvailableBroadcastToPackage(String packageName) {
199         Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
200         intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true);
201         intent.setPackage(packageName);
202         mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
203         if (mVerboseLoggingEnabled) {
204             Log.i(TAG, "Successfully sent out targeted broadcast for:" + mCurrentRequest);
205         }
206     }
207 
208     /**
209      * Tracks a request for PNO scan made by an app.
210      */
211     public static class ExternalPnoScanRequest {
212         private int mUid;
213         private String mPackageName;
214         private Set<String> mSsidStrings;
215         private Set<Integer> mFrequencies;
216         private IPnoScanResultsCallback mCallback;
217         private IBinder mBinder;
218 
219         /**
220          * @param uid identifies the caller
221          * @param binder obtained from the caller
222          * @param callback used to send results back to the caller
223          * @param ssids requested SSIDs for PNO scan
224          */
ExternalPnoScanRequest(int uid, String packageName, IBinder binder, IPnoScanResultsCallback callback, List<WifiSsid> ssids, int[] frequencies)225         public ExternalPnoScanRequest(int uid, String packageName, IBinder binder,
226                 IPnoScanResultsCallback callback, List<WifiSsid> ssids, int[] frequencies) {
227             mUid = uid;
228             mPackageName = packageName;
229             mBinder = binder;
230             mCallback = callback;
231             mSsidStrings = new ArraySet<>();
232             for (WifiSsid wifiSsid : ssids) {
233                 mSsidStrings.add(wifiSsid.toString());
234             }
235             mFrequencies = Arrays.stream(frequencies).boxed().collect(Collectors.toSet());
236         }
237 
238         @Override
toString()239         public String toString() {
240             StringBuilder sbuf = new StringBuilder();
241             sbuf.append("uid=").append(mUid)
242                     .append(", packageName=").append(mPackageName)
243                     .append(", binder=").append(mBinder)
244                     .append(", callback=").append(mCallback)
245                     .append(", mSsidStrings=");
246             for (String s : mSsidStrings) {
247                 sbuf.append(s).append(", ");
248             }
249             sbuf.append(" frequencies=");
250             for (int f : mFrequencies) {
251                 sbuf.append(f).append(", ");
252             }
253             return sbuf.toString();
254         }
255     }
256 
257     /**
258      * Binder has died. Perform cleanup.
259      */
260     @Override
binderDied()261     public void binderDied() {
262         // Log binder died, but keep the request since result will still be delivered with directed
263         // broadcast
264         Log.w(TAG, "Binder died.");
265     }
266 
267     /**
268      * Dump the local logs.
269      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)270     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
271         pw.println("Dump of ExternalPnoScanRequestManager");
272         pw.println("ExternalPnoScanRequestManager - Log Begin ----");
273         if (mCurrentRequest != null) {
274             pw.println("Current external PNO scan request:");
275             pw.println(mCurrentRequest.toString());
276         } else {
277             pw.println("No external PNO scan request set.");
278         }
279         pw.println("mCurrentRequestOnPnoNetworkFoundCount: "
280                 + mCurrentRequestOnPnoNetworkFoundCount);
281         pw.println("ExternalPnoScanRequestManager - Log End ----");
282     }
283 }
284