/*
 * Copyright (C) 2016 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 com.android.server.connectivity;

import com.android.server.SystemService;

import android.content.Context;
import android.net.ConnectivityMetricsEvent;
import android.net.ConnectivityMetricsLogger;
import android.net.IConnectivityMetricsLogger;
import android.net.IConnectivityMetricsLoggerSubscriber;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Log;

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

/** {@hide} */
public class MetricsLoggerService extends SystemService {
    private static String TAG = "ConnectivityMetricsLoggerService";
    private static final boolean DBG = true;
    private static final boolean VDBG = false;

    public MetricsLoggerService(Context context) {
        super(context);
    }

    @Override
    public void onStart() {
    }

    @Override
    public void onBootPhase(int phase) {
        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
            Log.d(TAG, "onBootPhase: PHASE_SYSTEM_SERVICES_READY");
            publishBinderService(ConnectivityMetricsLogger.CONNECTIVITY_METRICS_LOGGER_SERVICE,
                    mBinder);
        }
    }

    private final int MAX_NUMBER_OF_EVENTS = 100;
    private final int MAX_TIME_OFFSET = 15*60*1000; // 15 minutes
    private final List<ConnectivityMetricsEvent> mEvents = new ArrayList<>();
    private long mLastSentEventTimeMillis = System.currentTimeMillis();

    private final void enforceConnectivityInternalPermission() {
        getContext().enforceCallingPermission(
                android.Manifest.permission.CONNECTIVITY_INTERNAL,
                "MetricsLoggerService");
    }

    /**
     * Implementation of the IConnectivityMetricsLogger interface.
     */
    private final IConnectivityMetricsLogger.Stub mBinder = new IConnectivityMetricsLogger.Stub() {

        private final ArrayMap<IConnectivityMetricsLoggerSubscriber,
                IBinder.DeathRecipient> mSubscribers = new ArrayMap<>();


        private ConnectivityMetricsEvent[] prepareEventsToSendIfReady() {
            ConnectivityMetricsEvent[] eventsToSend = null;
            final long currentTimeMillis = System.currentTimeMillis();
            final long timeOffset = currentTimeMillis - mLastSentEventTimeMillis;
            if (timeOffset >= MAX_TIME_OFFSET
                    || timeOffset < 0 // system time has changed
                    || mEvents.size() >= MAX_NUMBER_OF_EVENTS) {
                // batch events
                mLastSentEventTimeMillis = currentTimeMillis;
                eventsToSend = new ConnectivityMetricsEvent[mEvents.size()];
                mEvents.toArray(eventsToSend);
                mEvents.clear();
            }
            return eventsToSend;
        }

        private void maybeSendEventsToSubscribers(ConnectivityMetricsEvent[] eventsToSend) {
            if (eventsToSend == null || eventsToSend.length == 0) return;
            synchronized (mSubscribers) {
                for (IConnectivityMetricsLoggerSubscriber s : mSubscribers.keySet()) {
                    try {
                        s.onEvents(eventsToSend);
                    } catch (RemoteException ex) {
                        Log.e(TAG, "RemoteException " + ex);
                    }
                }
            }
        }

        public void logEvent(ConnectivityMetricsEvent event) {
            ConnectivityMetricsEvent[] events = new ConnectivityMetricsEvent[]{event};
            logEvents(events);
        }

        public void logEvents(ConnectivityMetricsEvent[] events) {
            enforceConnectivityInternalPermission();
            ConnectivityMetricsEvent[] eventsToSend;

            if (VDBG) {
                for (ConnectivityMetricsEvent e : events) {
                    Log.v(TAG, "writeEvent(" + e.toString() + ")");
                }
            }

            synchronized (mEvents) {
                for (ConnectivityMetricsEvent e : events) {
                    mEvents.add(e);
                }

                eventsToSend = prepareEventsToSendIfReady();
            }

            maybeSendEventsToSubscribers(eventsToSend);
        }

        public boolean subscribe(IConnectivityMetricsLoggerSubscriber subscriber) {
            enforceConnectivityInternalPermission();
            if (VDBG) Log.v(TAG, "subscribe");

            synchronized (mSubscribers) {
                if (mSubscribers.containsKey(subscriber)) {
                    Log.e(TAG, "subscriber is already subscribed");
                    return false;
                }
                final IConnectivityMetricsLoggerSubscriber s = subscriber;
                IBinder.DeathRecipient dr = new IBinder.DeathRecipient() {
                    @Override
                    public void binderDied() {
                        if (VDBG) Log.v(TAG, "subscriber died");
                        synchronized (mSubscribers) {
                            mSubscribers.remove(s);
                        }
                    }
                };

                try {
                    subscriber.asBinder().linkToDeath(dr, 0);
                    mSubscribers.put(subscriber, dr);
                } catch (RemoteException e) {
                    Log.e(TAG, "subscribe failed: " + e);
                    return false;
                }
            }

            return true;
        }

        public void unsubscribe(IConnectivityMetricsLoggerSubscriber subscriber) {
            enforceConnectivityInternalPermission();
            if (VDBG) Log.v(TAG, "unsubscribe");
            synchronized (mSubscribers) {
                IBinder.DeathRecipient dr = mSubscribers.remove(subscriber);
                if (dr == null) {
                    Log.e(TAG, "subscriber is not subscribed");
                    return;
                }
                subscriber.asBinder().unlinkToDeath(dr, 0);
            }
        }
    };
}
