Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ee32ea42 authored by Andy Hung's avatar Andy Hung Committed by Android (Google) Code Review
Browse files

Merge "MediaMetrics: Add AnalyticsActions and AnalyticsState"

parents 8fd55b2c 0f7ad8cd
Loading
Loading
Loading
Loading
+74 −0
Original line number Diff line number Diff line
@@ -1004,7 +1004,81 @@ public:
        const char *toCString();
        const char *toCString(int version);

    /**
     * Returns true if the item has a property with a target value.
     *
     * If propName is nullptr, hasPropElem() returns false.
     *
     * \param propName is the property name.
     * \param elem is the value to match.  std::monostate matches any.
     */
    bool hasPropElem(const char *propName, const Prop::Elem& elem) const {
        if (propName == nullptr) return false;
        const Prop::Elem *e = get(propName);
        return e != nullptr && (std::holds_alternative<std::monostate>(elem) || elem == *e);
    }

    /**
     * Returns -2, -1, 0 (success) if the item has a property (wildcard matched) with a
     * target value.
     *
     * The enum RecursiveWildcardCheck designates the meaning of the returned value.
     *
     * RECURSIVE_WILDCARD_CHECK_NO_MATCH_NO_WILDCARD = -2,
     * RECURSIVE_WILDCARD_CHECK_NO_MATCH_WILDCARD_FOUND = -1,
     * RECURSIVE_WILDCARD_CHECK_MATCH_FOUND = 0.
     *
     * If url is nullptr, RECURSIVE_WILDCARD_CHECK_NO_MATCH_NO_WILDCARD is returned.
     *
     * \param url is the full item + property name, which may have wildcards '*'
     *            denoting an arbitrary sequence of 0 or more characters.
     * \param elem is the target property value to match. std::monostate matches any.
     * \return 0 if the property was matched,
     *         -1 if the property was not matched and a wildcard char was encountered,
     *         -2 if the property was not matched with no wildcard char encountered.
     */
    enum RecursiveWildcardCheck {
        RECURSIVE_WILDCARD_CHECK_NO_MATCH_NO_WILDCARD = -2,
        RECURSIVE_WILDCARD_CHECK_NO_MATCH_WILDCARD_FOUND = -1,
        RECURSIVE_WILDCARD_CHECK_MATCH_FOUND = 0,
    };

    enum RecursiveWildcardCheck recursiveWildcardCheckElem(
        const char *url, const Prop::Elem& elem) const {
        if (url == nullptr) return RECURSIVE_WILDCARD_CHECK_NO_MATCH_NO_WILDCARD;
        return recursiveWildcardCheckElem(getKey().c_str(), url, elem);
    }

private:

    enum RecursiveWildcardCheck recursiveWildcardCheckElem(
            const char *itemKeyPtr, const char *url, const Prop::Elem& elem) const {
        for (; *url && *itemKeyPtr; ++url, ++itemKeyPtr) {
            if (*url != *itemKeyPtr) {
                if (*url == '*') { // wildcard
                    ++url;
                    while (true) {
                        if (recursiveWildcardCheckElem(itemKeyPtr, url, elem)
                                == RECURSIVE_WILDCARD_CHECK_MATCH_FOUND) {
                            return RECURSIVE_WILDCARD_CHECK_MATCH_FOUND;
                        }
                        if (*itemKeyPtr == 0) break;
                        ++itemKeyPtr;
                    }
                    return RECURSIVE_WILDCARD_CHECK_NO_MATCH_WILDCARD_FOUND;
                }
                return RECURSIVE_WILDCARD_CHECK_NO_MATCH_NO_WILDCARD;
            }
        }
        if (itemKeyPtr[0] != 0 || url[0] != '.') {
            return RECURSIVE_WILDCARD_CHECK_NO_MATCH_NO_WILDCARD;
        }
        const char *propName = url + 1; // skip the '.'
        return hasPropElem(propName, elem)
                ? RECURSIVE_WILDCARD_CHECK_MATCH_FOUND
                : RECURSIVE_WILDCARD_CHECK_NO_MATCH_NO_WILDCARD;
    }

    // handle Parcel version 0
    int32_t writeToParcel0(Parcel *) const;
    int32_t readFromParcel0(const Parcel&);
