| /** |
| * Copyright (c) 2013, The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.server.connectivity; |
| |
| import android.annotation.WorkerThread; |
| import android.app.AlarmManager; |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.ServiceConnection; |
| import android.net.ProxyInfo; |
| import android.net.TrafficStats; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.SystemClock; |
| import android.os.SystemProperties; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.net.IProxyCallback; |
| import com.android.net.IProxyPortListener; |
| import com.android.net.IProxyService; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.net.URL; |
| import java.net.URLConnection; |
| |
| /** |
| * @hide |
| */ |
| public class PacManager { |
| private static final String PAC_PACKAGE = "com.android.pacprocessor"; |
| private static final String PAC_SERVICE = "com.android.pacprocessor.PacService"; |
| private static final String PAC_SERVICE_NAME = "com.android.net.IProxyService"; |
| |
| private static final String PROXY_PACKAGE = "com.android.proxyhandler"; |
| private static final String PROXY_SERVICE = "com.android.proxyhandler.ProxyService"; |
| |
| private static final String TAG = "PacManager"; |
| |
| private static final String ACTION_PAC_REFRESH = "android.net.proxy.PAC_REFRESH"; |
| |
| private static final String DEFAULT_DELAYS = "8 32 120 14400 43200"; |
| private static final int DELAY_1 = 0; |
| private static final int DELAY_4 = 3; |
| private static final int DELAY_LONG = 4; |
| private static final long MAX_PAC_SIZE = 20 * 1000 * 1000; |
| |
| // Return values for #setCurrentProxyScriptUrl |
| enum ToSendOrNotToSendBroadcast { |
| DONT_SEND_BROADCAST, DO_SEND_BROADCAST |
| } |
| |
| private String mCurrentPac; |
| @GuardedBy("mProxyLock") |
| private volatile Uri mPacUrl = Uri.EMPTY; |
| |
| private AlarmManager mAlarmManager; |
| @GuardedBy("mProxyLock") |
| private IProxyService mProxyService; |
| private PendingIntent mPacRefreshIntent; |
| private ServiceConnection mConnection; |
| private ServiceConnection mProxyConnection; |
| private Context mContext; |
| |
| private int mCurrentDelay; |
| private int mLastPort; |
| |
| private volatile boolean mHasSentBroadcast; |
| private volatile boolean mHasDownloaded; |
| |
| private Handler mConnectivityHandler; |
| private final int mProxyMessage; |
| |
| /** |
| * Used for locking when setting mProxyService and all references to mCurrentPac. |
| */ |
| private final Object mProxyLock = new Object(); |
| |
| /** |
| * Runnable to download PAC script. |
| * The behavior relies on the assumption it always runs on mNetThread to guarantee that the |
| * latest data fetched from mPacUrl is stored in mProxyService. |
| */ |
| private Runnable mPacDownloader = new Runnable() { |
| @Override |
| @WorkerThread |
| public void run() { |
| String file; |
| final Uri pacUrl = mPacUrl; |
| if (Uri.EMPTY.equals(pacUrl)) return; |
| final int oldTag = TrafficStats.getAndSetThreadStatsTag(TrafficStats.TAG_SYSTEM_PAC); |
| try { |
| file = get(pacUrl); |
| } catch (IOException ioe) { |
| file = null; |
| Log.w(TAG, "Failed to load PAC file: " + ioe); |
| } finally { |
| TrafficStats.setThreadStatsTag(oldTag); |
| } |
| if (file != null) { |
| synchronized (mProxyLock) { |
| if (!file.equals(mCurrentPac)) { |
| setCurrentProxyScript(file); |
| } |
| } |
| mHasDownloaded = true; |
| sendProxyIfNeeded(); |
| longSchedule(); |
| } else { |
| reschedule(); |
| } |
| } |
| }; |
| |
| private final Handler mNetThreadHandler; |
| |
| class PacRefreshIntentReceiver extends BroadcastReceiver { |
| public void onReceive(Context context, Intent intent) { |
| mNetThreadHandler.post(mPacDownloader); |
| } |
| } |
| |
| public PacManager(Context context, Handler handler, int proxyMessage) { |
| mContext = context; |
| mLastPort = -1; |
| final HandlerThread netThread = new HandlerThread("android.pacmanager", |
| android.os.Process.THREAD_PRIORITY_DEFAULT); |
| netThread.start(); |
| mNetThreadHandler = new Handler(netThread.getLooper()); |
| |
| mPacRefreshIntent = PendingIntent.getBroadcast( |
| context, 0, new Intent(ACTION_PAC_REFRESH), 0); |
| context.registerReceiver(new PacRefreshIntentReceiver(), |
| new IntentFilter(ACTION_PAC_REFRESH)); |
| mConnectivityHandler = handler; |
| mProxyMessage = proxyMessage; |
| } |
| |
| private AlarmManager getAlarmManager() { |
| if (mAlarmManager == null) { |
| mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); |
| } |
| return mAlarmManager; |
| } |
| |
| /** |
| * Updates the PAC Manager with current Proxy information. This is called by |
| * the ProxyTracker directly before a broadcast takes place to allow |
| * the PacManager to indicate that the broadcast should not be sent and the |
| * PacManager will trigger a new broadcast when it is ready. |
| * |
| * @param proxy Proxy information that is about to be broadcast. |
| * @return Returns whether the broadcast should be sent : either DO_ or DONT_SEND_BROADCAST |
| */ |
| synchronized ToSendOrNotToSendBroadcast setCurrentProxyScriptUrl(ProxyInfo proxy) { |
| if (!Uri.EMPTY.equals(proxy.getPacFileUrl())) { |
| if (proxy.getPacFileUrl().equals(mPacUrl) && (proxy.getPort() > 0)) { |
| // Allow to send broadcast, nothing to do. |
| return ToSendOrNotToSendBroadcast.DO_SEND_BROADCAST; |
| } |
| mPacUrl = proxy.getPacFileUrl(); |
| mCurrentDelay = DELAY_1; |
| mHasSentBroadcast = false; |
| mHasDownloaded = false; |
| getAlarmManager().cancel(mPacRefreshIntent); |
| bind(); |
| return ToSendOrNotToSendBroadcast.DONT_SEND_BROADCAST; |
| } else { |
| getAlarmManager().cancel(mPacRefreshIntent); |
| synchronized (mProxyLock) { |
| mPacUrl = Uri.EMPTY; |
| mCurrentPac = null; |
| if (mProxyService != null) { |
| try { |
| mProxyService.stopPacSystem(); |
| } catch (RemoteException e) { |
| Log.w(TAG, "Failed to stop PAC service", e); |
| } finally { |
| unbind(); |
| } |
| } |
| } |
| return ToSendOrNotToSendBroadcast.DO_SEND_BROADCAST; |
| } |
| } |
| |
| /** |
| * Does a post and reports back the status code. |
| * |
| * @throws IOException if the URL is malformed, or the PAC file is too big. |
| */ |
| private static String get(Uri pacUri) throws IOException { |
| URL url = new URL(pacUri.toString()); |
| URLConnection urlConnection = url.openConnection(java.net.Proxy.NO_PROXY); |
| long contentLength = -1; |
| try { |
| contentLength = Long.parseLong(urlConnection.getHeaderField("Content-Length")); |
| } catch (NumberFormatException e) { |
| // Ignore |
| } |
| if (contentLength > MAX_PAC_SIZE) { |
| throw new IOException("PAC too big: " + contentLength + " bytes"); |
| } |
| ByteArrayOutputStream bytes = new ByteArrayOutputStream(); |
| byte[] buffer = new byte[1024]; |
| int count; |
| while ((count = urlConnection.getInputStream().read(buffer)) != -1) { |
| bytes.write(buffer, 0, count); |
| if (bytes.size() > MAX_PAC_SIZE) { |
| throw new IOException("PAC too big"); |
| } |
| } |
| return bytes.toString(); |
| } |
| |
| private int getNextDelay(int currentDelay) { |
| if (++currentDelay > DELAY_4) { |
| return DELAY_4; |
| } |
| return currentDelay; |
| } |
| |
| private void longSchedule() { |
| mCurrentDelay = DELAY_1; |
| setDownloadIn(DELAY_LONG); |
| } |
| |
| private void reschedule() { |
| mCurrentDelay = getNextDelay(mCurrentDelay); |
| setDownloadIn(mCurrentDelay); |
| } |
| |
| private String getPacChangeDelay() { |
| final ContentResolver cr = mContext.getContentResolver(); |
| |
| // Check system properties for the default value then use secure settings value, if any. |
| String defaultDelay = SystemProperties.get( |
| "conn." + Settings.Global.PAC_CHANGE_DELAY, |
| DEFAULT_DELAYS); |
| String val = Settings.Global.getString(cr, Settings.Global.PAC_CHANGE_DELAY); |
| return (val == null) ? defaultDelay : val; |
| } |
| |
| private long getDownloadDelay(int delayIndex) { |
| String[] list = getPacChangeDelay().split(" "); |
| if (delayIndex < list.length) { |
| return Long.parseLong(list[delayIndex]); |
| } |
| return 0; |
| } |
| |
| private void setDownloadIn(int delayIndex) { |
| long delay = getDownloadDelay(delayIndex); |
| long timeTillTrigger = 1000 * delay + SystemClock.elapsedRealtime(); |
| getAlarmManager().set(AlarmManager.ELAPSED_REALTIME, timeTillTrigger, mPacRefreshIntent); |
| } |
| |
| private void setCurrentProxyScript(String script) { |
| if (mProxyService == null) { |
| Log.e(TAG, "setCurrentProxyScript: no proxy service"); |
| return; |
| } |
| try { |
| mProxyService.setPacFile(script); |
| mCurrentPac = script; |
| } catch (RemoteException e) { |
| Log.e(TAG, "Unable to set PAC file", e); |
| } |
| } |
| |
| private void bind() { |
| if (mContext == null) { |
| Log.e(TAG, "No context for binding"); |
| return; |
| } |
| Intent intent = new Intent(); |
| intent.setClassName(PAC_PACKAGE, PAC_SERVICE); |
| if ((mProxyConnection != null) && (mConnection != null)) { |
| // Already bound: no need to bind again, just download the new file. |
| mNetThreadHandler.post(mPacDownloader); |
| return; |
| } |
| mConnection = new ServiceConnection() { |
| @Override |
| public void onServiceDisconnected(ComponentName component) { |
| synchronized (mProxyLock) { |
| mProxyService = null; |
| } |
| } |
| |
| @Override |
| public void onServiceConnected(ComponentName component, IBinder binder) { |
| synchronized (mProxyLock) { |
| try { |
| Log.d(TAG, "Adding service " + PAC_SERVICE_NAME + " " |
| + binder.getInterfaceDescriptor()); |
| } catch (RemoteException e1) { |
| Log.e(TAG, "Remote Exception", e1); |
| } |
| ServiceManager.addService(PAC_SERVICE_NAME, binder); |
| mProxyService = IProxyService.Stub.asInterface(binder); |
| if (mProxyService == null) { |
| Log.e(TAG, "No proxy service"); |
| } else { |
| try { |
| mProxyService.startPacSystem(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Unable to reach ProxyService - PAC will not be started", e); |
| } |
| mNetThreadHandler.post(mPacDownloader); |
| } |
| } |
| } |
| }; |
| mContext.bindService(intent, mConnection, |
| Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND | Context.BIND_NOT_VISIBLE); |
| |
| intent = new Intent(); |
| intent.setClassName(PROXY_PACKAGE, PROXY_SERVICE); |
| mProxyConnection = new ServiceConnection() { |
| @Override |
| public void onServiceDisconnected(ComponentName component) { |
| } |
| |
| @Override |
| public void onServiceConnected(ComponentName component, IBinder binder) { |
| IProxyCallback callbackService = IProxyCallback.Stub.asInterface(binder); |
| if (callbackService != null) { |
| try { |
| callbackService.getProxyPort(new IProxyPortListener.Stub() { |
| @Override |
| public void setProxyPort(int port) { |
| if (mLastPort != -1) { |
| // Always need to send if port changed |
| mHasSentBroadcast = false; |
| } |
| mLastPort = port; |
| if (port != -1) { |
| Log.d(TAG, "Local proxy is bound on " + port); |
| sendProxyIfNeeded(); |
| } else { |
| Log.e(TAG, "Received invalid port from Local Proxy," |
| + " PAC will not be operational"); |
| } |
| } |
| }); |
| } catch (RemoteException e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| }; |
| mContext.bindService(intent, mProxyConnection, |
| Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND | Context.BIND_NOT_VISIBLE); |
| } |
| |
| private void unbind() { |
| if (mConnection != null) { |
| mContext.unbindService(mConnection); |
| mConnection = null; |
| } |
| if (mProxyConnection != null) { |
| mContext.unbindService(mProxyConnection); |
| mProxyConnection = null; |
| } |
| mProxyService = null; |
| mLastPort = -1; |
| } |
| |
| private void sendPacBroadcast(ProxyInfo proxy) { |
| mConnectivityHandler.sendMessage(mConnectivityHandler.obtainMessage(mProxyMessage, proxy)); |
| } |
| |
| private synchronized void sendProxyIfNeeded() { |
| if (!mHasDownloaded || (mLastPort == -1)) { |
| return; |
| } |
| if (!mHasSentBroadcast) { |
| sendPacBroadcast(new ProxyInfo(mPacUrl, mLastPort)); |
| mHasSentBroadcast = true; |
| } |
| } |
| } |