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 
17 package androidx.appcompat.mms;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.net.ConnectivityManager;
24 import android.net.NetworkInfo;
25 import android.os.Build;
26 import android.os.SystemClock;
27 import android.util.Log;
28 
29 import java.lang.reflect.Method;
30 import java.util.Timer;
31 import 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  */
38 class 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      */
setNetworkAcquireTimeout(final long timeoutMs)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 
MmsNetworkManager(final Context context)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 
getConnectivityManager()113     ConnectivityManager getConnectivityManager() {
114         return mConnectivityManager;
115     }
116 
getHttpClient()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      */
acquireNetwork()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      */
releaseNetwork()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 
getApnName()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.
onMmsConnectivityChange(final Context context, final Intent intent)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 
unblockWait()246     private void unblockWait() {
247         synchronized (this) {
248             notifyAll();
249         }
250     }
251 
startNetworkExtensionTimerLocked()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 
stopNetworkExtensionTimerLocked()275     private void stopNetworkExtensionTimerLocked() {
276         if (mExtensionTimer != null) {
277             mExtensionTimer.cancel();
278             mExtensionTimer = null;
279         }
280     }
281 
extendMmsConnectivityLocked()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 
startMmsConnectivity()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 
endMmsConnectivity()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 
registerConnectivityChangeReceiverLocked()325     private void registerConnectivityChangeReceiverLocked() {
326         if (!mReceiverRegistered) {
327             mContext.registerReceiver(mConnectivityChangeReceiver, mConnectivityIntentFilter);
328             mReceiverRegistered = true;
329         }
330     }
331 
unregisterConnectivityChangeReceiverLocked()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      */
getConnectivityChangeNetworkType(final Intent intent)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 
getMmsConnectivityResultString(int result)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 
isMobileDataEnabled()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