summaryrefslogtreecommitdiffstats
path: root/src/android/support/v7/mms/MmsService.java
blob: 650562f97b770f6331b35228e4a929625e02305e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
/*
 * 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.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process;
import android.telephony.SmsManager;
import android.util.Log;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;

/**
 * Service to execute MMS requests using deprecated legacy APIs on older platform (prior to L)
 */
public class MmsService extends Service {
    static final String TAG = "MmsLib";

    //The default number of threads allowed to run MMS requests
    private static final int DEFAULT_THREAD_POOL_SIZE = 4;
    // Delay before stopping the service
    private static final int SERVICE_STOP_DELAY_MILLIS = 2000;

    private static final String EXTRA_REQUEST = "request";
    private static final String EXTRA_MYPID = "mypid";

    private static final String WAKELOCK_ID = "mmslib_wakelock";

    /**
     * Thread pool size for each request queue
     */
    private static volatile int sThreadPoolSize = DEFAULT_THREAD_POOL_SIZE;

    /**
     * Optional wake lock to use
     */
    private static volatile boolean sUseWakeLock = true;
    private static volatile PowerManager.WakeLock sWakeLock = null;
    private static final Object sWakeLockLock = new Object();

    /**
     * Carrier configuration values loader
     */
    private static volatile CarrierConfigValuesLoader sCarrierConfigValuesLoader = null;

    /**
     * APN loader
     */
    private static volatile ApnSettingsLoader sApnSettingsLoader = null;

    /**
     * UserAgent and UA Prof URL loader
     */
    private static volatile UserAgentInfoLoader sUserAgentInfoLoader = null;

    /**
     * Set the size of thread pool for request execution.
     * Default is DEFAULT_THREAD_POOL_SIZE
     *
     * @param size thread pool size
     */
    static void setThreadPoolSize(final int size) {
        sThreadPoolSize = size;
    }

    /**
     * Set whether to use wake lock
     *
     * @param useWakeLock true to use wake lock, false otherwise
     */
    static void setUseWakeLock(final boolean useWakeLock) {
        sUseWakeLock = useWakeLock;
    }

    /**
     * Set the optional carrier config values
     *
     * @param loader the carrier config values loader
     */
    static void setCarrierConfigValuesLoader(final CarrierConfigValuesLoader loader) {
        sCarrierConfigValuesLoader = loader;
    }

    /**
     * Get the current carrier config values loader
     *
     * @return the carrier config values loader currently set
     */
    static CarrierConfigValuesLoader getCarrierConfigValuesLoader() {
        return sCarrierConfigValuesLoader;
    }

    /**
     * Set APN settings loader
     *
     * @param loader the APN settings loader
     */
    static void setApnSettingsLoader(final ApnSettingsLoader loader) {
        sApnSettingsLoader = loader;
    }

    /**
     * Get the current APN settings loader
     *
     * @return the APN settings loader currently set
     */
    static ApnSettingsLoader getApnSettingsLoader() {
        return sApnSettingsLoader;
    }

    /**
     * Set user agent info loader
     *
     * @param loader the user agent info loader
     */
    static void setUserAgentInfoLoader(final UserAgentInfoLoader loader) {
        sUserAgentInfoLoader = loader;
    }

    /**
     * Get the current user agent info loader
     *
     * @return the user agent info loader currently set
     */
    static UserAgentInfoLoader getUserAgentInfoLoader() {
        return sUserAgentInfoLoader;
    }

    /**
     * Make sure loaders are not null. Set to default if that's the case
     *
     * @param context the Context to use
     */
    private static void ensureLoaders(final Context context) {
        if (sUserAgentInfoLoader == null) {
            sUserAgentInfoLoader = new DefaultUserAgentInfoLoader(context);
        }
        if (sCarrierConfigValuesLoader == null) {
            sCarrierConfigValuesLoader = new DefaultCarrierConfigValuesLoader(context);
        }
        if (sApnSettingsLoader == null) {
            sApnSettingsLoader = new DefaultApnSettingsLoader(context);
        }
    }

