1/*
2 * Copyright (C) 2015 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
17package android.support.v7.mms;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.net.ConnectivityManager;
24import android.net.NetworkInfo;
25import android.os.Build;
26import android.os.SystemClock;
27import android.util.Log;
28
29import java.lang.reflect.Method;
30import java.util.Timer;
31import java.util.TimerTask;
32
33/**
34 * Class manages MMS network connectivity using legacy platform APIs
35 * (deprecated since Android L) on pre-L devices (or when forced to
36 * be used on L and later)
37 */
38class MmsNetworkManager {
39    // Hidden platform constants
40    private static final String FEATURE_ENABLE_MMS = "enableMMS";
41    private static final String REASON_VOICE_CALL_ENDED = "2GVoiceCallEnded";
42    private static final int APN_ALREADY_ACTIVE     = 0;
43    private static final int APN_REQUEST_STARTED    = 1;
44    private static final int APN_TYPE_NOT_AVAILABLE = 2;
45    private static final int APN_REQUEST_FAILED     = 3;
46    private static final int APN_ALREADY_INACTIVE   = 4;
47    // A map from platform APN constant to text string
48    private static final String[] APN_RESULT_STRING = new String[]{
49            "already active",
50            "request started",
51            "type not available",
52            "request failed",
53            "already inactive",
54            "unknown",
55    };
56
57    private static final long NETWORK_ACQUIRE_WAIT_INTERVAL_MS = 15000;
58    private static final long DEFAULT_NETWORK_ACQUIRE_TIMEOUT_MS = 180000;
59    private static final String MMS_NETWORK_EXTENSION_TIMER = "mms_network_extension_timer";
60    private static final long MMS_NETWORK_EXTENSION_TIMER_WAIT_MS = 30000;
61
62    private static volatile long sNetworkAcquireTimeoutMs = DEFAULT_NETWORK_ACQUIRE_TIMEOUT_MS;
63
64    /**
65     * Set the network acquire timeout
66     *
67     * @param timeoutMs timeout in millisecond
68     */
69    static void setNetworkAcquireTimeout(final long timeoutMs) {
70        sNetworkAcquireTimeoutMs = timeoutMs;
71    }
72
73    private final Context mContext;
74    private final ConnectivityManager mConnectivityManager;
75
76    // If the connectivity intent receiver is registered
77    private boolean mReceiverRegistered;
78    // Count of requests that are using the MMS network
79    private int mUseCount;
80    // Count of requests that are waiting for connectivity (i.e. in acquireNetwork wait loop)
81    private int mWaitCount;
82    // Timer to extend the network connectivity
83    private Timer mExtensionTimer;
84
85    private final MmsHttpClient mHttpClient;
86
87    private final IntentFilter mConnectivityIntentFilter;
88    private final BroadcastReceiver mConnectivityChangeReceiver = new BroadcastReceiver() {
89        @Override
90        public void onReceive(final Context context, final Intent intent) {
91            if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
92                return;
93            }
94            final int networkType = getConnectivityChangeNetworkType(intent);
95            if (networkType != ConnectivityManager.TYPE_MOBILE_MMS) {
96                return;
97            }
98            onMmsConnectivityChange(context, intent);
99        }
100    };
101
102    MmsNetworkManager(final Context context) {
103        mContext = context;
104        mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
105                Context.CONNECTIVITY_SERVICE);
106        mHttpClient = new MmsHttpClient(mContext);
107        mConnectivityIntentFilter = new IntentFilter();
108        mConnectivityIntentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
109        mUseCount = 0;
110        mWaitCount = 0;
111    }
112
113    ConnectivityManager getConnectivityManager() {
114        return mConnectivityManager;
115    }
116
117    MmsHttpClient getHttpClient() {
118        return mHttpClient;
119    }
120
121    /**
122     * Synchronously acquire MMS network connectivity
123     *
124     * @throws MmsNetworkException If failed permanently or timed out
125     */
126    void acquireNetwork() throws MmsNetworkException {
127        Log.i(MmsService.TAG, "Acquire MMS network");
128        synchronized (this) {
129            try {
130                mUseCount++;
131                mWaitCount++;
132                if (mWaitCount == 1) {
133                    // Register the receiver for the first waiting request
134                    registerConnectivityChangeReceiverLocked();
135                }
136                long waitMs = sNetworkAcquireTimeoutMs;
137                final long beginMs = SystemClock.elapsedRealtime();
138                do {
139                    if (!isMobileDataEnabled()) {
140                        // Fast fail if mobile data is not enabled
141                        throw new MmsNetworkException("Mobile data is disabled");
142                    }
143                    // Always try to extend and check the MMS network connectivity
144                    // before we start waiting to make sure we don't miss the change
145                    // of MMS connectivity. As one example, some devices fail to send
146                    // connectivity change intent. So this would make sure we catch
147                    // the state change.
148                    if (extendMmsConnectivityLocked()) {
149                        // Connected
150                        return;
151                    }
152                    try {
153                        wait(Math.min(waitMs, NETWORK_ACQUIRE_WAIT_INTERVAL_MS));
154                    } catch (final InterruptedException e) {
155                        Log.w(MmsService.TAG, "Unexpected exception", e);
156                    }
157                    // Calculate the remaining time to wait
158                    waitMs = sNetworkAcquireTimeoutMs - (SystemClock.elapsedRealtime() - beginMs);
159                } while (waitMs > 0);
160                // Last check
161                if (extendMmsConnectivityLocked()) {
162                    return;
163                } else {
164                    // Reaching here means timed out.
165                    throw new MmsNetworkException("Acquiring MMS network timed out");
166                }
167            } finally {
168                mWaitCount--;
169                if (mWaitCount == 0) {
170                    // Receiver is used to listen to connectivity change and unblock
171                    // the waiting requests. If nobody's waiting on change, there is
172                    // no need for the receiver. The auto extension timer will try
173                    // to maintain the connectivity periodically.
174                    unregisterConnectivityChangeReceiverLocked();
175                }
176            }
177        }
178    }
179
180    /**
181     * Release MMS network connectivity. This is ref counted. So it only disconnect
182     * when the ref count is 0.
183     */
184    void releaseNetwork() {
185        Log.i(MmsService.TAG, "release MMS network");
186        synchronized (this) {
187            mUseCount--;
188            if (mUseCount == 0) {
189                stopNetworkExtensionTimerLocked();
190                endMmsConnectivity();
191            }
192        }
193    }
194
195    String getApnName() {
196        String apnName = null;
197        final NetworkInfo mmsNetworkInfo = mConnectivityManager.getNetworkInfo(
198                ConnectivityManager.TYPE_MOBILE_MMS);
199        if (mmsNetworkInfo != null) {
200            apnName = mmsNetworkInfo.getExtraInfo();
201        }
202        return apnName;
203    }
204
205    // Process mobile MMS connectivity change, waking up the waiting request thread
206    // in certain conditions:
207    // - Successfully connected
208    // - Failed permanently
209    // - Required another kickoff
210    // We don't initiate connection here but just notifyAll so the waiting request
211    // would wake up and retry connection before next wait.
212    private void onMmsConnectivityChange(final Context context, final Intent intent) {
213        if (mUseCount < 1) {
214            return;
215        }
216        final NetworkInfo mmsNetworkInfo =
217                mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_MMS);
218        // Check availability of the mobile network.
219        if (mmsNetworkInfo != null) {
220            if (REASON_VOICE_CALL_ENDED.equals(mmsNetworkInfo.getReason())) {
221                // This is a very specific fix to handle the case where the phone receives an
222                // incoming call during the time we're trying to setup the mms connection.
223                // When the call ends, restart the process of mms connectivity.
224                // Once the waiting request is unblocked, before the next wait, we would start
225                // MMS network again.
226                unblockWait();
227            } else {
228                final NetworkInfo.State state = mmsNetworkInfo.getState();
229                if (state == NetworkInfo.State.CONNECTED ||
230                        (state == NetworkInfo.State.DISCONNECTED && !isMobileDataEnabled())) {
231                    // Unblock the waiting request when we either connected
232                    // OR
233                    // disconnected due to mobile data disabled therefore needs to fast fail
234                    // (on some devices if mobile data disabled and starting MMS would cause
235                    // an immediate state change to disconnected, so causing a tight loop of
236                    // trying and failing)
237                    // Once the waiting request is unblocked, before the next wait, we would
238                    // check mobile data and start MMS network again. So we should catch
239                    // both the success and the fast failure.
240                    unblockWait();
241                }
242            }
243        }
244    }
245
246    private void unblockWait() {
247        synchronized (this) {
248            notifyAll();
249        }
250    }
251
252    private void startNetworkExtensionTimerLocked() {
253        if (mExtensionTimer == null) {
254            mExtensionTimer = new Timer(MMS_NETWORK_EXTENSION_TIMER, true/*daemon*/);
255            mExtensionTimer.schedule(
256                    new TimerTask() {
257                        @Override
258                        public void run() {
259                            synchronized (this) {
260                                if (mUseCount > 0) {
261                                    try {
262                                        // Try extending the connectivity
263                                        extendMmsConnectivityLocked();
264                                    } catch (final MmsNetworkException e) {
265                                        // Ignore the exception
266                                    }
267                                }
268                            }
269                        }
270                    },
271                    MMS_NETWORK_EXTENSION_TIMER_WAIT_MS);
272        }
273    }
274
275    private void stopNetworkExtensionTimerLocked() {
276        if (mExtensionTimer != null) {
277            mExtensionTimer.cancel();
278            mExtensionTimer = null;
279        }
280    }
281
282    private boolean extendMmsConnectivityLocked() throws MmsNetworkException {
283        final int result = startMmsConnectivity();
284        if (result == APN_ALREADY_ACTIVE) {
285            // Already active
286            startNetworkExtensionTimerLocked();
287            return true;
288        } else if (result != APN_REQUEST_STARTED) {
289            stopNetworkExtensionTimerLocked();
290            throw new MmsNetworkException("Cannot acquire MMS network: " +
291                    result + " - " + getMmsConnectivityResultString(result));
292        }
293        return false;
294    }
295
296    private int startMmsConnectivity() {
297        Log.i(MmsService.TAG, "Start MMS connectivity");
298        try {
299            final Method method = mConnectivityManager.getClass().getMethod(
300                "startUsingNetworkFeature", Integer.TYPE, String.class);
301            if (method != null) {
302                return (Integer) method.invoke(
303                    mConnectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
304            }
305        } catch (final Exception e) {
306            Log.w(MmsService.TAG, "ConnectivityManager.startUsingNetworkFeature failed " + e);
307        }
308        return APN_REQUEST_FAILED;
309    }
310
311    private void endMmsConnectivity() {
312        Log.i(MmsService.TAG, "End MMS connectivity");
313        try {
314            final Method method = mConnectivityManager.getClass().getMethod(
315                "stopUsingNetworkFeature", Integer.TYPE, String.class);
316            if (method != null) {
317                method.invoke(
318                        mConnectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
319            }
320        } catch (final Exception e) {
321            Log.w(MmsService.TAG, "ConnectivityManager.stopUsingNetworkFeature failed " + e);
322        }
323    }
324
325    private void registerConnectivityChangeReceiverLocked() {
326        if (!mReceiverRegistered) {
327            mContext.registerReceiver(mConnectivityChangeReceiver, mConnectivityIntentFilter);
328            mReceiverRegistered = true;
329        }
330    }
331
332    private void unregisterConnectivityChangeReceiverLocked() {
333        if (mReceiverRegistered) {
334            mContext.unregisterReceiver(mConnectivityChangeReceiver);
335            mReceiverRegistered = false;
336        }
337    }
338
339    /**
340     * The absence of a connection type.
341     */
342    private static final int TYPE_NONE = -1;
343
344    /**
345     * Get the network type of the connectivity change
346     *
347     * @param intent the broadcast intent of connectivity change
348     * @return The change's network type
349     */
350    private static int getConnectivityChangeNetworkType(final Intent intent) {
351        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
352            return intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, TYPE_NONE);
353        } else {
354            final NetworkInfo info = intent.getParcelableExtra(
355                    ConnectivityManager.EXTRA_NETWORK_INFO);
356            if (info != null) {
357                return info.getType();
358            }
359        }
360        return TYPE_NONE;
361    }
362
363    private static String getMmsConnectivityResultString(int result) {
364        if (result < 0 || result >= APN_RESULT_STRING.length) {
365            result = APN_RESULT_STRING.length - 1;
366        }
367        return APN_RESULT_STRING[result];
368    }
369
370    private boolean isMobileDataEnabled() {
371        try {
372            final Class cmClass = mConnectivityManager.getClass();
373            final Method method = cmClass.getDeclaredMethod("getMobileDataEnabled");
374            method.setAccessible(true); // Make the method callable
375            // get the setting for "mobile data"
376            return (Boolean) method.invoke(mConnectivityManager);
377        } catch (final Exception e) {
378            Log.w(MmsService.TAG, "TelephonyManager.getMobileDataEnabled failed", e);
379        }
380        return false;
381    }
382}
383