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