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