blob: 650562f97b770f6331b35228e4a929625e02305e [file] [log] [blame]
/*
* 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;
}
}