aboutsummaryrefslogtreecommitdiff
path: root/src/android/support/v7/mms/MmsNetworkManager.java
blob: 059ca8f4fac9ff04cdcf6c175419522f08b95dc2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.appcompat.mms;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.SystemClock;
import android.util.Log;

import java.lang.reflect.Method;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Class manages MMS network connectivity using legacy platform APIs
 * (deprecated since Android L) on pre-L devices (or when forced to
 * be used on L and later)
 */
class MmsNetworkManager {
    // Hidden platform constants
    private static final String FEATURE_ENABLE_MMS = "enableMMS";
    private static final String REASON_VOICE_CALL_ENDED = "2GVoiceCallEnded";
    private static final int APN_ALREADY_ACTIVE     = 0;
    private static final int APN_REQUEST_STARTED    = 1;
    private static final int APN_TYPE_NOT_AVAILABLE = 2;
    private static final int APN_REQUEST_FAILED     = 3;
    private static final int APN_ALREADY_INACTIVE   = 4;
    // A map from platform APN constant to text string
    private static final String[] APN_RESULT_STRING = new String[]{
            "already active",
            "request started",
            "type not available",
            "request failed",
            "already inactive",
            "unknown",
    };

    private static final long NETWORK_ACQUIRE_WAIT_INTERVAL_MS = 15000;
    private static final long DEFAULT_NETWORK_ACQUIRE_TIMEOUT_MS = 180000;
    private static final String MMS_NETWORK_EXTENSION_TIMER = "mms_network_extension_timer";
    private static final long MMS_NETWORK_EXTENSION_TIMER_WAIT_MS = 30000;

    private static volatile long sNetworkAcquireTimeoutMs = DEFAULT_NETWORK_ACQUIRE_TIMEOUT_MS;

    /**
     * Set the network acquire timeout
     *
     * @param timeoutMs timeout in millisecond
     */
    static void setNetworkAcquireTimeout(final long timeoutMs) {
        sNetworkAcquireTimeoutMs = timeoutMs;
    }

    private final Context mContext;
    private final ConnectivityManager mConnectivityManager;

    // If the connectivity intent receiver is registered
    private boolean mReceiverRegistered;
    // Count of requests that are using the MMS network
    private int mUseCount;
    // Count of requests that are waiting for connectivity (i.e. in acquireNetwork wait loop)
    private int mWaitCount;
    // Timer to extend the network connectivity
    private Timer mExtensionTimer;

    private final MmsHttpClient mHttpClient;