+150 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.
 */

#pragma once

#include <media/MediaMetricsItem.h>
#include <mutex>

namespace android::mediametrics {

/**
 * AnalyticsActions consists of a map of pairs <trigger, action> which
 * are evaluated for a given incoming MediaMetrics item.
 *
 * A vector of Actions are returned from getActionsForItem() which
 * should be executed outside of any locks.
 *
 * Mediametrics assumes weak consistency, which is fine as the analytics database
 * is generally strictly increasing in size (until gc removes values that are
 * supposedly no longer needed).
 */

class AnalyticsActions {
public:

    using Elem = mediametrics::Item::Prop::Elem;
    /**
     * Trigger: a pair consisting of
     * std::string: A wildcard url specifying a property in the item,
     *              where '*' indicates 0 or more arbitrary characters
     *              for the item key match.
     * Elem: A value that needs to match exactly.
     *
     * Trigger is used in a map sort;  default less with std::string as primary key.
     * The wildcard accepts a string with '*' as being 0 or more arbitrary
     * characters for the item key match.  A wildcard is preferred over general
     * regexp for simple fast lookup.
     *
     * TODO: incorporate a regexp option.
     */
    using Trigger = std::pair<std::string, Elem>;

    /**
     * Function: The function to be executed.
     */
    using Function = std::function<
            void(const std::shared_ptr<const mediametrics::Item>& item)>;

    /**
     * Action:  An action to execute.  This is a shared pointer to Function.
     */
    using Action = std::shared_ptr<Function>;

    /**
     * Adds a new action.
     *
     * \param url references a property in the item with wildcards
     * \param value references a value (cast to Elem automatically)
     *              so be careful of the type.  It must be one of
     *              the types acceptable to Elem.
     * \param action is a function or lambda to execute if the url matches value
     *               in the item.
     */
    template <typename T, typename U, typename A>
    void addAction(T&& url, U&& value, A&& action) {
        std::lock_guard l(mLock);
        mFilters[ { std::forward<T>(url), std::forward<U>(value) } ]
                = std::forward<A>(action);
    }

    // TODO: remove an action.

    /**
     * Get all the actions triggered for a particular item.
     *
     * \param item to be analyzed for actions.
     */
    std::vector<Action>
    getActionsForItem(const std::shared_ptr<const mediametrics::Item>& item) {
        std::vector<Action> actions;
        std::lock_guard l(mLock);

        // Essentially the code looks like this:
        /*
        for (auto &[trigger, action] : mFilters) {
            if (isMatch(trigger, item)) {
                actions.push_back(action);
            }
        }
        */

        // Optimization: there should only be one match for a non-wildcard url.
        auto it = mFilters.upper_bound( {item->getKey(), std::monostate{} });
        if (it != mFilters.end()) {
            const auto &[trigger, action] = *it;
            if (isMatch(trigger, item)) {
                actions.push_back(action);
            }
        }

        // Optimization: for wildcard URLs we go backwards until there is no
        // match with the prefix before the wildcard.
        while (it != mFilters.begin()) {  // this walks backwards, cannot start at begin.
            const auto &[trigger, action] = *--it;  // look backwards
            int ret = isWildcardMatch(trigger, item);
            if (ret == mediametrics::Item::RECURSIVE_WILDCARD_CHECK_MATCH_FOUND) {
                actions.push_back(action);    // match found.
            } else if (ret == mediametrics::Item::RECURSIVE_WILDCARD_CHECK_NO_MATCH_NO_WILDCARD) {
                break;                        // no match before wildcard.
            }
            // a wildcard was encountered when matching prefix, so we should check again.
        }
        return actions;
    }

private:

    static inline bool isMatch(const Trigger& trigger,
            const std::shared_ptr<const mediametrics::Item>& item) {
        const auto& [key, elem] = trigger;
        if (!startsWith(key, item->getKey())) return false;
        // The trigger key is in format (item key).propName, so + 1 skips '.' delimeter.
        const char *propName = key.c_str() + item->getKey().size() + 1;
        return item->hasPropElem(propName, elem);
    }

