/* * 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; } }