| /* |
| * 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.app.PendingIntent; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.net.ConnectivityMetricsEvent; |
| import android.net.ConnectivityMetricsLogger; |
| import android.net.IConnectivityMetricsLogger; |
| import android.os.Binder; |
| import android.os.Parcel; |
| import android.text.format.DateUtils; |
| import android.util.Log; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| |
| /** {@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() { |
| resetThrottlingCounters(System.currentTimeMillis()); |
| } |
| |
| @Override |
| public void onBootPhase(int phase) { |
| if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { |
| if (DBG) Log.d(TAG, "onBootPhase: PHASE_SYSTEM_SERVICES_READY"); |
| publishBinderService(ConnectivityMetricsLogger.CONNECTIVITY_METRICS_LOGGER_SERVICE, |
| mBinder); |
| mDnsListener = new DnsEventListenerService(getContext()); |
| publishBinderService(mDnsListener.SERVICE_NAME, mDnsListener); |
| } |
| } |
| |
| // TODO: read from system property |
| private final int MAX_NUMBER_OF_EVENTS = 1000; |
| |
| // TODO: read from system property |
| private final int EVENTS_NOTIFICATION_THRESHOLD = 300; |
| |
| // TODO: read from system property |
| private final int THROTTLING_TIME_INTERVAL_MILLIS = 60 * 60 * 1000; // 1 hour |
| |
| // TODO: read from system property |
| private final int THROTTLING_MAX_NUMBER_OF_MESSAGES_PER_COMPONENT = 1000; |
| |
| private int mEventCounter = 0; |
| |
| /** |
| * Reference of the last event in the list of cached events. |
| * |
| * When client of this service retrieves events by calling getEvents, it is passing |
| * ConnectivityMetricsEvent.Reference object. After getEvents returns, that object will |
| * contain this reference. The client can save it and use next time it calls getEvents. |
| * This way only new events will be returned. |
| */ |
| private long mLastEventReference = 0; |
| |
| private final int mThrottlingCounters[] = |
| new int[ConnectivityMetricsLogger.NUMBER_OF_COMPONENTS]; |
| |
| private long mThrottlingIntervalBoundaryMillis; |
| |
| private final ArrayDeque<ConnectivityMetricsEvent> mEvents = new ArrayDeque<>(); |
| |
| private DnsEventListenerService mDnsListener; |
| |
| private void enforceConnectivityInternalPermission() { |
| getContext().enforceCallingOrSelfPermission( |
| android.Manifest.permission.CONNECTIVITY_INTERNAL, |
| "MetricsLoggerService"); |
| } |
| |
| private void enforceDumpPermission() { |
| getContext().enforceCallingOrSelfPermission( |
| android.Manifest.permission.DUMP, |
| "MetricsLoggerService"); |
| } |
| |
| private void resetThrottlingCounters(long currentTimeMillis) { |
| for (int i = 0; i < mThrottlingCounters.length; i++) { |
| mThrottlingCounters[i] = 0; |
| } |
| mThrottlingIntervalBoundaryMillis = |
| currentTimeMillis + THROTTLING_TIME_INTERVAL_MILLIS; |
| } |
| |
| private void addEvent(ConnectivityMetricsEvent e) { |
| if (VDBG) { |
| Log.v(TAG, "writeEvent(" + e.toString() + ")"); |
| } |
| |
| while (mEvents.size() >= MAX_NUMBER_OF_EVENTS) { |
| mEvents.removeFirst(); |
| } |
| |
| mEvents.addLast(e); |
| } |
| |
| /** |
| * Implementation of the IConnectivityMetricsLogger interface. |
| */ |
| private final IConnectivityMetricsLogger.Stub mBinder = new IConnectivityMetricsLogger.Stub() { |
| |
| private final ArrayList<PendingIntent> mPendingIntents = new ArrayList<>(); |
| |
| @Override |
| protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP) |
| != PackageManager.PERMISSION_GRANTED) { |
| pw.println("Permission Denial: can't dump ConnectivityMetricsLoggerService " + |
| "from from pid=" + Binder.getCallingPid() + ", uid=" + |
| Binder.getCallingUid()); |
| return; |
| } |
| |
| boolean dumpSerializedSize = false; |
| boolean dumpEvents = false; |
| for (String arg : args) { |
| switch (arg) { |
| case "--events": |
| dumpEvents = true; |
| break; |
| |
| case "--size": |
| dumpSerializedSize = true; |
| break; |
| |
| case "--all": |
| dumpEvents = true; |
| dumpSerializedSize = true; |
| break; |
| } |
| } |
| |
| synchronized (mEvents) { |
| pw.println("Number of events: " + mEvents.size()); |
| if (mEvents.size() > 0) { |
| pw.println("Time span: " + |
| DateUtils.formatElapsedTime( |
| (System.currentTimeMillis() - mEvents.peekFirst().timestamp) |
| / 1000)); |
| } |
| |
| if (dumpSerializedSize) { |
| long dataSize = 0; |
| Parcel p = Parcel.obtain(); |
| for (ConnectivityMetricsEvent e : mEvents) { |
| dataSize += 16; // timestamp and 2 stamps |
| |
| p.writeParcelable(e.data, 0); |
| } |
| dataSize += p.dataSize(); |
| p.recycle(); |
| pw.println("Serialized data size: " + dataSize); |
| } |
| |
| if (dumpEvents) { |
| pw.println(); |
| pw.println("Events:"); |
| for (ConnectivityMetricsEvent e : mEvents) { |
| pw.println(e.toString()); |
| } |
| } |
| } |
| |
| if (!mPendingIntents.isEmpty()) { |
| pw.println(); |
| pw.println("Pending intents:"); |
| for (PendingIntent pi : mPendingIntents) { |
| pw.println(pi.toString()); |
| } |
| } |
| |
| pw.println(); |
| mDnsListener.dump(pw); |
| } |
| |
| public long logEvent(ConnectivityMetricsEvent event) { |
| ConnectivityMetricsEvent[] events = new ConnectivityMetricsEvent[]{event}; |
| return logEvents(events); |
| } |
| |
| /** |
| * @param events |
| * |
| * Note: All events must belong to the same component. |
| * |
| * @return 0 on success |
| * <0 if error happened |
| * >0 timestamp after which new events will be accepted |
| */ |
| public long logEvents(ConnectivityMetricsEvent[] events) { |
| enforceConnectivityInternalPermission(); |
| |
| if (events == null || events.length == 0) { |
| Log.wtf(TAG, "No events passed to logEvents()"); |
| return -1; |
| } |
| |
| int componentTag = events[0].componentTag; |
| if (componentTag < 0 || |
| componentTag >= ConnectivityMetricsLogger.NUMBER_OF_COMPONENTS) { |
| Log.wtf(TAG, "Unexpected tag: " + componentTag); |
| return -1; |
| } |
| |
| synchronized (mThrottlingCounters) { |
| long currentTimeMillis = System.currentTimeMillis(); |
| if (currentTimeMillis > mThrottlingIntervalBoundaryMillis) { |
| resetThrottlingCounters(currentTimeMillis); |
| } |
| |
| mThrottlingCounters[componentTag] += events.length; |
| |
| if (mThrottlingCounters[componentTag] > |
| THROTTLING_MAX_NUMBER_OF_MESSAGES_PER_COMPONENT) { |
| Log.w(TAG, "Too many events from #" + componentTag + |
| ". Block until " + mThrottlingIntervalBoundaryMillis); |
| |
| return mThrottlingIntervalBoundaryMillis; |
| } |
| } |
| |
| boolean sendPendingIntents = false; |
| |
| synchronized (mEvents) { |
| for (ConnectivityMetricsEvent e : events) { |
| if (e.componentTag != componentTag) { |
| Log.wtf(TAG, "Unexpected tag: " + e.componentTag); |
| return -1; |
| } |
| |
| addEvent(e); |
| } |
| |
| mLastEventReference += events.length; |
| |
| mEventCounter += events.length; |
| if (mEventCounter >= EVENTS_NOTIFICATION_THRESHOLD) { |
| mEventCounter = 0; |
| sendPendingIntents = true; |
| } |
| } |
| |
| if (sendPendingIntents) { |
| synchronized (mPendingIntents) { |
| for (PendingIntent pi : mPendingIntents) { |
| if (VDBG) Log.v(TAG, "Send pending intent"); |
| try { |
| pi.send(getContext(), 0, null, null, null); |
| } catch (PendingIntent.CanceledException e) { |
| Log.e(TAG, "Pending intent canceled: " + pi); |
| mPendingIntents.remove(pi); |
| } |
| } |
| } |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Retrieve events |
| * |
| * @param reference of the last event previously returned. The function will return |
| * events following it. |
| * If 0 then all events will be returned. |
| * After the function call it will contain reference of the |
| * last returned event. |
| * @return events |
| */ |
| public ConnectivityMetricsEvent[] getEvents(ConnectivityMetricsEvent.Reference reference) { |
| enforceDumpPermission(); |
| long ref = reference.getValue(); |
| if (VDBG) Log.v(TAG, "getEvents(" + ref + ")"); |
| |
| ConnectivityMetricsEvent[] result; |
| synchronized (mEvents) { |
| if (ref > mLastEventReference) { |
| Log.e(TAG, "Invalid reference"); |
| reference.setValue(mLastEventReference); |
| return null; |
| } |
| if (ref < mLastEventReference - mEvents.size()) { |
| ref = mLastEventReference - mEvents.size(); |
| } |
| |
| int numEventsToSkip = |
| mEvents.size() // Total number of events |
| - (int)(mLastEventReference - ref); // Number of events to return |
| |
| result = new ConnectivityMetricsEvent[mEvents.size() - numEventsToSkip]; |
| int i = 0; |
| for (ConnectivityMetricsEvent e : mEvents) { |
| if (numEventsToSkip > 0) { |
| numEventsToSkip--; |
| } else { |
| result[i++] = e; |
| } |
| } |
| } |
| |
| reference.setValue(mLastEventReference); |
| |
| return result; |
| } |
| |
| public boolean register(PendingIntent newEventsIntent) { |
| enforceDumpPermission(); |
| if (VDBG) Log.v(TAG, "register(" + newEventsIntent + ")"); |
| |
| synchronized (mPendingIntents) { |
| if (mPendingIntents.remove(newEventsIntent)) { |
| Log.w(TAG, "Replacing registered pending intent"); |
| } |
| mPendingIntents.add(newEventsIntent); |
| } |
| |
| return true; |
| } |
| |
| public void unregister(PendingIntent newEventsIntent) { |
| enforceDumpPermission(); |
| if (VDBG) Log.v(TAG, "unregister(" + newEventsIntent + ")"); |
| |
| synchronized (mPendingIntents) { |
| if (!mPendingIntents.remove(newEventsIntent)) { |
| Log.e(TAG, "Pending intent is not registered"); |
| } |
| } |
| } |
| }; |
| } |