1/*
2 * Copyright (C) 2014 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
17package com.android.mms.service;
18
19import android.content.Context;
20import android.net.ConnectivityManager;
21import android.net.Network;
22import android.net.NetworkCapabilities;
23import android.net.NetworkInfo;
24import android.net.NetworkRequest;
25import android.os.Handler;
26import android.os.Looper;
27import android.os.SystemClock;
28
29import com.android.mms.service.exception.MmsNetworkException;
30
31/**
32 * Manages the MMS network connectivity
33 */
34public class MmsNetworkManager {
35    // Timeout used to call ConnectivityManager.requestNetwork
36    // Given that the telephony layer will retry on failures, this timeout should be high enough.
37    private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 30 * 60 * 1000;
38    // Wait timeout for this class, a little bit longer than the above timeout
39    // to make sure we don't bail prematurely
40    private static final int NETWORK_ACQUIRE_TIMEOUT_MILLIS =
41            NETWORK_REQUEST_TIMEOUT_MILLIS + (5 * 1000);
42    // Waiting time used before releasing a network prematurely. This allows the MMS download
43    // acknowledgement messages to be sent using the same network that was used to download the data
44    private static final int NETWORK_RELEASE_TIMEOUT_MILLIS = 5 * 1000;
45
46    private final Context mContext;
47
48    // The requested MMS {@link android.net.Network} we are holding
49    // We need this when we unbind from it. This is also used to indicate if the
50    // MMS network is available.
51    private Network mNetwork;
52    // The current count of MMS requests that require the MMS network
53    // If mMmsRequestCount is 0, we should release the MMS network.
54    private int mMmsRequestCount;
55    // This is really just for using the capability
56    private final NetworkRequest mNetworkRequest;
57    // The callback to register when we request MMS network
58    private ConnectivityManager.NetworkCallback mNetworkCallback;
59
60    private volatile ConnectivityManager mConnectivityManager;
61
62    // The MMS HTTP client for this network
63    private MmsHttpClient mMmsHttpClient;
64
65    // The handler used for delayed release of the network
66    private final Handler mReleaseHandler;
67
68    // The task that does the delayed releasing of the network.
69    private final Runnable mNetworkReleaseTask;
70
71    // The SIM ID which we use to connect
72    private final int mSubId;
73
74    /**
75     * Network callback for our network request
76     */
77    private class NetworkRequestCallback extends ConnectivityManager.NetworkCallback {
78        @Override
79        public void onAvailable(Network network) {
80            super.onAvailable(network);
81            LogUtil.i("NetworkCallbackListener.onAvailable: network=" + network);
82            synchronized (MmsNetworkManager.this) {
83                mNetwork = network;
84                MmsNetworkManager.this.notifyAll();
85            }
86        }
87
88        @Override
89        public void onLost(Network network) {
90            super.onLost(network);
91            LogUtil.w("NetworkCallbackListener.onLost: network=" + network);
92            synchronized (MmsNetworkManager.this) {
93                releaseRequestLocked(this);
94                MmsNetworkManager.this.notifyAll();
95            }
96        }
97
98        @Override
99        public void onUnavailable() {
100            super.onUnavailable();
101            LogUtil.w("NetworkCallbackListener.onUnavailable");
102            synchronized (MmsNetworkManager.this) {
103                releaseRequestLocked(this);
104                MmsNetworkManager.this.notifyAll();
105            }
106        }
107    }
108
109    public MmsNetworkManager(Context context, int subId) {
110        mContext = context;
111        mNetworkCallback = null;
112        mNetwork = null;
113        mMmsRequestCount = 0;
114        mConnectivityManager = null;
115        mMmsHttpClient = null;
116        mSubId = subId;
117        mReleaseHandler = new Handler(Looper.getMainLooper());
118        mNetworkRequest = new NetworkRequest.Builder()
119                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
120                .addCapability(NetworkCapabilities.NET_CAPABILITY_MMS)
121                .setNetworkSpecifier(Integer.toString(mSubId))
122                .build();
123
124        mNetworkReleaseTask = new Runnable() {
125            @Override
126            public void run() {
127                synchronized (this) {
128                    if (mMmsRequestCount < 1) {
129                        releaseRequestLocked(mNetworkCallback);
130                    }
131                }
132            }
133        };
134    }
135
136    /**
137     * Acquire the MMS network
138     *
139     * @param requestId request ID for logging
140     * @throws com.android.mms.service.exception.MmsNetworkException if we fail to acquire it
141     */
142    public void acquireNetwork(final String requestId) throws MmsNetworkException {
143        synchronized (this) {
144            // Since we are acquiring the network, remove the network release task if exists.
145            mReleaseHandler.removeCallbacks(mNetworkReleaseTask);
146            mMmsRequestCount += 1;
147            if (mNetwork != null) {
148                // Already available
149                LogUtil.d(requestId, "MmsNetworkManager: already available");
150                return;
151            }
152            // Not available, so start a new request if not done yet
153            if (mNetworkCallback == null) {
154                LogUtil.d(requestId, "MmsNetworkManager: start new network request");
155                startNewNetworkRequestLocked();
156            }
157            final long shouldEnd = SystemClock.elapsedRealtime() + NETWORK_ACQUIRE_TIMEOUT_MILLIS;
158            long waitTime = NETWORK_ACQUIRE_TIMEOUT_MILLIS;
159            while (waitTime > 0) {
160                try {
161                    this.wait(waitTime);
162                } catch (InterruptedException e) {
163                    LogUtil.w(requestId, "MmsNetworkManager: acquire network wait interrupted");
164                }
165                if (mNetwork != null) {
166                    // Success
167                    return;
168                }
169                // Calculate remaining waiting time to make sure we wait the full timeout period
170                waitTime = shouldEnd - SystemClock.elapsedRealtime();
171            }
172            // Timed out, so release the request and fail
173            LogUtil.e(requestId, "MmsNetworkManager: timed out");
174            releaseRequestLocked(mNetworkCallback);
175            throw new MmsNetworkException("Acquiring network timed out");
176        }
177    }
178
179    /**
180     * Release the MMS network when nobody is holding on to it.
181     *
182     * @param requestId request ID for logging
183     * @param shouldDelayRelease whether the release should be delayed for 5 seconds, the regular
184     *                           use case is to delay this for DownloadRequests to use the network
185     *                           for sending an acknowledgement on the same network
186     */
187    public void releaseNetwork(final String requestId, final boolean shouldDelayRelease) {
188        synchronized (this) {
189            if (mMmsRequestCount > 0) {
190                mMmsRequestCount -= 1;
191                LogUtil.d(requestId, "MmsNetworkManager: release, count=" + mMmsRequestCount);
192                if (mMmsRequestCount < 1) {
193                    if (shouldDelayRelease) {
194                        // remove previously posted task and post a delayed task on the release
195                        // handler to release the network
196                        mReleaseHandler.removeCallbacks(mNetworkReleaseTask);
197                        mReleaseHandler.postDelayed(mNetworkReleaseTask,
198                                NETWORK_RELEASE_TIMEOUT_MILLIS);
199                    } else {
200                        releaseRequestLocked(mNetworkCallback);
201                    }
202                }
203            }
204        }
205    }
206
207    /**
208     * Start a new {@link android.net.NetworkRequest} for MMS
209     */
210    private void startNewNetworkRequestLocked() {
211        final ConnectivityManager connectivityManager = getConnectivityManager();
212        mNetworkCallback = new NetworkRequestCallback();
213        connectivityManager.requestNetwork(
214                mNetworkRequest, mNetworkCallback, NETWORK_REQUEST_TIMEOUT_MILLIS);
215    }
216
217    /**
218     * Release the current {@link android.net.NetworkRequest} for MMS
219     *
220     * @param callback the {@link android.net.ConnectivityManager.NetworkCallback} to unregister
221     */
222    private void releaseRequestLocked(ConnectivityManager.NetworkCallback callback) {
223        if (callback != null) {
224            final ConnectivityManager connectivityManager = getConnectivityManager();
225            try {
226                connectivityManager.unregisterNetworkCallback(callback);
227            } catch (IllegalArgumentException e) {
228                // It is possible ConnectivityManager.requestNetwork may fail silently due
229                // to RemoteException. When that happens, we may get an invalid
230                // NetworkCallback, which causes an IllegalArgumentexception when we try to
231                // unregisterNetworkCallback. This exception in turn causes
232                // MmsNetworkManager to skip resetLocked() in the below. Thus MMS service
233                // would get stuck in the bad state until the device restarts. This fix
234                // catches the exception so that state clean up can be executed.
235                LogUtil.w("Unregister network callback exception", e);
236            }
237        }
238        resetLocked();
239    }
240
241    /**
242     * Reset the state
243     */
244    private void resetLocked() {
245        mNetworkCallback = null;
246        mNetwork = null;
247        mMmsRequestCount = 0;
248        mMmsHttpClient = null;
249    }
250
251    private ConnectivityManager getConnectivityManager() {
252        if (mConnectivityManager == null) {
253            mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
254                    Context.CONNECTIVITY_SERVICE);
255        }
256        return mConnectivityManager;
257    }
258
259    /**
260     * Get an MmsHttpClient for the current network
261     *
262     * @return The MmsHttpClient instance
263     */
264    public MmsHttpClient getOrCreateHttpClient() {
265        synchronized (this) {
266            if (mMmsHttpClient == null) {
267                if (mNetwork != null) {
268                    // Create new MmsHttpClient for the current Network
269                    mMmsHttpClient = new MmsHttpClient(mContext, mNetwork, mConnectivityManager);
270                }
271            }
272            return mMmsHttpClient;
273        }
274    }
275
276    /**
277     * Get the APN name for the active network
278     *
279     * @return The APN name if available, otherwise null
280     */
281    public String getApnName() {
282        Network network = null;
283        synchronized (this) {
284            if (mNetwork == null) {
285                return null;
286            }
287            network = mNetwork;
288        }
289        String apnName = null;
290        final ConnectivityManager connectivityManager = getConnectivityManager();
291        final NetworkInfo mmsNetworkInfo = connectivityManager.getNetworkInfo(network);
292        if (mmsNetworkInfo != null) {
293            apnName = mmsNetworkInfo.getExtraInfo();
294        }
295        return apnName;
296    }
297}
298