    /**
     * Acquire the wake lock
     *
     * @param context the context to use
     */
    private static void acquireWakeLock(final Context context) {
        synchronized (sWakeLockLock) {
            if (sWakeLock == null) {
                final PowerManager pm =
                        (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                sWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_ID);
            }
            sWakeLock.acquire();
        }
    }

    /**
     * Release the wake lock
     */
    private static void releaseWakeLock() {
        boolean releasedEmptyWakeLock = false;
        synchronized (sWakeLockLock) {
            if (sWakeLock != null) {
                sWakeLock.release();
            } else {
                releasedEmptyWakeLock = true;
            }
        }
        if (releasedEmptyWakeLock) {
            Log.w(TAG, "Releasing empty wake lock");
        }
    }

    /**
     * Check if wake lock is not held (e.g. when service stops)
     */
    private static void verifyWakeLockNotHeld() {
        boolean wakeLockHeld = false;
        synchronized (sWakeLockLock) {
            wakeLockHeld = sWakeLock != null && sWakeLock.isHeld();
        }
        if (wakeLockHeld) {
            Log.e(TAG, "Wake lock still held!");
        }
    }

    // Remember my PID to discard restarted intent
    private static volatile int sMyPid = -1;

    /**
     * Get the current PID
     *
     * @return the current PID
     */
    private static int getMyPid() {
        if (sMyPid < 0) {
            sMyPid = Process.myPid();
        }
        return sMyPid;
    }

    /**
     * Check if the intent is coming from this process
     *
     * @param intent the incoming intent for the service
     * @return true if the intent is from the current process
     */
    private static boolean fromThisProcess(final Intent intent) {
        final int pid = intent.getIntExtra(EXTRA_MYPID, -1);
        return pid == getMyPid();
    }

    // Request execution thread pools. One thread pool for sending and one for downloading.
    // The size of the thread pool controls the parallelism of request execution.
    // See {@link setThreadPoolSize}
    private ExecutorService[] mExecutors = new ExecutorService[2];

    // Active request count
    private int mActiveRequestCount;
    // The latest intent startId, used for safely stopping service
    private int mLastStartId;

    private MmsNetworkManager mNetworkManager;

    // Handler for scheduling service stop
    private final Handler mHandler = new Handler();
    // Service stop task
    private final Runnable mServiceStopRunnable = new Runnable() {
        @Override
        public void run() {
            tryStopService();
        }
    };

