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

Commit dea8db3d authored by Danning Chen's avatar Danning Chen Committed by Android (Google) Code Review
Browse files

Merge "Listen to the new calls and SMS/MMS messages and store the derived...

Merge "Listen to the new calls and SMS/MMS messages and store the derived events in the People Service event store"
parents 83e6bdd2 e84ee5f9
Loading
Loading
Loading
Loading
+121 −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.
 */

package com.android.server.people.data;

import android.annotation.WorkerThread;
import android.content.Context;
import android.database.Cursor;
import android.provider.CallLog.Calls;
import android.text.TextUtils;
import android.util.Slog;
import android.util.SparseIntArray;

import java.util.function.BiConsumer;

/** A helper class that queries the call log database. */
class CallLogQueryHelper {

    private static final String TAG = "CallLogQueryHelper";

    private static final SparseIntArray CALL_TYPE_TO_EVENT_TYPE = new SparseIntArray();

    static {
        CALL_TYPE_TO_EVENT_TYPE.put(Calls.INCOMING_TYPE, Event.TYPE_CALL_INCOMING);
        CALL_TYPE_TO_EVENT_TYPE.put(Calls.OUTGOING_TYPE, Event.TYPE_CALL_OUTGOING);
        CALL_TYPE_TO_EVENT_TYPE.put(Calls.MISSED_TYPE, Event.TYPE_CALL_MISSED);
    }

    private final Context mContext;
    private final BiConsumer<String, Event> mEventConsumer;
    private long mLastCallTimestamp;

    /**
     * @param context Context for accessing the content resolver.
     * @param eventConsumer Consumes the events created from the call log records. The first input
     *                      param is the normalized phone number.
     */
    CallLogQueryHelper(Context context, BiConsumer<String, Event> eventConsumer) {
        mContext = context;
        mEventConsumer = eventConsumer;
    }

    /**
     * Queries the call log database for the new data added since {@code sinceTime} and returns
     * true if the query runs successfully and at least one call log entry is found.
     */
    @WorkerThread
    boolean querySince(long sinceTime) {
        String[] projection = new String[] {
                Calls.CACHED_NORMALIZED_NUMBER, Calls.DATE, Calls.DURATION, Calls.TYPE };
        String selection = Calls.DATE + " > ?";
        String[] selectionArgs = new String[] { Long.toString(sinceTime) };
        boolean hasResults = false;
        try (Cursor cursor = mContext.getContentResolver().query(
                Calls.CONTENT_URI, projection, selection, selectionArgs,
                Calls.DEFAULT_SORT_ORDER)) {
            if (cursor == null) {
                Slog.w(TAG, "Cursor is null when querying call log.");
                return false;
            }
            while (cursor.moveToNext()) {
                // Phone number
                int numberIndex = cursor.getColumnIndex(Calls.CACHED_NORMALIZED_NUMBER);
                String phoneNumber = cursor.getString(numberIndex);

                // Date
                int dateIndex = cursor.getColumnIndex(Calls.DATE);
                long date = cursor.getLong(dateIndex);

                // Duration
                int durationIndex = cursor.getColumnIndex(Calls.DURATION);
                long durationSeconds = cursor.getLong(durationIndex);

                // Type
                int typeIndex = cursor.getColumnIndex(Calls.TYPE);
                int callType = cursor.getInt(typeIndex);

                mLastCallTimestamp = Math.max(mLastCallTimestamp, date);
                if (addEvent(phoneNumber, date, durationSeconds, callType)) {
                    hasResults = true;
                }
            }
        }
        return hasResults;
    }

    long getLastCallTimestamp() {
        return mLastCallTimestamp;
    }

    private boolean addEvent(String phoneNumber, long date, long durationSeconds, int callType) {
        if (!validateEvent(phoneNumber, date, callType)) {
            return false;
        }
        @Event.EventType int eventType  = CALL_TYPE_TO_EVENT_TYPE.get(callType);
        Event event = new Event.Builder(date, eventType)
                .setCallDetails(new Event.CallDetails(durationSeconds))
                .build();
        mEventConsumer.accept(phoneNumber, event);
        return true;
    }

