| /* |
| * 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 android.support.v7.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; |
| } |
| } |