/* * Copyright (C) 2015 Google Inc. All Rights Reserved. * * 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.android.libraries.cast.companionlibrary.cast.reconnection; import static com.google.android.libraries.cast.companionlibrary.utils.LogUtils.LOGD; import static com.google.android.libraries.cast.companionlibrary.utils.LogUtils.LOGE; import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager; import com.google.android.libraries.cast.companionlibrary.cast.CastConfiguration; import com.google.android.libraries.cast.companionlibrary.cast.VideoCastManager; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException; import com.google.android.libraries.cast.companionlibrary.utils.LogUtils; import com.google.android.libraries.cast.companionlibrary.utils.Utils; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.NetworkInfo; import android.net.wifi.WifiManager; import android.os.IBinder; import android.os.SystemClock; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** * A service to run in the background when the playback of a media starts, to help with reconnection * if needed. Due to various reasons, connectivity to the cast device can be lost; for example wifi * radio may turn off when device goes to sleep or user may step outside of the wifi range, etc. * This service helps with recovering the connectivity when circumstances are right, for example * when user steps back within the wifi range, etc. In order to avoid ending up with a background * service that lingers around longer than it is needed, this implementation uses certain heuristics * to stop itself when needed. */ public class ReconnectionService extends Service { private static final String TAG = LogUtils.makeLogTag(ReconnectionService.class); // the tolerance for considering a time value (in millis) to be zero private static final long EPSILON_MS = 500; private static final int RECONNECTION_ATTEMPT_PERIOD_S = 15; private BroadcastReceiver mScreenOnOffBroadcastReceiver; private VideoCastManager mCastManager; private BroadcastReceiver mWifiBroadcastReceiver; private boolean mWifiConnectivity = true; private ScheduledFuture<?> mTerminationHandler; private final ScheduledExecutorService mScheduler = Executors.newScheduledThreadPool(1); @Override public int onStartCommand(Intent intent, int flags, int startId) { LOGD(TAG, "onStartCommand() is called"); setUpEndTimer(); return Service.START_STICKY; } @Override public void onCreate() { LOGD(TAG, "onCreate() is called"); mCastManager = VideoCastManager.getInstance(); if (!mCastManager.isConnected() && !mCastManager.isConnecting()) { mCastManager.reconnectSessionIfPossible(); } // register a broadcast receiver to be notified when screen goes on or off IntentFilter screenOnOffIntentFilter = new IntentFilter(Intent.ACTION_SCREEN_ON); screenOnOffIntentFilter.addAction(Intent.ACTION_SCREEN_OFF); mScreenOnOffBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { LOGD(TAG, "ScreenOnOffBroadcastReceiver: onReceive(): " + intent.getAction()); long timeLeft = getMediaRemainingTime(); if (timeLeft < EPSILON_MS) { handleTermination(); } } }; registerReceiver(mScreenOnOffBroadcastReceiver, screenOnOffIntentFilter); // register a wifi receiver that would be notified when the network state changes IntentFilter networkIntentFilter = new IntentFilter(); networkIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); mWifiBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); boolean connected = info.isConnected(); String networkSsid = connected ? Utils.getWifiSsid(context) : null; ReconnectionService.this.onWifiConnectivityChanged(connected, networkSsid); } } }; registerReceiver(mWifiBroadcastReceiver, networkIntentFilter); super.onCreate(); } /** * Since framework calls this method twice when a change happens, we are guarding against that * by caching the state the first time and avoiding the second call if it is the same status. */ public void onWifiConnectivityChanged(boolean connected, final String networkSsid) { LOGD(TAG, "WIFI connectivity changed to " + (connected ? "enabled" : "disabled")); if (connected && !mWifiConnectivity) { mWifiConnectivity = true; if (mCastManager.isFeatureEnabled(CastConfiguration.FEATURE_WIFI_RECONNECT)) { mCastManager.startCastDiscovery(); mCastManager.reconnectSessionIfPossible(RECONNECTION_ATTEMPT_PERIOD_S, networkSsid); } } else { mWifiConnectivity = connected; } } @Override public void onDestroy() { LOGD(TAG, "onDestroy()"); if (mScreenOnOffBroadcastReceiver != null) { unregisterReceiver(mScreenOnOffBroadcastReceiver); mScreenOnOffBroadcastReceiver = null; } if (mWifiBroadcastReceiver != null) { unregisterReceiver(mWifiBroadcastReceiver); mWifiBroadcastReceiver = null; } clearEndTimer(); super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return null; } final private Runnable mTerminationRunnable = new Runnable() { @Override public void run() { LOGD(TAG, "setUpEndTimer(): stopping ReconnectionService since reached the end of" + " allotted time"); handleTermination(); } }; private void setUpEndTimer() { LOGD(TAG, "setUpEndTimer(): setting up a timer for the end of current media"); long timeLeft = getMediaRemainingTime(); if (timeLeft <= 0) { stopSelf(); return; } clearEndTimer(); mTerminationHandler = mScheduler .schedule(mTerminationRunnable, timeLeft, TimeUnit.MILLISECONDS); } private void clearEndTimer() { if (mTerminationHandler != null && !mTerminationHandler.isCancelled()) { mTerminationHandler.cancel(true); } } private long getMediaRemainingTime() { long endTime = mCastManager.getPreferenceAccessor().getLongFromPreference( BaseCastManager.PREFS_KEY_MEDIA_END, 0); return endTime - SystemClock.elapsedRealtime(); } private void handleTermination() { if (!mCastManager.isConnected()) { mCastManager.clearMediaSession(); mCastManager.clearPersistedConnectionInfo(BaseCastManager.CLEAR_ALL); stopSelf(); } else { // since we are connected and our timer has gone off, lets update the time remaining // on the media (since media may have been paused) and reset teh time left long timeLeft = 0; try { timeLeft = mCastManager.isRemoteStreamLive() ? 0 : mCastManager.getMediaTimeRemaining(); } catch (TransientNetworkDisconnectionException | NoConnectionException e) { LOGE(TAG, "Failed to calculate the time left for media due to lack of connectivity", e); } if (timeLeft < EPSILON_MS) { // no time left stopSelf(); } else { // lets reset the counter mCastManager.getPreferenceAccessor().saveLongToPreference( BaseCastManager.PREFS_KEY_MEDIA_END, timeLeft + SystemClock.elapsedRealtime()); LOGD(TAG, "handleTermination(): resetting the timer"); setUpEndTimer(); } } } }