Home | History | Annotate | Download | only in mms
      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 android.support.v7.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      */
     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