Merge "DO NOT MERGE: Check permission before accessing contacts provider" into rvc-qpr-dev
diff --git a/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java b/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
index ecacb51..c4f6170 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
@@ -51,8 +51,7 @@
 
     public AsyncQueryLiveData(Context context, QueryParam.Provider provider,
             ExecutorService executorService) {
-        mObservableAsyncQuery = new ObservableAsyncQuery(provider, context.getContentResolver(),
-                this::onCursorLoaded);
+        mObservableAsyncQuery = new ObservableAsyncQuery(context, provider, this::onCursorLoaded);
         mExecutorService = executorService;
     }
 
diff --git a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
index 8ed7e73..3bdf52a 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
@@ -16,6 +16,7 @@
 
 package com.android.car.telephony.common;
 
+import android.Manifest;
 import android.content.Context;
 import android.database.Cursor;
 import android.provider.ContactsContract;
@@ -119,7 +120,8 @@
                         ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
                         ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
                         ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE},
-                ContactsContract.Contacts.DISPLAY_NAME + " ASC ");
+                ContactsContract.Contacts.DISPLAY_NAME + " ASC ",
+                Manifest.permission.READ_CONTACTS);
         mContactListAsyncQueryLiveData = new AsyncQueryLiveData<List<Contact>>(mContext,
                 QueryParam.of(contactListQueryParam), Executors.newSingleThreadExecutor()) {
             @Override
diff --git a/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java b/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
index 394b6d4..9f2f0cb 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
@@ -18,12 +18,15 @@
 
 import android.content.AsyncQueryHandler;
 import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
 import android.database.ContentObserver;
 import android.database.Cursor;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
 
 import com.android.car.apps.common.log.L;
 
@@ -48,6 +51,7 @@
         void onQueryFinished(@Nullable Cursor cursor);
     }
 
+    private Context mContext;
     private AsyncQueryHandler mAsyncQueryHandler;
     private QueryParam.Provider mQueryParamProvider;
     private OnQueryFinishedListener mOnQueryFinishedListener;