    private final IntentFilter mConnectivityIntentFilter;
    private final BroadcastReceiver mConnectivityChangeReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
                return;
            }
            final int networkType = getConnectivityChangeNetworkType(intent);
            if (networkType != ConnectivityManager.TYPE_MOBILE_MMS) {
                return;
            }
            onMmsConnectivityChange(context, intent);
        }
    };

    MmsNetworkManager(final Context context) {
        mContext = context;
        mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
                Context.CONNECTIVITY_SERVICE);
        mHttpClient = new MmsHttpClient(mContext);
        mConnectivityIntentFilter = new IntentFilter();
        mConnectivityIntentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
        mUseCount = 0;
        mWaitCount = 0;
    }

    ConnectivityManager getConnectivityManager() {
        return mConnectivityManager;
    }

    MmsHttpClient getHttpClient() {
        return mHttpClient;
    }

    /**
     * Synchronously acquire MMS network connectivity
     *
     * @throws MmsNetworkException If failed permanently or timed out
     */
    void acquireNetwork() throws MmsNetworkException {
        Log.i(MmsService.TAG, "Acquire MMS network");
        synchronized (this) {
            try {
                mUseCount++;
                mWaitCount++;
                if (mWaitCount == 1) {
                    // Register the receiver for the first waiting request
                    registerConnectivityChangeReceiverLocked();
                }
                long waitMs = sNetworkAcquireTimeoutMs;
                final long beginMs = SystemClock.elapsedRealtime();
                do {
                    if (!isMobileDataEnabled()) {
                        // Fast fail if mobile data is not enabled
                        throw new MmsNetworkException("Mobile data is disabled");
                    }
                    // Always try to extend and check the MMS network connectivity
                    // before we start waiting to make sure we don't miss the change
                    // of MMS connectivity. As one example, some devices fail to send
                    // connectivity change intent. So this would make sure we catch
                    // the state change.
                    if (extendMmsConnectivityLocked()) {
                        // Connected
                        return;
                    }
                    try {
                        wait(Math.min(waitMs, NETWORK_ACQUIRE_WAIT_INTERVAL_MS));
                    } catch (final InterruptedException e) {
                        Log.w(MmsService.TAG, "Unexpected exception", e);
                    }
                    // Calculate the remaining time to wait
                    waitMs = sNetworkAcquireTimeoutMs - (SystemClock.elapsedRealtime() - beginMs);
                } while (waitMs > 0);
                // Last check
                if (extendMmsConnectivityLocked()) {
                    return;
                } else {
                    // Reaching here means timed out.
                    throw new MmsNetworkException("Acquiring MMS network timed out");
                }
            } finally {
                mWaitCount--;
                if (mWaitCount == 0) {
                    // Receiver is used to listen to connectivity change and unblock
                    // the waiting requests. If nobody's waiting on change, there is
                    // no need for the receiver. The auto extension timer will try
                    // to maintain the connectivity periodically.
                    unregisterConnectivityChangeReceiverLocked();
                }
            }
        }
    }

    /**
     * Release MMS network connectivity. This is ref counted. So it only disconnect
     * when the ref count is 0.
     */
    void releaseNetwork() {
        Log.i(MmsService.TAG, "release MMS network");
        synchronized (this) {
            mUseCount--;
            if (mUseCount == 0) {
                stopNetworkExtensionTimerLocked();
                endMmsConnectivity();
            }
        }
    }

    String getApnName() {
        String apnName = null;
        final NetworkInfo mmsNetworkInfo = mConnectivityManager.getNetworkInfo(
                ConnectivityManager.TYPE_MOBILE_MMS);
        if (mmsNetworkInfo != null) {
            apnName = mmsNetworkInfo.getExtraInfo();
        }
        return apnName;
    }

    // Process mobile MMS connectivity change, waking up the waiting request thread
    // in certain conditions:
    // - Successfully connected
    // - Failed permanently
    // - Required another kickoff
    // We don't initiate connection here but just notifyAll so the waiting request
    // would wake up and retry connection before next wait.
    private void onMmsConnectivityChange(final Context context, final Intent intent) {
        if (mUseCount < 1) {
            return;
        }
        final NetworkInfo mmsNetworkInfo =
                mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_MMS);
        // Check availability of the mobile network.
        if (mmsNetworkInfo != null) {
            if (REASON_VOICE_CALL_ENDED.equals(mmsNetworkInfo.getReason())) {
                // This is a very specific fix to handle the case where the phone receives an
                // incoming call during the time we're trying to setup the mms connection.
                // When the call ends, restart the process of mms connectivity.
                // Once the waiting request is unblocked, before the next wait, we would start
                // MMS network again.
                unblockWait();
            } else {
                final NetworkInfo.State state = mmsNetworkInfo.getState();
                if (state == NetworkInfo.State.CONNECTED ||
                        (state == NetworkInfo.State.DISCONNECTED && !isMobileDataEnabled())) {
                    // Unblock the waiting request when we either connected
                    // OR
                    // disconnected due to mobile data disabled therefore needs to fast fail
                    // (on some devices if mobile data disabled and starting MMS would cause
                    // an immediate state change to disconnected, so causing a tight loop of
                    // trying and failing)
                    // Once the waiting request is unblocked, before the next wait, we would
                    // check mobile data and start MMS network again. So we should catch
                    // both the success and the fast failure.
                    unblockWait();
                }
            }
        }
    }

    private void unblockWait() {
        synchronized (this) {
            notifyAll();
        }
    }

    private void startNetworkExtensionTimerLocked() {
        if (mExtensionTimer == null) {
            mExtensionTimer = new Timer(MMS_NETWORK_EXTENSION_TIMER, true/*daemon*/);
            mExtensionTimer.schedule(
                    new TimerTask() {
                        @Override
                        public void run() {
                            synchronized (this) {
                                if (mUseCount > 0) {
                                    try {
                                        // Try extending the connectivity
                                        extendMmsConnectivityLocked();
                                    } catch (final MmsNetworkException e) {
                                        // Ignore the exception
                                    }
                                }
                            }
                        }
                    },
                    MMS_NETWORK_EXTENSION_TIMER_WAIT_MS);
        }
    }

    private void stopNetworkExtensionTimerLocked() {
        if (mExtensionTimer != null) {
            mExtensionTimer.cancel();
            mExtensionTimer = null;
        }
    }

    private boolean extendMmsConnectivityLocked() throws MmsNetworkException {
        final int result = startMmsConnectivity();
        if (result == APN_ALREADY_ACTIVE) {
            // Already active
            startNetworkExtensionTimerLocked();
            return true;
        } else if (result != APN_REQUEST_STARTED) {
            stopNetworkExtensionTimerLocked();
            throw new MmsNetworkException("Cannot acquire MMS network: " +
                    result + " - " + getMmsConnectivityResultString(result));
        }
        return false;
    }

    private int startMmsConnectivity() {
        Log.i(MmsService.TAG, "Start MMS connectivity");
        try {
            final Method method = mConnectivityManager.getClass().getMethod(
                "startUsingNetworkFeature", Integer.TYPE, String.class);
            if (method != null) {
                return (Integer) method.invoke(
                    mConnectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
            }
        } catch (final Exception e) {
            Log.w(MmsService.TAG, "ConnectivityManager.startUsingNetworkFeature failed " + e);
        }
        return APN_REQUEST_FAILED;
    }

    private void endMmsConnectivity() {
        Log.i(MmsService.TAG, "End MMS connectivity");
        try {
            final Method method = mConnectivityManager.getClass().getMethod(
                "stopUsingNetworkFeature", Integer.TYPE, String.class);
            if (method != null) {
                method.invoke(
                        mConnectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
            }
        } catch (final Exception e) {
            Log.w(MmsService.TAG, "ConnectivityManager.stopUsingNetworkFeature failed " + e);
        }
    }

    private void registerConnectivityChangeReceiverLocked() {
        if (!mReceiverRegistered) {
            mContext.registerReceiver(mConnectivityChangeReceiver, mConnectivityIntentFilter);
            mReceiverRegistered = true;
        }
    }

    private void unregisterConnectivityChangeReceiverLocked() {
        if (mReceiverRegistered) {
            mContext.unregisterReceiver(mConnectivityChangeReceiver);
            mReceiverRegistered = false;
        }
    }

    /**
     * The absence of a connection type.
     */
    private static final int TYPE_NONE = -1;

    /**
     * Get the network type of the connectivity change
     *
     * @param intent the broadcast intent of connectivity change
     * @return The change's network type
     */
    private static int getConnectivityChangeNetworkType(final Intent intent) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            return intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, TYPE_NONE);
        } else {
            final NetworkInfo info = intent.getParcelableExtra(
                    ConnectivityManager.EXTRA_NETWORK_INFO);
            if (info != null) {
                return info.getType();
            }
        }
        return TYPE_NONE;
    }

    private static String getMmsConnectivityResultString(int result) {
        if (result < 0 || result >= APN_RESULT_STRING.length) {
            result = APN_RESULT_STRING.length - 1;
        }
        return APN_RESULT_STRING[result];
    }

    private boolean isMobileDataEnabled() {
        try {
            final Class cmClass = mConnectivityManager.getClass();
            final Method method = cmClass.getDeclaredMethod("getMobileDataEnabled");
            method.setAccessible(true); // Make the method callable
            // get the setting for "mobile data"
            return (Boolean) method.invoke(mConnectivityManager);
        } catch (final Exception e) {
            Log.w(MmsService.TAG, "TelephonyManager.getMobileDataEnabled failed", e);
        }
        return false;
    }
}