    static inline int isWildcardMatch(const Trigger& trigger,
            const std::shared_ptr<const mediametrics::Item>& item) {
        const auto& [key, elem] = trigger;
        return item->recursiveWildcardCheckElem(key.c_str(), elem);
    }

    mutable std::mutex mLock;
    std::map<Trigger, Action> mFilters; // GUARDED_BY mLock
};

} // namespace android::mediametrics
+113 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.
 */

#pragma once

#include "TimeMachine.h"
#include "TransactionLog.h"

namespace android::mediametrics {

/**
 * AnalyticsState consists of a TimeMachine and TransactionLog for a set
 * of MediaMetrics Items.
 *
 * One can add new Items with the submit() method.
 *
 * The AnalyticsState may be cleared or duplicated to preserve state after crashes
 * in services are detected.
 *
 * As its members may not be moveable due to mutexes, we use this encapsulation
 * with a shared pointer in order to save it or duplicate it.
 */
class AnalyticsState {
public:
    /**
     * Returns success if AnalyticsState accepts the item.
     *
     * A trusted source can create a new key, an untrusted source
     * can only modify the key if the uid will match that authorized
     * on the existing key.
     *
     * \param item the item to be submitted.
     * \param isTrusted whether the transaction comes from a trusted source.
     *        In this case, a trusted source is verified by binder
     *        UID to be a system service by MediaMetrics service.
     *        Do not use true if you haven't really checked!
     *
     * \return NO_ERROR on success or
     *         PERMISSION_DENIED if the item cannot be put into the AnalyticsState.
     */
    status_t submit(const std::shared_ptr<const mediametrics::Item>& item, bool isTrusted) {
        return mTimeMachine.put(item, isTrusted) ?: mTransactionLog.put(item);
    }

    /**
     * Returns a pair consisting of the dump string, and the number of lines in the string.
     *
     * The number of lines in the returned pair is used as an optimization
     * for subsequent line limiting.
     *
     * The TimeMachine and the TransactionLog are dumped separately under
     * different locks, so may not be 100% consistent with the last data
     * delivered.
     *
     * \param lines the maximum number of lines in the string returned.
     */
    std::pair<std::string, int32_t> dump(int32_t lines = INT32_MAX) const {
        std::stringstream ss;
        int32_t ll = lines;

        if (ll > 0) {
            ss << "TransactionLog:\n";
            --ll;
        }
        if (ll > 0) {
            auto [s, l] = mTransactionLog.dump(ll);
            ss << s;
            ll -= l;
        }
        if (ll > 0) {
            ss << "TimeMachine:\n";
            --ll;
        }
        if (ll > 0) {
            auto [s, l] = mTimeMachine.dump(ll);
            ss << s;
            ll -= l;
        }
        return { ss.str(), lines - ll };
    }

    /**
     * Clears the AnalyticsState.
     */
    void clear() {
        mTimeMachine.clear();
        mTransactionLog.clear();
    }

private:
    // Note: TimeMachine and TransactionLog are individually locked.
    // Access to these objects under multiple threads will be weakly synchronized,
    // which is acceptable as modifications only increase the history (or with GC,
    // eliminates very old history).

