// Copyright 2019 Google LLC
//
// 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.firebase.firestore.remote;

import static com.google.firebase.firestore.util.Assert.hardAssert;

import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.os.Build;
import com.google.firebase.firestore.util.Consumer;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Android implementation of ConnectivityMonitor. Parallel implementations exist for N+ and pre-N.
 *
 * <p>Implementation note: Most of the code here was shamelessly stolen from
 * https://github.com/grpc/grpc-java/blob/master/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java
 */
public final class AndroidConnectivityMonitor implements ConnectivityMonitor {

  private final Context context;
  @Nullable private final ConnectivityManager connectivityManager;
  @Nullable private Runnable unregisterRunnable;
  private final List<Consumer<NetworkStatus>> callbacks = new ArrayList<>();

  public AndroidConnectivityMonitor(Context context) {
    // This notnull restriction could be eliminated... the pre-N method doesn't
    // require a Context, and we could use that even on N+ if necessary.
    hardAssert(context != null, "Context must be non-null");
    this.context = context;

    connectivityManager =
        (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    configureNetworkMonitoring();
  }

  @Override
  public void addCallback(Consumer<NetworkStatus> callback) {
    callbacks.add(callback);
  }

  @Override
  public void shutdown() {
    if (unregisterRunnable != null) {
      unregisterRunnable.run();
      unregisterRunnable = null;
    }
  }

  private void configureNetworkMonitoring() {
    // Android N added the registerDefaultNetworkCallback API to listen to changes in the device's
    // default network. For earlier Android API levels, use the BroadcastReceiver API.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && connectivityManager != null) {
      final DefaultNetworkCallback defaultNetworkCallback = new DefaultNetworkCallback();
      connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
      unregisterRunnable =
          new Runnable() {
            @Override
            public void run() {
              connectivityManager.unregisterNetworkCallback(defaultNetworkCallback);
            }
          };
    } else {
      NetworkReceiver networkReceiver = new NetworkReceiver();
      IntentFilter networkIntentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
      context.registerReceiver(networkReceiver, networkIntentFilter);
      unregisterRunnable =
          new Runnable() {
            @Override
            public void run() {
              context.unregisterReceiver(networkReceiver);
            }
          };
    }
  }

  /** Respond to changes in the default network. Only used on API levels 24+. */
  @TargetApi(Build.VERSION_CODES.N)
  private class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback {
    @Override
    public void onAvailable(Network network) {
      for (Consumer<NetworkStatus> callback : callbacks) {
        callback.accept(NetworkStatus.REACHABLE);
      }
    }

    @Override
    public void onLost(Network network) {
      for (Consumer<NetworkStatus> callback : callbacks) {
        callback.accept(NetworkStatus.UNREACHABLE);
      }
    }
  }

  /** Respond to network changes. Only used on API levels < 24. */
  private class NetworkReceiver extends BroadcastReceiver {
    private boolean isConnected = false;

    @Override
    public void onReceive(Context context, Intent intent) {
      ConnectivityManager conn =
          (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
      NetworkInfo networkInfo = conn.getActiveNetworkInfo();
      boolean wasConnected = isConnected;
      isConnected = networkInfo != null && networkInfo.isConnected();
      if (isConnected && !wasConnected) {
        for (Consumer<NetworkStatus> callback : callbacks) {
          callback.accept(NetworkStatus.REACHABLE);
        }
      } else if (!isConnected && wasConnected) {
        for (Consumer<NetworkStatus> callback : callbacks) {
          callback.accept(NetworkStatus.UNREACHABLE);
        }
      }
    }
  }
}

@@ -2267,24 +2267,57 @@
 callback) %7B%0A
+    synchronized (callbacks) %7B%0A  
     callback
@@ -2333,16 +2333,22 @@
 lback);%0A
+    %7D%0A
   %7D%0A%0A  @
@@ -3916,32 +3916,67 @@
 work network) %7B%0A
+      synchronized (callbacks) %7B%0A  
       for (Consu
@@ -4018,32 +4018,34 @@
 acks) %7B%0A        
+ 
+ 
 callback.accept(
@@ -4062,32 +4062,42 @@
 tus.REACHABLE);%0A
+        %7D%0A
       %7D%0A    %7D%0A%0A 
@@ -4147,24 +4147,59 @@
  network) %7B%0A
+      synchronized (callbacks) %7B%0A  
       for (C
@@ -4245,32 +4245,34 @@
 acks) %7B%0A        
+ 
+ 
 callback.accept(
@@ -4295,24 +4295,34 @@
 REACHABLE);%0A
+        %7D%0A
       %7D%0A    
@@ -4899,32 +4899,69 @@
 wasConnected) %7B%0A
+        synchronized (callbacks) %7B%0A  
         for (Con
@@ -4997,32 +4997,34 @@
  : callbacks) %7B%0A
+  
           callba
@@ -5051,32 +5051,44 @@
 tus.REACHABLE);%0A
+          %7D%0A
         %7D%0A      
@@ -5122,32 +5122,69 @@
 wasConnected) %7B%0A
+        synchronized (callbacks) %7B%0A  
         for (Con
@@ -5220,32 +5220,34 @@
  : callbacks) %7B%0A
+  
           callba
@@ -5276,32 +5276,44 @@
 s.UNREACHABLE);%0A
+          %7D%0A
         %7D%0A      

Synchronize access to AndroidConnectivityMonitor.callbacks (#341)