chromium/content/public/android/java/src/org/chromium/content/browser/BackgroundSyncNetworkObserver.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.os.Process;

import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeClassQualifiedName;
import org.jni_zero.NativeMethods;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.net.ConnectionType;
import org.chromium.net.NetworkChangeNotifierAutoDetect;
import org.chromium.net.RegistrationPolicyAlwaysRegister;

import java.util.ArrayList;
import java.util.List;

/**
 * Contains the Java code used by the BackgroundSyncNetworkObserverAndroid C++ class.
 *
 * The purpose of this class is to listen for and forward network connectivity events to the
 * BackgroundSyncNetworkObserverAndroid objects even when the application is paused. The standard
 * NetworkChangeNotifier does not listen for connectivity events when the application is paused.
 *
 * This class maintains a NetworkChangeNotifierAutoDetect, which exists for as long as any
 * BackgroundSyncNetworkObserverAndroid objects are registered.
 *
 * This class lives on the main thread.
 */
@JNINamespace("content")
@VisibleForTesting
public class BackgroundSyncNetworkObserver implements NetworkChangeNotifierAutoDetect.Observer {
    private static final String TAG = "BgSyncNetObserver";
    private static boolean sSetConnectionTypeForTesting;
    private NetworkChangeNotifierAutoDetect mNotifier;

    // The singleton instance.
    @SuppressLint("StaticFieldLeak")
    private static BackgroundSyncNetworkObserver sInstance;

    // List of native observers. These are each called when the network state changes.
    private List<Long> mNativePtrs;

    private @ConnectionType int mLastBroadcastConnectionType;
    private boolean mHasBroadcastConnectionType;

    public static void setConnectionTypeForTesting(@ConnectionType int connectionType) {
        sSetConnectionTypeForTesting = true;
        getBackgroundSyncNetworkObserver().broadcastNetworkChangeIfNecessary(connectionType);
    }

    private BackgroundSyncNetworkObserver() {
        ThreadUtils.assertOnUiThread();
        mNativePtrs = new ArrayList<Long>();
        sSetConnectionTypeForTesting = false;
    }

    private static boolean canCreateObserver() {
        return ApiCompatibilityUtils.checkPermission(
                        ContextUtils.getApplicationContext(),
                        Manifest.permission.ACCESS_NETWORK_STATE,
                        Process.myPid(),
                        Process.myUid())
                == PackageManager.PERMISSION_GRANTED;
    }

    private static BackgroundSyncNetworkObserver getBackgroundSyncNetworkObserver() {
        if (sInstance == null) {
            sInstance = new BackgroundSyncNetworkObserver();
        }
        return sInstance;
    }

    @CalledByNative
    private static BackgroundSyncNetworkObserver createObserver(long nativePtr) {
        ThreadUtils.assertOnUiThread();

        getBackgroundSyncNetworkObserver().registerObserver(nativePtr);
        return sInstance;
    }

    private void registerObserver(final long nativePtr) {
        ThreadUtils.assertOnUiThread();
        if (!canCreateObserver()) {
            RecordHistogram.recordBooleanHistogram(
                    "BackgroundSync.NetworkObserver.HasPermission", false);
            return;
        }

        // Create the NetworkChangeNotifierAutoDetect if it does not exist already.
        if (mNotifier == null) {
            mNotifier =
                    new NetworkChangeNotifierAutoDetect(
                            this, new RegistrationPolicyAlwaysRegister());
            RecordHistogram.recordBooleanHistogram(
                    "BackgroundSync.NetworkObserver.HasPermission", true);
        }
        mNativePtrs.add(nativePtr);
        // TODO(crbug.com/40936429): remove this call if it is not necessary.
        mNotifier.updateCurrentNetworkState();
        BackgroundSyncNetworkObserverJni.get()
                .notifyConnectionTypeChanged(
                        nativePtr,
                        BackgroundSyncNetworkObserver.this,
                        mNotifier.getCurrentNetworkState().getConnectionType());
    }

    @CalledByNative
    private void removeObserver(long nativePtr) {
        ThreadUtils.assertOnUiThread();
        mNativePtrs.remove(nativePtr);
        // Destroy the NetworkChangeNotifierAutoDetect if there are no more observers.
        if (mNativePtrs.size() == 0 && mNotifier != null) {
            mNotifier.destroy();
            mNotifier = null;
        }
    }

    private void broadcastNetworkChangeIfNecessary(@ConnectionType int newConnectionType) {
        if (mHasBroadcastConnectionType && newConnectionType == mLastBroadcastConnectionType) {
            return;
        }

        mHasBroadcastConnectionType = true;
        mLastBroadcastConnectionType = newConnectionType;
        for (Long nativePtr : mNativePtrs) {
            BackgroundSyncNetworkObserverJni.get()
                    .notifyConnectionTypeChanged(
                            nativePtr, BackgroundSyncNetworkObserver.this, newConnectionType);
        }
    }

    @Override
    public void onConnectionTypeChanged(@ConnectionType int newConnectionType) {
        ThreadUtils.assertOnUiThread();
        if (sSetConnectionTypeForTesting) return;

        broadcastNetworkChangeIfNecessary(newConnectionType);
    }

    @Override
    public void onConnectionCostChanged(int newConnectionCost) {}

    @Override
    public void onConnectionSubtypeChanged(int newConnectionSubtype) {}

    @Override
    public void onNetworkConnect(long netId, @ConnectionType int connectionType) {
        ThreadUtils.assertOnUiThread();
        if (sSetConnectionTypeForTesting) return;

        // If we're in doze mode (N+ devices), onConnectionTypeChanged may not
        // be called, but this function should. So update the connection type
        // if necessary.
        broadcastNetworkChangeIfNecessary(connectionType);
    }

    @Override
    public void onNetworkSoonToDisconnect(long netId) {}

    @Override
    public void onNetworkDisconnect(long netId) {
        ThreadUtils.assertOnUiThread();
        if (sSetConnectionTypeForTesting) return;

        // If we're in doze mode (N+ devices), onConnectionTypeChanged may not
        // be called, but this function should. So update the connection type
        // if necessary.
        // TODO(crbug.com/40936429): remove this call if it is not necessary.
        mNotifier.updateCurrentNetworkState();
        broadcastNetworkChangeIfNecessary(mNotifier.getCurrentNetworkState().getConnectionType());
    }

    @Override
    public void purgeActiveNetworkList(long[] activeNetIds) {}

    @NativeMethods
    interface Natives {
        @NativeClassQualifiedName("BackgroundSyncNetworkObserverAndroid::Observer")
        void notifyConnectionTypeChanged(
                long nativePtr, BackgroundSyncNetworkObserver caller, int newConnectionType);
    }
}