    TimeMachine    mTimeMachine;
    TransactionLog mTransactionLog;
};

} // namespace android::mediametrics
+36 −12
Original line number Diff line number Diff line
@@ -27,6 +27,22 @@ namespace android::mediametrics {
AudioAnalytics::AudioAnalytics()
{
    ALOGD("%s", __func__);

    // Add action to save AnalyticsState if audioserver is restarted.
    // This triggers on an item of "audio.flinger"
    // with a property "event" set to "AudioFlinger" (the constructor).
    mActions.addAction(
        "audio.flinger.event",
        std::string("AudioFlinger"),
        std::make_shared<AnalyticsActions::Function>(
            [this](const std::shared_ptr<const android::mediametrics::Item> &){
                ALOGW("Audioflinger() constructor event detected");
                mPreviousAnalyticsState.set(std::make_shared<AnalyticsState>(
                        *mAnalyticsState.get()));
                // Note: get returns shared_ptr temp, whose lifetime is extended
                // to end of full expression.
                mAnalyticsState->clear();  // TODO: filter the analytics state.
            }));
}

AudioAnalytics::~AudioAnalytics()
@@ -37,11 +53,14 @@ AudioAnalytics::~AudioAnalytics()
status_t AudioAnalytics::submit(
        const std::shared_ptr<const mediametrics::Item>& item, bool isTrusted)
{
    if (startsWith(item->getKey(), "audio.")) {
        return mTimeMachine.put(item, isTrusted)
                ?: mTransactionLog.put(item);
    }
    return BAD_VALUE;
    if (!startsWith(item->getKey(), "audio.")) return BAD_VALUE;
    status_t status = mAnalyticsState->submit(item, isTrusted);
    if (status != NO_ERROR) return status;  // may not be permitted.

    // Only if the item was successfully submitted (permission)
    // do we check triggered actions.
    checkActions(item);
    return NO_ERROR;
}

std::pair<std::string, int32_t> AudioAnalytics::dump(int32_t lines) const
@@ -50,24 +69,29 @@ std::pair<std::string, int32_t> AudioAnalytics::dump(int32_t lines) const
    int32_t ll = lines;

    if (ll > 0) {
        ss << "TransactionLog:\n";
        --ll;
    }
    if (ll > 0) {
        auto [s, l] = mTransactionLog.dump(ll);
        auto [s, l] = mAnalyticsState->dump(ll);
        ss << s;
        ll -= l;
    }
    if (ll > 0) {
        ss << "TimeMachine:\n";
        ss << "Prior audioserver state:\n";
        --ll;
    }
    if (ll > 0) {
        auto [s, l] = mTimeMachine.dump(ll);
        auto [s, l] = mPreviousAnalyticsState->dump(ll);
        ss << s;
        ll -= l;
    }
    return { ss.str(), lines - ll };
}

void AudioAnalytics::checkActions(const std::shared_ptr<const mediametrics::Item>& item)
{
    auto actions = mActions.getActionsForItem(item); // internally locked.
    // Execute actions with no lock held.
    for (const auto& action : actions) {
        (*action)(item);
    }
}

} // namespace android
+23 −6
Original line number Diff line number Diff line
@@ -16,8 +16,9 @@

#pragma once

#include "TimeMachine.h"
#include "TransactionLog.h"
#include "AnalyticsActions.h"
#include "AnalyticsState.h"
#include "Wrap.h"

namespace android::mediametrics {

@@ -27,7 +28,6 @@ public:
    AudioAnalytics();
    ~AudioAnalytics();

    // TODO: update with conditions for keys.
    /**
     * Returns success if AudioAnalytics recognizes item.
     *
@@ -42,6 +42,10 @@ public:
     *        In this case, a trusted source is verified by binder
     *        UID to be a system service by MediaMetrics service.
     *        Do not use true if you haven't really checked!
     *
     * \return NO_ERROR on success,
     *         PERMISSION_DENIED if the item cannot be put into the AnalyticsState,
     *         BAD_VALUE if the item key does not start with "audio.".
     */
    status_t submit(const std::shared_ptr<const mediametrics::Item>& item, bool isTrusted);

@@ -60,9 +64,22 @@ public:
    std::pair<std::string, int32_t> dump(int32_t lines = INT32_MAX) const;

private:
    // The following are locked internally
    TimeMachine mTimeMachine;
    TransactionLog mTransactionLog;

    /**
     * Checks for any pending actions for a particular item.
     *
     * \param item to check against the current AnalyticsActions.
     */
    void checkActions(const std::shared_ptr<const mediametrics::Item>& item);

    // Actions is individually locked
    AnalyticsActions mActions;

    // AnalyticsState is individually locked, and we use SharedPtrWrap
    // to allow safe access even if the shared pointer changes underneath.

    SharedPtrWrap<AnalyticsState> mAnalyticsState;
    SharedPtrWrap<AnalyticsState> mPreviousAnalyticsState;
};

} // namespace android::mediametrics
Loading