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