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