    /**
     * Start the service with a request
     *
     * @param context the Context to use
     * @param request the request to start
     */
    public static void startRequest(final Context context, final MmsRequest request) {
        final boolean useWakeLock = sUseWakeLock;
        request.setUseWakeLock(useWakeLock);
        final Intent intent = new Intent(context, MmsService.class);
        intent.putExtra(EXTRA_REQUEST, request);
        intent.putExtra(EXTRA_MYPID, getMyPid());
        if (useWakeLock) {
            acquireWakeLock(context);
        }
        if (context.startService(intent) == null) {
            if (useWakeLock) {
                releaseWakeLock();
            }
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();

        ensureLoaders(this);

        for (int i = 0; i < mExecutors.length; i++) {
            mExecutors[i] = Executors.newFixedThreadPool(sThreadPoolSize);
        }

        mNetworkManager = new MmsNetworkManager(this);

        synchronized (this) {
            mActiveRequestCount = 0;
            mLastStartId = -1;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        for (ExecutorService executor : mExecutors) {
            executor.shutdown();
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Always remember the latest startId for use when we try releasing the service
        synchronized (this) {
            mLastStartId = startId;
        }
        boolean scheduled = false;
        if (intent != null) {
            // There is a rare situation that right after a intent is started,
            // the service gets killed. Then the service will restart with
            // the old intent which we don't want it to run since it will
            // break our assumption for wake lock. Check the process ID
            // embedded in the intent to make sure it is indeed from the
            // the current life of this service.
            if (fromThisProcess(intent)) {
                final MmsRequest request = intent.getParcelableExtra(EXTRA_REQUEST);
                if (request != null) {
                    try {
                        retainService(request, new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    request.execute(
                                            MmsService.this,
                                            mNetworkManager,
                                            getApnSettingsLoader(),
                                            getCarrierConfigValuesLoader(),
                                            getUserAgentInfoLoader());
                                } catch (Exception e) {
                                    Log.w(TAG, "Unexpected execution failure", e);
                                } finally {
                                    if (request.getUseWakeLock()) {
                                        releaseWakeLock();
                                    }
                                    releaseService();
                                }
                            }
                        });
                        scheduled = true;
                    } catch (RejectedExecutionException e) {
                        // Rare thing happened. Send back failure using the pending intent
                        // and also release the wake lock.
                        Log.w(TAG, "Executing request failed " + e);
                        request.returnResult(this, SmsManager.MMS_ERROR_UNSPECIFIED,
                                null/*response*/, 0/*httpStatusCode*/);
                        if (request.getUseWakeLock()) {
                            releaseWakeLock();
                        }
                    }
                } else {
                    Log.w(TAG, "Empty request");
                }
            } else {
                Log.w(TAG, "Got a restarted intent from previous incarnation");
            }
        } else {
            Log.w(TAG, "Empty intent");
        }
        if (!scheduled) {
            // If the request is not started successfully, we need to try shutdown the service
            // if nobody is using it.
            tryScheduleStop();
        }
        return START_NOT_STICKY;
    }

    /**
     * Retain the service for executing the request in service thread pool
     *
     * @param request The request to execute
     * @param runnable The runnable to run the request in thread pool
     */
    private void retainService(final MmsRequest request, final Runnable runnable) {
        final ExecutorService executor = getRequestExecutor(request);
        synchronized (this) {
            executor.execute(runnable);
            mActiveRequestCount++;
        }
    }

    /**
     * Release the service from the request. If nobody is using it, schedule service stop.
     */
    private void releaseService() {
        synchronized (this) {
            mActiveRequestCount--;
            if (mActiveRequestCount <= 0) {
                mActiveRequestCount = 0;
                rescheduleServiceStop();
            }
        }
    }

    /**
     * Schedule the service stop if there is no active request
     */
    private void tryScheduleStop() {
        synchronized (this) {
            if (mActiveRequestCount == 0) {
                rescheduleServiceStop();
            }
        }
    }

    /**
     * Reschedule service stop task
     */
    private void rescheduleServiceStop() {
        mHandler.removeCallbacks(mServiceStopRunnable);
        mHandler.postDelayed(mServiceStopRunnable, SERVICE_STOP_DELAY_MILLIS);
    }

    /**
     * Really try to stop the service if there is not active request
     */
    private void tryStopService() {
        Boolean stopped = null;
        synchronized (this) {
            if (mActiveRequestCount == 0) {
                stopped = stopSelfResult(mLastStartId);
            }
        }
        logServiceStop(stopped);
    }

    /**
     * Log the result of service stopping. Also check wake lock status when service stops.
     *
     * @param stopped Not empty if service stop is performed: true if really stopped, false
     *                if cancelled.
     */
    private void logServiceStop(final Boolean stopped) {
        if (stopped != null) {
            if (stopped) {
                Log.i(TAG, "Service successfully stopped");
                verifyWakeLockNotHeld();
            } else {
                Log.i(TAG, "Service stopping cancelled");
            }
        }
    }

    private ExecutorService getRequestExecutor(final MmsRequest request) {
        if (request instanceof SendRequest) {
            // Send
            return mExecutors[0];
        } else {
            // Download
            return mExecutors[1];
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}