@@ -61,11 +65,12 @@
      * @param listener           Listener which will be called when data is available.
      */
     public ObservableAsyncQuery(
+            @NonNull Context context,
             @NonNull QueryParam.Provider queryParamProvider,
-            @NonNull ContentResolver cr,
             @NonNull OnQueryFinishedListener listener) {
-        mAsyncQueryHandler = new AsyncQueryHandlerImpl(this, cr);
-        mContentResolver = cr;
+        mContext = context;
+        mContentResolver = context.getContentResolver();
+        mAsyncQueryHandler = new AsyncQueryHandlerImpl(this, mContentResolver);
         mContentObserver = new ContentObserver(mAsyncQueryHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -88,7 +93,8 @@
 
         mToken++;
         QueryParam queryParam = mQueryParamProvider.getQueryParam();
-        if (queryParam != null) {
+        if (queryParam != null && ContextCompat.checkSelfPermission(mContext,
+                queryParam.mPermission) == PackageManager.PERMISSION_GRANTED) {
             mAsyncQueryHandler.startQuery(
                     mToken,
                     null,
diff --git a/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java b/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java
index 9628124..6ceb7c5 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java
@@ -53,17 +53,21 @@
     final String[] mSelectionArgs;
     /** Used by {@link ObservableAsyncQuery#startQuery()} as query param. */
     final String mOrderBy;
+    /** Used by {@link ObservableAsyncQuery#startQuery()} to check query permission. */
+    final String mPermission;
 
     public QueryParam(
             @NonNull Uri uri,
             @Nullable String[] projection,
             @Nullable String selection,
             @Nullable String[] selectionArgs,
-            @Nullable String orderBy) {
+            @Nullable String orderBy,
+            @NonNull String permission) {
         mUri = uri;
         mProjection = projection;
         mSelection = selection;
         mSelectionArgs = selectionArgs;
         mOrderBy = orderBy;
+        mPermission = permission;
     }
 }
diff --git a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
index 40d93a7..a80a854 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
@@ -42,6 +42,7 @@
 import android.widget.ImageView;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 import androidx.core.content.ContextCompat;
 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
@@ -187,17 +188,21 @@
     public static final class PhoneNumberInfo {
         private final String mPhoneNumber;
         private final String mDisplayName;
+        private final String mDisplayNameAlt;
         private final String mInitials;
         private final Uri mAvatarUri;
         private final String mTypeLabel;
+        private final String mLookupKey;
 
-        public PhoneNumberInfo(String phoneNumber, String displayName,
-                String initials, Uri avatarUri, String typeLabel) {
+        public PhoneNumberInfo(String phoneNumber, String displayName, String displayNameAlt,
+                String initials, Uri avatarUri, String typeLabel, String lookupKey) {
             mPhoneNumber = phoneNumber;
             mDisplayName = displayName;
+            mDisplayNameAlt = displayNameAlt;
             mInitials = initials;
             mAvatarUri = avatarUri;
             mTypeLabel = typeLabel;
+            mLookupKey = lookupKey;
         }
 
         public String getPhoneNumber() {
@@ -208,6 +213,10 @@
             return mDisplayName;
         }
 
+        public String getDisplayNameAlt() {
+            return mDisplayNameAlt;
+        }
+
         /**
          * Returns the initials of the contact related to the phone number. Returns null if there is
          * no related contact.
@@ -226,6 +235,12 @@
             return mTypeLabel;
         }
 
+        /** Returns the lookup key of the contact if any is found. */
+        @Nullable
+        public String getLookupKey() {
+            return mLookupKey;
+        }
+
     }
 
     /**
@@ -240,97 +255,127 @@
             return CompletableFuture.completedFuture(new PhoneNumberInfo(
                     number,
                     context.getString(R.string.unknown),
+                    context.getString(R.string.unknown),
                     null,
                     null,
-                    ""));
+                    "",
+                    null));
         }
 
         if (isVoicemailNumber(context, number)) {
             return CompletableFuture.completedFuture(new PhoneNumberInfo(
                     number,
                     context.getString(R.string.voicemail),
+                    context.getString(R.string.voicemail),
                     null,
                     makeResourceUri(context, R.drawable.ic_voicemail),
-                    ""));
+                    "",
+                    null));
         }
 
-        if (InMemoryPhoneBook.isInitialized()) {
-            Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number);
-            if (contact != null) {
-                String name = contact.getDisplayName();
-                if (name == null) {
-                    name = getFormattedNumber(context, number);
-                }
+        return CompletableFuture.supplyAsync(() -> lookupNumberInBackground(context, number));
+    }
 
-                if (name == null) {
-                    name = context.getString(R.string.unknown);
-                }
+    /** Lookup phone number info in background. */
+    @WorkerThread
+    public static PhoneNumberInfo lookupNumberInBackground(Context context, String number) {
+        if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
+                != PackageManager.PERMISSION_GRANTED) {
+            String readableNumber = getReadableNumber(context, number);
+            return new PhoneNumberInfo(number, readableNumber, readableNumber, null, null, null,
+                    null);
+        }
 
-                PhoneNumber phoneNumber = contact.getPhoneNumber(context, number);
-                CharSequence typeLabel = "";
-                if (phoneNumber != null) {
-                    typeLabel = Phone.getTypeLabel(context.getResources(),
-                            phoneNumber.getType(),
-                            phoneNumber.getLabel());
-                }
+        Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number);
+        if (contact != null) {
+            String name = contact.getDisplayName();
+            String nameAlt = contact.getDisplayNameAlt();
+            if (TextUtils.isEmpty(name)) {
+                name = getReadableNumber(context, number);
+            }
+            if (TextUtils.isEmpty(nameAlt)) {
+                nameAlt = name;
+            }
 
-                return CompletableFuture.completedFuture(new PhoneNumberInfo(
-                        number,
-                        name,
-                        contact.getInitials(),
-                        contact.getAvatarUri(),
-                        typeLabel.toString()));
+            PhoneNumber phoneNumber = contact.getPhoneNumber(context, number);
+            CharSequence typeLabel = phoneNumber == null ? "" : phoneNumber.getReadableLabel(
+                    context.getResources());
+
+            return new PhoneNumberInfo(
+                    number,
+                    name,
+                    nameAlt,
+                    contact.getInitials(),
+                    contact.getAvatarUri(),
+                    typeLabel.toString(),
+                    contact.getLookupKey());
+        }
+
+        String name = null;
+        String nameAlt = null;
+        String initials = null;
+        String photoUriString = null;
+        CharSequence typeLabel = "";
+        String lookupKey = null;
+
+        ContentResolver cr = context.getContentResolver();
+        try (Cursor cursor = cr.query(
+                Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
+                new String[]{
+                        PhoneLookup.DISPLAY_NAME,
+                        PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
+                        PhoneLookup.PHOTO_URI,
+                        PhoneLookup.TYPE,
+                        PhoneLookup.LABEL,
+                        PhoneLookup.LOOKUP_KEY,
+                },
+                null, null, null)) {
+
+            if (cursor != null && cursor.moveToFirst()) {
+                int nameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
+                int altNameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME_ALTERNATIVE);
+                int photoUriColumn = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
+                int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
+                int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
+                int lookupKeyColumn = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY);
+
+                name = cursor.getString(nameColumn);
+                nameAlt = cursor.getString(altNameColumn);
+                photoUriString = cursor.getString(photoUriColumn);
+                initials = getInitials(name, nameAlt);
+
+                int type = cursor.getInt(typeColumn);
+                String label = cursor.getString(labelColumn);
+                typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
+
+                lookupKey = cursor.getString(lookupKeyColumn);
             }
         }
 
-        return CompletableFuture.supplyAsync(() -> {
-            String name = null;
-            String nameAlt = null;
-            String photoUriString = null;
-            CharSequence typeLabel = "";
-            ContentResolver cr = context.getContentResolver();
-            String initials;
-            try (Cursor cursor = cr.query(
-                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
-                    new String[]{
-                            PhoneLookup.DISPLAY_NAME,
-                            PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
-                            PhoneLookup.PHOTO_URI,
-                            PhoneLookup.TYPE,
-                            PhoneLookup.LABEL,
-                    },
-                    null, null, null)) {
+        if (TextUtils.isEmpty(name)) {
+            name = getReadableNumber(context, number);
+        }
+        if (TextUtils.isEmpty(nameAlt)) {
+            nameAlt = name;
+        }
 
-                if (cursor != null && cursor.moveToFirst()) {
-                    int nameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
-                    int altNameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME_ALTERNATIVE);
-                    int photoUriColumn = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
-                    int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
-                    int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
+        return new PhoneNumberInfo(
+                number,
+                name,
+                nameAlt,
+                initials,
+                TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString),
+                typeLabel.toString(),
+                lookupKey);
+    }
 
-                    name = cursor.getString(nameColumn);
-                    nameAlt = cursor.getString(altNameColumn);
-                    photoUriString = cursor.getString(photoUriColumn);
-                    int type = cursor.getInt(typeColumn);
-                    String label = cursor.getString(labelColumn);
-                    typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
-                }
-            }
+    private static String getReadableNumber(Context context, String number) {
+        String readableNumber = getFormattedNumber(context, number);
 
-            initials = getInitials(name, nameAlt);
-
-            if (name == null) {
-                name = getFormattedNumber(context, number);
-            }
-
-            if (name == null) {
-                name = context.getString(R.string.unknown);
-            }
-
-            return new PhoneNumberInfo(number, name, initials,
-                    TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString),
-                    typeLabel.toString());
-        });
+        if (readableNumber == null) {
+            readableNumber = context.getString(R.string.unknown);
+        }
+        return readableNumber;
     }
 
     /**
@@ -476,6 +521,11 @@
      * Set the given phone number as the primary phone number for its associated contact.
      */
     public static void setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber) {
+        if (context.checkSelfPermission(Manifest.permission.WRITE_CONTACTS)
+                != PackageManager.PERMISSION_GRANTED) {
+            L.w(TAG, "Missing WRITE_CONTACTS permission, not setting primary number.");
+            return;
+        }
         // Update the primary values in the data record.
         ContentValues values = new ContentValues(1);
         values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
@@ -487,23 +537,6 @@
     }
 
     /**
-     * Add a contact to favorite or remove it from favorite.
-     */
-    public static int setAsFavoriteContact(Context context, Contact contact, boolean isFavorite) {
-        if (contact.isStarred() == isFavorite) {
-            return 0;
-        }
-
-        ContentValues values = new ContentValues(1);
-        values.put(ContactsContract.Contacts.STARRED, isFavorite ? 1 : 0);
-
-        String where = ContactsContract.Contacts._ID + " = ?";
-        String[] selectionArgs = new String[]{Long.toString(contact.getId())};
-        return context.getContentResolver().update(ContactsContract.Contacts.CONTENT_URI, values,
-                where, selectionArgs);
-    }
-
-    /**
      * Mark missed call log matching given phone number as read. If phone number string is not
      * valid, it will mark all new missed call log as read.
      */