1 /** 2 * Copyright (c) 2013, 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 package com.android.server.connectivity; 17 18 import android.annotation.WorkerThread; 19 import android.app.AlarmManager; 20 import android.app.PendingIntent; 21 import android.content.BroadcastReceiver; 22 import android.content.ComponentName; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.content.ServiceConnection; 28 import android.net.ProxyInfo; 29 import android.net.TrafficStats; 30 import android.net.Uri; 31 import android.os.Handler; 32 import android.os.HandlerThread; 33 import android.os.IBinder; 34 import android.os.RemoteException; 35 import android.os.ServiceManager; 36 import android.os.SystemClock; 37 import android.os.SystemProperties; 38 import android.provider.Settings; 39 import android.util.Log; 40 41 import com.android.internal.annotations.GuardedBy; 42 import com.android.internal.util.TrafficStatsConstants; 43 import com.android.net.IProxyCallback; 44 import com.android.net.IProxyPortListener; 45 import com.android.net.IProxyService; 46 47 import java.io.ByteArrayOutputStream; 48 import java.io.IOException; 49 import java.net.URL; 50 import java.net.URLConnection; 51 52 /** 53 * @hide 54 */ 55 public class PacManager { 56 private static final String PAC_PACKAGE = "com.android.pacprocessor"; 57 private static final String PAC_SERVICE = "com.android.pacprocessor.PacService"; 58 private static final String PAC_SERVICE_NAME = "com.android.net.IProxyService"; 59 60 private static final String PROXY_PACKAGE = "com.android.proxyhandler"; 61 private static final String PROXY_SERVICE = "com.android.proxyhandler.ProxyService"; 62 63 private static final String TAG = "PacManager"; 64 65 private static final String ACTION_PAC_REFRESH = "android.net.proxy.PAC_REFRESH"; 66 67 private static final String DEFAULT_DELAYS = "8 32 120 14400 43200"; 68 private static final int DELAY_1 = 0; 69 private static final int DELAY_4 = 3; 70 private static final int DELAY_LONG = 4; 71 private static final long MAX_PAC_SIZE = 20 * 1000 * 1000; 72 73 // Return values for #setCurrentProxyScriptUrl 74 public static final boolean DONT_SEND_BROADCAST = false; 75 public static final boolean DO_SEND_BROADCAST = true; 76 77 private String mCurrentPac; 78 @GuardedBy("mProxyLock") 79 private volatile Uri mPacUrl = Uri.EMPTY; 80 81 private AlarmManager mAlarmManager; 82 @GuardedBy("mProxyLock") 83 private IProxyService mProxyService; 84 private PendingIntent mPacRefreshIntent; 85 private ServiceConnection mConnection; 86 private ServiceConnection mProxyConnection; 87 private Context mContext; 88 89 private int mCurrentDelay; 90 private int mLastPort; 91 92 private volatile boolean mHasSentBroadcast; 93 private volatile boolean mHasDownloaded; 94 95 private Handler mConnectivityHandler; 96 private final int mProxyMessage; 97 98 /** 99 * Used for locking when setting mProxyService and all references to mCurrentPac. 100 */ 101 private final Object mProxyLock = new Object(); 102 103 /** 104 * Runnable to download PAC script. 105 * The behavior relies on the assumption it always runs on mNetThread to guarantee that the 106 * latest data fetched from mPacUrl is stored in mProxyService. 107 */ 108 private Runnable mPacDownloader = new Runnable() { 109 @Override 110 @WorkerThread 111 public void run() { 112 String file; 113 final Uri pacUrl = mPacUrl; 114 if (Uri.EMPTY.equals(pacUrl)) return; 115 final int oldTag = TrafficStats.getAndSetThreadStatsTag( 116 TrafficStatsConstants.TAG_SYSTEM_PAC); 117 try { 118 file = get(pacUrl); 119 } catch (IOException ioe) { 120 file = null; 121 Log.w(TAG, "Failed to load PAC file: " + ioe); 122 } finally { 123 TrafficStats.setThreadStatsTag(oldTag); 124 } 125 if (file != null) { 126 synchronized (mProxyLock) { 127 if (!file.equals(mCurrentPac)) { 128 setCurrentProxyScript(file); 129 } 130 } 131 mHasDownloaded = true; 132 sendProxyIfNeeded(); 133 longSchedule(); 134 } else { 135 reschedule(); 136 } 137 } 138 }; 139 140 private final Handler mNetThreadHandler; 141 142 class PacRefreshIntentReceiver extends BroadcastReceiver { onReceive(Context context, Intent intent)143 public void onReceive(Context context, Intent intent) { 144 mNetThreadHandler.post(mPacDownloader); 145 } 146 } 147 PacManager(Context context, Handler handler, int proxyMessage)148 public PacManager(Context context, Handler handler, int proxyMessage) { 149 mContext = context; 150 mLastPort = -1; 151 final HandlerThread netThread = new HandlerThread("android.pacmanager", 152 android.os.Process.THREAD_PRIORITY_DEFAULT); 153 netThread.start(); 154 mNetThreadHandler = new Handler(netThread.getLooper()); 155 156 mPacRefreshIntent = PendingIntent.getBroadcast( 157 context, 0, new Intent(ACTION_PAC_REFRESH), 0); 158 context.registerReceiver(new PacRefreshIntentReceiver(), 159 new IntentFilter(ACTION_PAC_REFRESH)); 160 mConnectivityHandler = handler; 161 mProxyMessage = proxyMessage; 162 } 163 getAlarmManager()164 private AlarmManager getAlarmManager() { 165 if (mAlarmManager == null) { 166 mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); 167 } 168 return mAlarmManager; 169 } 170 171 /** 172 * Updates the PAC Manager with current Proxy information. This is called by 173 * the ProxyTracker directly before a broadcast takes place to allow 174 * the PacManager to indicate that the broadcast should not be sent and the 175 * PacManager will trigger a new broadcast when it is ready. 176 * 177 * @param proxy Proxy information that is about to be broadcast. 178 * @return Returns whether the broadcast should be sent : either DO_ or DONT_SEND_BROADCAST 179 */ setCurrentProxyScriptUrl(ProxyInfo proxy)180 synchronized boolean setCurrentProxyScriptUrl(ProxyInfo proxy) { 181 if (!Uri.EMPTY.equals(proxy.getPacFileUrl())) { 182 if (proxy.getPacFileUrl().equals(mPacUrl) && (proxy.getPort() > 0)) { 183 // Allow to send broadcast, nothing to do. 184 return DO_SEND_BROADCAST; 185 } 186 mPacUrl = proxy.getPacFileUrl(); 187 mCurrentDelay = DELAY_1; 188 mHasSentBroadcast = false; 189 mHasDownloaded = false; 190 getAlarmManager().cancel(mPacRefreshIntent); 191 bind(); 192 return DONT_SEND_BROADCAST; 193 } else { 194 getAlarmManager().cancel(mPacRefreshIntent); 195 synchronized (mProxyLock) { 196 mPacUrl = Uri.EMPTY; 197 mCurrentPac = null; 198 if (mProxyService != null) { 199 unbind(); 200 } 201 } 202 return DO_SEND_BROADCAST; 203 } 204 } 205 206 /** 207 * Does a post and reports back the status code. 208 * 209 * @throws IOException if the URL is malformed, or the PAC file is too big. 210 */ get(Uri pacUri)211 private static String get(Uri pacUri) throws IOException { 212 URL url = new URL(pacUri.toString()); 213 URLConnection urlConnection = url.openConnection(java.net.Proxy.NO_PROXY); 214 long contentLength = -1; 215 try { 216 contentLength = Long.parseLong(urlConnection.getHeaderField("Content-Length")); 217 } catch (NumberFormatException e) { 218 // Ignore 219 } 220 if (contentLength > MAX_PAC_SIZE) { 221 throw new IOException("PAC too big: " + contentLength + " bytes"); 222 } 223 ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 224 byte[] buffer = new byte[1024]; 225 int count; 226 while ((count = urlConnection.getInputStream().read(buffer)) != -1) { 227 bytes.write(buffer, 0, count); 228 if (bytes.size() > MAX_PAC_SIZE) { 229 throw new IOException("PAC too big"); 230 } 231 } 232 return bytes.toString(); 233 } 234 getNextDelay(int currentDelay)235 private int getNextDelay(int currentDelay) { 236 if (++currentDelay > DELAY_4) { 237 return DELAY_4; 238 } 239 return currentDelay; 240 } 241 longSchedule()242 private void longSchedule() { 243 mCurrentDelay = DELAY_1; 244 setDownloadIn(DELAY_LONG); 245 } 246 reschedule()247 private void reschedule() { 248 mCurrentDelay = getNextDelay(mCurrentDelay); 249 setDownloadIn(mCurrentDelay); 250 } 251 getPacChangeDelay()252 private String getPacChangeDelay() { 253 final ContentResolver cr = mContext.getContentResolver(); 254 255 // Check system properties for the default value then use secure settings value, if any. 256 String defaultDelay = SystemProperties.get( 257 "conn." + Settings.Global.PAC_CHANGE_DELAY, 258 DEFAULT_DELAYS); 259 String val = Settings.Global.getString(cr, Settings.Global.PAC_CHANGE_DELAY); 260 return (val == null) ? defaultDelay : val; 261 } 262 getDownloadDelay(int delayIndex)263 private long getDownloadDelay(int delayIndex) { 264 String[] list = getPacChangeDelay().split(" "); 265 if (delayIndex < list.length) { 266 return Long.parseLong(list[delayIndex]); 267 } 268 return 0; 269 } 270 setDownloadIn(int delayIndex)271 private void setDownloadIn(int delayIndex) { 272 long delay = getDownloadDelay(delayIndex); 273 long timeTillTrigger = 1000 * delay + SystemClock.elapsedRealtime(); 274 getAlarmManager().set(AlarmManager.ELAPSED_REALTIME, timeTillTrigger, mPacRefreshIntent); 275 } 276 setCurrentProxyScript(String script)277 private void setCurrentProxyScript(String script) { 278 if (mProxyService == null) { 279 Log.e(TAG, "setCurrentProxyScript: no proxy service"); 280 return; 281 } 282 try { 283 mProxyService.setPacFile(script); 284 mCurrentPac = script; 285 } catch (RemoteException e) { 286 Log.e(TAG, "Unable to set PAC file", e); 287 } 288 } 289 bind()290 private void bind() { 291 if (mContext == null) { 292 Log.e(TAG, "No context for binding"); 293 return; 294 } 295 Intent intent = new Intent(); 296 intent.setClassName(PAC_PACKAGE, PAC_SERVICE); 297 if ((mProxyConnection != null) && (mConnection != null)) { 298 // Already bound: no need to bind again, just download the new file. 299 mNetThreadHandler.post(mPacDownloader); 300 return; 301 } 302 mConnection = new ServiceConnection() { 303 @Override 304 public void onServiceDisconnected(ComponentName component) { 305 synchronized (mProxyLock) { 306 mProxyService = null; 307 } 308 } 309 310 @Override 311 public void onServiceConnected(ComponentName component, IBinder binder) { 312 synchronized (mProxyLock) { 313 try { 314 Log.d(TAG, "Adding service " + PAC_SERVICE_NAME + " " 315 + binder.getInterfaceDescriptor()); 316 } catch (RemoteException e1) { 317 Log.e(TAG, "Remote Exception", e1); 318 } 319 ServiceManager.addService(PAC_SERVICE_NAME, binder); 320 mProxyService = IProxyService.Stub.asInterface(binder); 321 if (mProxyService == null) { 322 Log.e(TAG, "No proxy service"); 323 } else { 324 mNetThreadHandler.post(mPacDownloader); 325 } 326 } 327 } 328 }; 329 mContext.bindService(intent, mConnection, 330 Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND | Context.BIND_NOT_VISIBLE); 331 332 intent = new Intent(); 333 intent.setClassName(PROXY_PACKAGE, PROXY_SERVICE); 334 mProxyConnection = new ServiceConnection() { 335 @Override 336 public void onServiceDisconnected(ComponentName component) { 337 } 338 339 @Override 340 public void onServiceConnected(ComponentName component, IBinder binder) { 341 IProxyCallback callbackService = IProxyCallback.Stub.asInterface(binder); 342 if (callbackService != null) { 343 try { 344 callbackService.getProxyPort(new IProxyPortListener.Stub() { 345 @Override 346 public void setProxyPort(int port) { 347 if (mLastPort != -1) { 348 // Always need to send if port changed 349 mHasSentBroadcast = false; 350 } 351 mLastPort = port; 352 if (port != -1) { 353 Log.d(TAG, "Local proxy is bound on " + port); 354 sendProxyIfNeeded(); 355 } else { 356 Log.e(TAG, "Received invalid port from Local Proxy," 357 + " PAC will not be operational"); 358 } 359 } 360 }); 361 } catch (RemoteException e) { 362 e.printStackTrace(); 363 } 364 } 365 } 366 }; 367 mContext.bindService(intent, mProxyConnection, 368 Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND | Context.BIND_NOT_VISIBLE); 369 } 370 unbind()371 private void unbind() { 372 if (mConnection != null) { 373 mContext.unbindService(mConnection); 374 mConnection = null; 375 } 376 if (mProxyConnection != null) { 377 mContext.unbindService(mProxyConnection); 378 mProxyConnection = null; 379 } 380 mProxyService = null; 381 mLastPort = -1; 382 } 383 sendPacBroadcast(ProxyInfo proxy)384 private void sendPacBroadcast(ProxyInfo proxy) { 385 mConnectivityHandler.sendMessage(mConnectivityHandler.obtainMessage(mProxyMessage, proxy)); 386 } 387 sendProxyIfNeeded()388 private synchronized void sendProxyIfNeeded() { 389 if (!mHasDownloaded || (mLastPort == -1)) { 390 return; 391 } 392 if (!mHasSentBroadcast) { 393 sendPacBroadcast(new ProxyInfo(mPacUrl, mLastPort)); 394 mHasSentBroadcast = true; 395 } 396 } 397 } 398