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