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 try { 200 mProxyService.stopPacSystem(); 201 } catch (RemoteException e) { 202 Log.w(TAG, "Failed to stop PAC service", e); 203 } finally { 204 unbind(); 205 } 206 } 207 } 208 return DO_SEND_BROADCAST; 209 } 210 } 211 212 /** 213 * Does a post and reports back the status code. 214 * 215 * @throws IOException if the URL is malformed, or the PAC file is too big. 216 */ get(Uri pacUri)217 private static String get(Uri pacUri) throws IOException { 218 URL url = new URL(pacUri.toString()); 219 URLConnection urlConnection = url.openConnection(java.net.Proxy.NO_PROXY); 220 long contentLength = -1; 221 try { 222 contentLength = Long.parseLong(urlConnection.getHeaderField("Content-Length")); 223 } catch (NumberFormatException e) { 224 // Ignore 225 } 226 if (contentLength > MAX_PAC_SIZE) { 227 throw new IOException("PAC too big: " + contentLength + " bytes"); 228 } 229 ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 230 byte[] buffer = new byte[1024]; 231 int count; 232 while ((count = urlConnection.getInputStream().read(buffer)) != -1) { 233 bytes.write(buffer, 0, count); 234 if (bytes.size() > MAX_PAC_SIZE) { 235 throw new IOException("PAC too big"); 236 } 237 } 238 return bytes.toString(); 239 } 240 getNextDelay(int currentDelay)241 private int getNextDelay(int currentDelay) { 242 if (++currentDelay > DELAY_4) { 243 return DELAY_4; 244 } 245 return currentDelay; 246 } 247 longSchedule()248 private void longSchedule() { 249 mCurrentDelay = DELAY_1; 250 setDownloadIn(DELAY_LONG); 251 } 252 reschedule()253 private void reschedule() { 254 mCurrentDelay = getNextDelay(mCurrentDelay); 255 setDownloadIn(mCurrentDelay); 256 } 257 getPacChangeDelay()258 private String getPacChangeDelay() { 259 final ContentResolver cr = mContext.getContentResolver(); 260 261 // Check system properties for the default value then use secure settings value, if any. 262 String defaultDelay = SystemProperties.get( 263 "conn." + Settings.Global.PAC_CHANGE_DELAY, 264 DEFAULT_DELAYS); 265 String val = Settings.Global.getString(cr, Settings.Global.PAC_CHANGE_DELAY); 266 return (val == null) ? defaultDelay : val; 267 } 268 getDownloadDelay(int delayIndex)269 private long getDownloadDelay(int delayIndex) { 270 String[] list = getPacChangeDelay().split(" "); 271 if (delayIndex < list.length) { 272 return Long.parseLong(list[delayIndex]); 273 } 274 return 0; 275 } 276 setDownloadIn(int delayIndex)277 private void setDownloadIn(int delayIndex) { 278 long delay = getDownloadDelay(delayIndex); 279 long timeTillTrigger = 1000 * delay + SystemClock.elapsedRealtime(); 280 getAlarmManager().set(AlarmManager.ELAPSED_REALTIME, timeTillTrigger, mPacRefreshIntent); 281 } 282 setCurrentProxyScript(String script)283 private void setCurrentProxyScript(String script) { 284 if (mProxyService == null) { 285 Log.e(TAG, "setCurrentProxyScript: no proxy service"); 286 return; 287 } 288 try { 289 mProxyService.setPacFile(script); 290 mCurrentPac = script; 291 } catch (RemoteException e) { 292 Log.e(TAG, "Unable to set PAC file", e); 293 } 294 } 295 bind()296 private void bind() { 297 if (mContext == null) { 298 Log.e(TAG, "No context for binding"); 299 return; 300 } 301 Intent intent = new Intent(); 302 intent.setClassName(PAC_PACKAGE, PAC_SERVICE); 303 if ((mProxyConnection != null) && (mConnection != null)) { 304 // Already bound: no need to bind again, just download the new file. 305 mNetThreadHandler.post(mPacDownloader); 306 return; 307 } 308 mConnection = new ServiceConnection() { 309 @Override 310 public void onServiceDisconnected(ComponentName component) { 311 synchronized (mProxyLock) { 312 mProxyService = null; 313 } 314 } 315 316 @Override 317 public void onServiceConnected(ComponentName component, IBinder binder) { 318 synchronized (mProxyLock) { 319 try { 320 Log.d(TAG, "Adding service " + PAC_SERVICE_NAME + " " 321 + binder.getInterfaceDescriptor()); 322 } catch (RemoteException e1) { 323 Log.e(TAG, "Remote Exception", e1); 324 } 325 ServiceManager.addService(PAC_SERVICE_NAME, binder); 326 mProxyService = IProxyService.Stub.asInterface(binder); 327 if (mProxyService == null) { 328 Log.e(TAG, "No proxy service"); 329 } else { 330 try { 331 mProxyService.startPacSystem(); 332 } catch (RemoteException e) { 333 Log.e(TAG, "Unable to reach ProxyService - PAC will not be started", e); 334 } 335 mNetThreadHandler.post(mPacDownloader); 336 } 337 } 338 } 339 }; 340 mContext.bindService(intent, mConnection, 341 Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND | Context.BIND_NOT_VISIBLE); 342 343 intent = new Intent(); 344 intent.setClassName(PROXY_PACKAGE, PROXY_SERVICE); 345 mProxyConnection = new ServiceConnection() { 346 @Override 347 public void onServiceDisconnected(ComponentName component) { 348 } 349 350 @Override 351 public void onServiceConnected(ComponentName component, IBinder binder) { 352 IProxyCallback callbackService = IProxyCallback.Stub.asInterface(binder); 353 if (callbackService != null) { 354 try { 355 callbackService.getProxyPort(new IProxyPortListener.Stub() { 356 @Override 357 public void setProxyPort(int port) { 358 if (mLastPort != -1) { 359 // Always need to send if port changed 360 mHasSentBroadcast = false; 361 } 362 mLastPort = port; 363 if (port != -1) { 364 Log.d(TAG, "Local proxy is bound on " + port); 365 sendProxyIfNeeded(); 366 } else { 367 Log.e(TAG, "Received invalid port from Local Proxy," 368 + " PAC will not be operational"); 369 } 370 } 371 }); 372 } catch (RemoteException e) { 373 e.printStackTrace(); 374 } 375 } 376 } 377 }; 378 mContext.bindService(intent, mProxyConnection, 379 Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND | Context.BIND_NOT_VISIBLE); 380 } 381 unbind()382 private void unbind() { 383 if (mConnection != null) { 384 mContext.unbindService(mConnection); 385 mConnection = null; 386 } 387 if (mProxyConnection != null) { 388 mContext.unbindService(mProxyConnection); 389 mProxyConnection = null; 390 } 391 mProxyService = null; 392 mLastPort = -1; 393 } 394 sendPacBroadcast(ProxyInfo proxy)395 private void sendPacBroadcast(ProxyInfo proxy) { 396 mConnectivityHandler.sendMessage(mConnectivityHandler.obtainMessage(mProxyMessage, proxy)); 397 } 398 sendProxyIfNeeded()399 private synchronized void sendProxyIfNeeded() { 400 if (!mHasDownloaded || (mLastPort == -1)) { 401 return; 402 } 403 if (!mHasSentBroadcast) { 404 sendPacBroadcast(new ProxyInfo(mPacUrl, mLastPort)); 405 mHasSentBroadcast = true; 406 } 407 } 408 } 409