/* * Copyright 2011 Google Inc. * * 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 com.google.ipc.invalidation.ticl.android.c2dm; import com.google.common.base.Preconditions; import com.google.ipc.invalidation.external.client.SystemResources.Logger; import com.google.ipc.invalidation.external.client.android.service.AndroidLogger; import android.content.Context; import android.os.Build; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * Singleton that manages wake locks identified by a key. Wake locks are refcounted so if they are * acquired multiple times with the same key they will not unlocked until they are released an * equivalent number of times. */ public class WakeLockManager { /** Logger. */ private static final Logger logger = AndroidLogger.forTag("WakeLockMgr"); /** Lock over all state. Must be acquired by all non-private methods. */ private static final Object LOCK = new Object(); /** * SDK_INT version taken from android.BUILD.VERSION_CODE.ICE_CREAM_SANDWICH. We cannot reference * the field directly because if it is not inlined by the Java compiler, it will not be available * in the earlier versions of Android for which the version check in acquire() exists. */ private static final int ICE_CREAM_SANDWICH_VERSION_CODE = 14; /** Singleton instance. */ private static WakeLockManager theManager; /** Wake locks by key. */ private final Map<Object, PowerManager.WakeLock> wakeLocks = new HashMap<Object, PowerManager.WakeLock>(); private final PowerManager powerManager; private final Context applicationContext; private WakeLockManager(Context context) { powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); applicationContext = Preconditions.checkNotNull(context); } /** Returns the wake lock manager. */ public static WakeLockManager getInstance(Context context) { Preconditions.checkNotNull(context); Preconditions.checkNotNull(context.getApplicationContext()); synchronized (LOCK) { if (theManager == null) { theManager = new WakeLockManager(context.getApplicationContext()); } else { Preconditions.checkState(theManager.applicationContext == context.getApplicationContext(), "Provided context %s does not match stored context %s", context.getApplicationContext(), theManager.applicationContext); } return theManager; } } /** * Acquires a wake lock identified by the {@code key} that will be automatically released after at * most {@code timeoutMs}. */ public void acquire(Object key, int timeoutMs) { synchronized (LOCK) { cleanup(); Preconditions.checkNotNull(key, "Key can not be null"); // Prior to ICS, acquiring a lock with a timeout and then explicitly releasing the lock // results in runtime errors. We rely on the invalidation system correctly releasing locks // rather than defensively requesting a timeout. if (Build.VERSION.SDK_INT >= ICE_CREAM_SANDWICH_VERSION_CODE) { log(key, "acquiring with timeout " + timeoutMs); getWakeLock(key).acquire(timeoutMs); } else { log(key, "acquiring"); getWakeLock(key).acquire(); } } } /** * Releases the wake lock identified by the {@code key} if it is currently held. */ public void release(Object key) { synchronized (LOCK) { cleanup(); Preconditions.checkNotNull(key, "Key can not be null"); PowerManager.WakeLock wakelock = getWakeLock(key); // If the lock is not held (if for instance there is a wake lock timeout), we cannot release // again without triggering a RuntimeException. if (!wakelock.isHeld()) { logger.warning("Over-release of wakelock: %s", key); return; } // We held the wake lock recently, so it's likely safe to release it. Between the isHeld() // check and the release() call, the wake lock may time out however and we catch the resulting // RuntimeException. try { wakelock.release(); } catch (RuntimeException exception) { logger.warning("Over-release of wakelock: %s, %s", key, exception); } log(key, "released"); // Now if the lock is not held, that means we were the last holder, so we should remove it // from the map. if (!wakelock.isHeld()) { wakeLocks.remove(key); log(key, "freed"); } } } /** * Returns whether there is currently a wake lock held for the provided {@code key}. */ public boolean isHeld(Object key) { synchronized (LOCK) { cleanup(); Preconditions.checkNotNull(key, "Key can not be null"); if (!wakeLocks.containsKey(key)) { return false; } return getWakeLock(key).isHeld(); } } /** Returns whether the manager has any active (held) wake locks. */ public boolean hasWakeLocks() { synchronized (LOCK) { cleanup(); return !wakeLocks.isEmpty(); } } /** Discards (without releasing) all wake locks. */ public void resetForTest() { synchronized (LOCK) { cleanup(); wakeLocks.clear(); } } /** * Returns a wake lock to use for {@code key}. If a lock is already present in the map, * returns that lock. Else, creates a new lock, installs it in the map, and returns it. * <p> * REQUIRES: caller must hold {@link #LOCK}. */ private PowerManager.WakeLock getWakeLock(Object key) { if (key == null) { throw new IllegalArgumentException("Key can not be null"); } PowerManager.WakeLock wakeLock = wakeLocks.get(key); if (wakeLock == null) { wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, key.toString()); wakeLocks.put(key, wakeLock); } return wakeLock; } /** * Removes any non-held wake locks from {@link #wakeLocks}. Such locks may be present when a * wake lock acquired with a timeout is not released before the timeout expires. We only * explicitly remove wake locks from the map when {@link #release} is called, so a timeout results * in a non-held wake lock in the map. * <p> * Must be called as the first line of all non-private methods. * <p> * REQUIRES: caller must hold {@link #LOCK}. */ private void cleanup() { Iterator<Map.Entry<Object, WakeLock>> wakeLockIter = wakeLocks.entrySet().iterator(); // Check each map entry. while (wakeLockIter.hasNext()) { Map.Entry<Object, WakeLock> wakeLockEntry = wakeLockIter.next(); if (!wakeLockEntry.getValue().isHeld()) { // Warn and remove the entry from the map if the lock is not held. logger.warning("Found un-held wakelock '%s' -- timed-out?", wakeLockEntry.getKey()); wakeLockIter.remove(); } } } /** Logs a debug message that {@code action} has occurred for {@code key}. */ private static void log(Object key, String action) { logger.fine("WakeLock %s for key: {%s}", action, key); } }