    private boolean validateEvent(String phoneNumber, long date, int callType) {
        return !TextUtils.isEmpty(phoneNumber)
                && date > 0L
                && CALL_TYPE_TO_EVENT_TYPE.indexOfKey(callType) >= 0;
    }
}
+149 −5
Original line number Diff line number Diff line
@@ -45,7 +45,9 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.CallLog;
import android.provider.ContactsContract.Contacts;
import android.provider.Telephony.MmsSms;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.telecom.TelecomManager;
@@ -65,6 +67,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
@@ -76,7 +79,7 @@ public class DataManager {
    private static final String PLATFORM_PACKAGE_NAME = "android";
    private static final int MY_UID = Process.myUid();
    private static final int MY_PID = Process.myPid();
    private static final long USAGE_STATS_QUERY_MAX_EVENT_AGE_MS = DateUtils.DAY_IN_MILLIS;
    private static final long QUERY_EVENTS_MAX_AGE_MS = DateUtils.DAY_IN_MILLIS;
    private static final long USAGE_STATS_QUERY_INTERVAL_SEC = 120L;

    private final Context mContext;
@@ -89,6 +92,8 @@ public class DataManager {
    private final SparseArray<ScheduledFuture<?>> mUsageStatsQueryFutures = new SparseArray<>();
    private final SparseArray<NotificationListenerService> mNotificationListeners =
            new SparseArray<>();
    private final ContentObserver mCallLogContentObserver;
    private final ContentObserver mMmsSmsContentObserver;

    private ShortcutServiceInternal mShortcutServiceInternal;
    private UsageStatsManagerInternal mUsageStatsManagerInternal;
@@ -96,9 +101,7 @@ public class DataManager {
    private UserManager mUserManager;

    public DataManager(Context context) {
        mContext = context;
        mInjector = new Injector();
        mUsageStatsQueryExecutor = mInjector.createScheduledExecutor();
        this(context, new Injector());
    }

    @VisibleForTesting
@@ -106,6 +109,10 @@ public class DataManager {
        mContext = context;
        mInjector = injector;
        mUsageStatsQueryExecutor = mInjector.createScheduledExecutor();
        mCallLogContentObserver = new CallLogContentObserver(
                BackgroundThread.getHandler());
        mMmsSmsContentObserver = new MmsSmsContentObserver(
                BackgroundThread.getHandler());
    }

    /** Initialization. Called when the system services are up running. */
@@ -158,6 +165,18 @@ public class DataManager {
        } catch (RemoteException e) {
            // Should never occur for local calls.
        }

        if (userId == UserHandle.USER_SYSTEM) {
            // The call log and MMS/SMS messages are shared across user profiles. So only need to
            // register the content observers once for the primary user.
            // TODO: Register observers after the conversations and events being loaded from disk.
            mContext.getContentResolver().registerContentObserver(
                    CallLog.CONTENT_URI, /* notifyForDescendants= */ true,
                    mCallLogContentObserver, UserHandle.USER_SYSTEM);
            mContext.getContentResolver().registerContentObserver(
                    MmsSms.CONTENT_URI, /* notifyForDescendants= */ false,
                    mMmsSmsContentObserver, UserHandle.USER_SYSTEM);
        }
    }

    /** This method is called when a user is stopped. */
@@ -182,6 +201,10 @@ public class DataManager {
                // Should never occur for local calls.
            }
        }
        if (userId == UserHandle.USER_SYSTEM) {
            mContext.getContentResolver().unregisterContentObserver(mCallLogContentObserver);
            mContext.getContentResolver().unregisterContentObserver(mMmsSmsContentObserver);
        }
    }

    /**
@@ -274,6 +297,15 @@ public class DataManager {
                userId, MY_PID, MY_UID);
    }

    private void forAllUnlockedUsers(Consumer<UserData> consumer) {
        for (int i = 0; i < mUserDataArray.size(); i++) {
            UserData userData = mUserDataArray.get(i);
            if (userData.isUnlocked()) {
                consumer.accept(userData);
            }
        }
    }

    @Nullable
    private UserData getUnlockedUserData(int userId) {
        UserData userData = mUserDataArray.get(userId);
@@ -387,11 +419,26 @@ public class DataManager {
        return mContactsContentObservers.get(userId);
    }

    @VisibleForTesting
    ContentObserver getCallLogContentObserverForTesting() {
        return mCallLogContentObserver;
    }

    @VisibleForTesting
    ContentObserver getMmsSmsContentObserverForTesting() {
        return mMmsSmsContentObserver;
    }

    @VisibleForTesting
    NotificationListenerService getNotificationListenerServiceForTesting(@UserIdInt int userId) {
        return mNotificationListeners.get(userId);
    }

    @VisibleForTesting
    UserData getUserDataForTesting(@UserIdInt int userId) {
        return mUserDataArray.get(userId);
    }

    /** Observer that observes the changes in the Contacts database. */
    private class ContactsContentObserver extends ContentObserver {

@@ -442,6 +489,88 @@ public class DataManager {
        }
    }

    /** Observer that observes the changes in the call log database. */
    private class CallLogContentObserver extends ContentObserver implements
            BiConsumer<String, Event> {

        private final CallLogQueryHelper mCallLogQueryHelper;
        private long mLastCallTimestamp;

        private CallLogContentObserver(Handler handler) {
            super(handler);
            mCallLogQueryHelper = mInjector.createCallLogQueryHelper(mContext, this);
            mLastCallTimestamp = System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
        }

        @Override
        public void onChange(boolean selfChange) {
            if (mCallLogQueryHelper.querySince(mLastCallTimestamp)) {
                mLastCallTimestamp = mCallLogQueryHelper.getLastCallTimestamp();
            }
        }

        @Override
        public void accept(String phoneNumber, Event event) {
            forAllUnlockedUsers(userData -> {
                PackageData defaultDialer = userData.getDefaultDialer();
                if (defaultDialer == null) {
                    return;
                }
                ConversationStore conversationStore = defaultDialer.getConversationStore();
                if (conversationStore.getConversationByPhoneNumber(phoneNumber) == null) {
                    return;
                }
                EventStore eventStore = defaultDialer.getEventStore();
                eventStore.getOrCreateCallEventHistory(phoneNumber).addEvent(event);
            });
        }
    }

    /** Observer that observes the changes in the MMS & SMS database. */
    private class MmsSmsContentObserver extends ContentObserver implements
            BiConsumer<String, Event> {

        private final MmsQueryHelper mMmsQueryHelper;
        private long mLastMmsTimestamp;

        private final SmsQueryHelper mSmsQueryHelper;
        private long mLastSmsTimestamp;

        private MmsSmsContentObserver(Handler handler) {
            super(handler);
            mMmsQueryHelper = mInjector.createMmsQueryHelper(mContext, this);
            mSmsQueryHelper = mInjector.createSmsQueryHelper(mContext, this);
            mLastSmsTimestamp = mLastMmsTimestamp =
                    System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
        }

        @Override
        public void onChange(boolean selfChange) {
            if (mMmsQueryHelper.querySince(mLastMmsTimestamp)) {
                mLastMmsTimestamp = mMmsQueryHelper.getLastMessageTimestamp();
            }
            if (mSmsQueryHelper.querySince(mLastSmsTimestamp)) {
                mLastSmsTimestamp = mSmsQueryHelper.getLastMessageTimestamp();
            }
        }

        @Override
        public void accept(String phoneNumber, Event event) {
            forAllUnlockedUsers(userData -> {
                PackageData defaultSmsApp = userData.getDefaultSmsApp();
                if (defaultSmsApp == null) {
                    return;
                }
                ConversationStore conversationStore = defaultSmsApp.getConversationStore();
                if (conversationStore.getConversationByPhoneNumber(phoneNumber) == null) {
                    return;
                }
                EventStore eventStore = defaultSmsApp.getEventStore();
                eventStore.getOrCreateSmsEventHistory(phoneNumber).addEvent(event);
            });
        }
    }

    /** Listener for the shortcut data changes. */
    private class ShortcutServiceListener implements
            ShortcutServiceInternal.ShortcutChangeListener {
@@ -487,7 +616,7 @@ public class DataManager {

        private UsageStatsQueryRunnable(int userId) {
            mUserId = userId;
            mLastQueryTime = System.currentTimeMillis() - USAGE_STATS_QUERY_MAX_EVENT_AGE_MS;
            mLastQueryTime = System.currentTimeMillis() - QUERY_EVENTS_MAX_AGE_MS;
        }

        @Override
@@ -535,6 +664,21 @@ public class DataManager {
            return new ContactsQueryHelper(context);
        }

        CallLogQueryHelper createCallLogQueryHelper(Context context,
                BiConsumer<String, Event> eventConsumer) {
            return new CallLogQueryHelper(context, eventConsumer);
        }

        MmsQueryHelper createMmsQueryHelper(Context context,
                BiConsumer<String, Event> eventConsumer) {
            return new MmsQueryHelper(context, eventConsumer);
        }

        SmsQueryHelper createSmsQueryHelper(Context context,
                BiConsumer<String, Event> eventConsumer) {
            return new SmsQueryHelper(context, eventConsumer);
        }

        int getCallingUserId() {
            return Binder.getCallingUserHandle().getIdentifier();
        }
+151 −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.
 */

package com.android.server.people.data;

import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.Telephony.BaseMmsColumns;
import android.provider.Telephony.Mms;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Slog;
import android.util.SparseIntArray;

import com.google.android.mms.pdu.PduHeaders;

import java.util.function.BiConsumer;

/** A helper class that queries the MMS database tables. */
class MmsQueryHelper {

    private static final String TAG = "MmsQueryHelper";
    private static final long MILLIS_PER_SECONDS = 1000L;
    private static final SparseIntArray MSG_BOX_TO_EVENT_TYPE = new SparseIntArray();

    static {
        MSG_BOX_TO_EVENT_TYPE.put(BaseMmsColumns.MESSAGE_BOX_INBOX, Event.TYPE_SMS_INCOMING);
        MSG_BOX_TO_EVENT_TYPE.put(BaseMmsColumns.MESSAGE_BOX_SENT, Event.TYPE_SMS_OUTGOING);
    }

    private final Context mContext;
    private final BiConsumer<String, Event> mEventConsumer;
    private long mLastMessageTimestamp;
    private String mCurrentCountryIso;

    /**
     * @param context Context for accessing the content resolver.
     * @param eventConsumer Consumes the events created from the message records. The first input
     *                      param is the normalized phone number.
     */
    MmsQueryHelper(Context context, BiConsumer<String, Event> eventConsumer) {
        mContext = context;
        mEventConsumer = eventConsumer;
        mCurrentCountryIso = Utils.getCurrentCountryIso(mContext);
    }

    /**
     * Queries the MMS database tables for the new data added since {@code sinceTime} (in millis)
     * and returns true if the query runs successfully and at least one message entry is found.
     */
    @WorkerThread
    boolean querySince(long sinceTime) {
        String[] projection = new String[] { Mms._ID, Mms.DATE, Mms.MESSAGE_BOX };
        String selection = Mms.DATE + " > ?";
        // NOTE: The field Mms.DATE is stored in seconds, not milliseconds.
        String[] selectionArgs = new String[] { Long.toString(sinceTime / MILLIS_PER_SECONDS) };
        boolean hasResults = false;
        try (Cursor cursor = mContext.getContentResolver().query(
                Mms.CONTENT_URI, projection, selection, selectionArgs, null)) {
            if (cursor == null) {
                Slog.w(TAG, "Cursor is null when querying MMS table.");
                return false;
            }
            while (cursor.moveToNext()) {
                // ID
                int msgIdIndex = cursor.getColumnIndex(Mms._ID);
                String msgId = cursor.getString(msgIdIndex);

                // Date
                int dateIndex = cursor.getColumnIndex(Mms.DATE);
                long date = cursor.getLong(dateIndex) * MILLIS_PER_SECONDS;

                // Message box
                int msgBoxIndex = cursor.getColumnIndex(Mms.MESSAGE_BOX);
                int msgBox = cursor.getInt(msgBoxIndex);

                mLastMessageTimestamp = Math.max(mLastMessageTimestamp, date);
                String address = getMmsAddress(msgId, msgBox);
                if (address != null && addEvent(address, date, msgBox)) {
                    hasResults = true;
                }
            }
        }
        return hasResults;
    }

    long getLastMessageTimestamp() {
        return mLastMessageTimestamp;
    }

    @Nullable
    private String getMmsAddress(String msgId, int msgBox) {
        Uri addressUri = Mms.Addr.getAddrUriForMessage(msgId);
        String[] projection = new String[] { Mms.Addr.ADDRESS, Mms.Addr.TYPE };
        String address = null;
        try (Cursor cursor = mContext.getContentResolver().query(
                addressUri, projection, null, null, null)) {
            if (cursor == null) {
                Slog.w(TAG, "Cursor is null when querying MMS address table.");
                return null;
            }
            while (cursor.moveToNext()) {
                // Type
                int typeIndex = cursor.getColumnIndex(Mms.Addr.TYPE);
                int type = cursor.getInt(typeIndex);

                if ((msgBox == BaseMmsColumns.MESSAGE_BOX_INBOX && type == PduHeaders.FROM)
                        || (msgBox == BaseMmsColumns.MESSAGE_BOX_SENT && type == PduHeaders.TO)) {
                    // Address
                    int addrIndex = cursor.getColumnIndex(Mms.Addr.ADDRESS);
                    address = cursor.getString(addrIndex);
                }
            }
        }
        if (!Mms.isPhoneNumber(address)) {
            return null;
        }
        return PhoneNumberUtils.formatNumberToE164(address, mCurrentCountryIso);
    }

    private boolean addEvent(String phoneNumber, long date, int msgBox) {
        if (!validateEvent(phoneNumber, date, msgBox)) {
            return false;
        }
        @Event.EventType int eventType  = MSG_BOX_TO_EVENT_TYPE.get(msgBox);
        mEventConsumer.accept(phoneNumber, new Event(date, eventType));
        return true;
    }

    private boolean validateEvent(String phoneNumber, long date, int msgBox) {
        return !TextUtils.isEmpty(phoneNumber)
                && date > 0L
                && MSG_BOX_TO_EVENT_TYPE.indexOfKey(msgBox) >= 0;
    }
}
+10 −13
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.content.LocusId;
import android.text.TextUtils;

import java.util.function.Consumer;
import java.util.function.Predicate;

/** The data associated with a package. */
public class PackageData {
@@ -38,15 +39,19 @@ public class PackageData {
    @NonNull
    private final EventStore mEventStore;

    private boolean mIsDefaultDialer;
    private final Predicate<String> mIsDefaultDialerPredicate;

    private boolean mIsDefaultSmsApp;
    private final Predicate<String> mIsDefaultSmsAppPredicate;

    PackageData(@NonNull String packageName, @UserIdInt int userId) {
    PackageData(@NonNull String packageName, @UserIdInt int userId,
            @NonNull Predicate<String> isDefaultDialerPredicate,
            @NonNull Predicate<String> isDefaultSmsAppPredicate) {
        mPackageName = packageName;
        mUserId = userId;
        mConversationStore = new ConversationStore();
        mEventStore = new EventStore();
        mIsDefaultDialerPredicate = isDefaultDialerPredicate;
        mIsDefaultSmsAppPredicate = isDefaultSmsAppPredicate;
    }

    @NonNull
@@ -124,11 +129,11 @@ public class PackageData {
    }

    public boolean isDefaultDialer() {
        return mIsDefaultDialer;
        return mIsDefaultDialerPredicate.test(mPackageName);
    }

    public boolean isDefaultSmsApp() {
        return mIsDefaultSmsApp;
        return mIsDefaultSmsAppPredicate.test(mPackageName);
    }

    @NonNull
@@ -141,14 +146,6 @@ public class PackageData {
        return mEventStore;
    }

    void setIsDefaultDialer(boolean value) {
        mIsDefaultDialer = value;
    }

    void setIsDefaultSmsApp(boolean value) {
        mIsDefaultSmsApp = value;
    }

    void onDestroy() {
        // TODO: STOPSHIP: Implements this method for the case of package being uninstalled.
    }
+119 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading