• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.telephony.common;
18 
19 import android.Manifest;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.pm.PackageManager;
25 import android.content.res.Resources;
26 import android.database.Cursor;
27 import android.net.Uri;
28 import android.provider.CallLog;
29 import android.provider.ContactsContract;
30 import android.provider.ContactsContract.CommonDataKinds.Phone;
31 import android.provider.ContactsContract.PhoneLookup;
32 import android.provider.Settings;
33 import android.telecom.Call;
34 import android.telephony.PhoneNumberUtils;
35 import android.telephony.TelephonyManager;
36 import android.text.TextUtils;
37 import android.util.Log;
38 import android.widget.ImageView;
39 
40 import androidx.annotation.Nullable;
41 import androidx.core.util.Pair;
42 
43 import com.android.car.apps.common.LetterTileDrawable;
44 
45 import com.bumptech.glide.Glide;
46 import com.bumptech.glide.request.RequestOptions;
47 import com.google.i18n.phonenumbers.NumberParseException;
48 import com.google.i18n.phonenumbers.PhoneNumberUtil;
49 import com.google.i18n.phonenumbers.Phonenumber;
50 
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.Locale;
54 
55 /** Helper methods. */
56 public class TelecomUtils {
57     private static final String TAG = "CD.TelecomUtils";
58 
59     private static final String[] CONTACT_ID_PROJECTION = new String[]{
60             PhoneLookup.DISPLAY_NAME,
61             PhoneLookup.TYPE,
62             PhoneLookup.LABEL,
63             PhoneLookup._ID
64     };
65 
66     private static String sVoicemailNumber;
67     private static TelephonyManager sTelephonyManager;
68 
69     /**
70      * Return the label for the given phone number.
71      *
72      * @param number Caller phone number
73      * @return the label if it is found, empty string otherwise.
74      */
getTypeFromNumber(Context context, String number)75     public static CharSequence getTypeFromNumber(Context context, String number) {
76         if (Log.isLoggable(TAG, Log.DEBUG)) {
77             Log.d(TAG, "getTypeFromNumber, number: " + number);
78         }
79 
80         String defaultLabel = "";
81         if (TextUtils.isEmpty(number)) {
82             return defaultLabel;
83         }
84 
85         ContentResolver cr = context.getContentResolver();
86         Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
87         Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null);
88 
89         try {
90             if (cursor != null && cursor.moveToFirst()) {
91                 int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
92                 int type = cursor.getInt(typeColumn);
93                 int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
94                 String label = cursor.getString(labelColumn);
95                 CharSequence typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
96                 return typeLabel;
97             }
98         } finally {
99             if (cursor != null) {
100                 cursor.close();
101             }
102         }
103         return defaultLabel;
104     }
105 
106     /**
107      * Get the voicemail number.
108      */
getVoicemailNumber(Context context)109     public static String getVoicemailNumber(Context context) {
110         if (sVoicemailNumber == null) {
111             sVoicemailNumber = getTelephonyManager(context).getVoiceMailNumber();
112         }
113         return sVoicemailNumber;
114     }
115 
116     /**
117      * Returns {@code true} if the given number is a voice mail number.
118      *
119      * @see TelephonyManager#getVoiceMailNumber()
120      */
isVoicemailNumber(Context context, String number)121     public static boolean isVoicemailNumber(Context context, String number) {
122         return !TextUtils.isEmpty(number) && number.equals(getVoicemailNumber(context));
123     }
124 
125     /**
126      * Get the {@link TelephonyManager} instance.
127      */
128     // TODO(deanh): remove this, getSystemService is not slow.
getTelephonyManager(Context context)129     public static TelephonyManager getTelephonyManager(Context context) {
130         if (sTelephonyManager == null) {
131             sTelephonyManager =
132                     (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
133         }
134         return sTelephonyManager;
135     }
136 
137     /**
138      * Format a number as a phone number.
139      */
getFormattedNumber(Context context, String number)140     public static String getFormattedNumber(Context context, String number) {
141         if (Log.isLoggable(TAG, Log.DEBUG)) {
142             Log.d(TAG, "getFormattedNumber: " + number);
143         }
144         if (number == null) {
145             return "";
146         }
147 
148         String countryIso = getIsoDefaultCountryNumber(context);
149         if (Log.isLoggable(TAG, Log.DEBUG)) {
150             Log.d(TAG, "PhoneNumberUtils.formatNumberToE16, number: "
151                     + number + ", country: " + countryIso);
152         }
153         String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso);
154         String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso);
155         formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber;
156         if (Log.isLoggable(TAG, Log.DEBUG)) {
157             Log.d(TAG, "getFormattedNumber, result: " + formattedNumber);
158         }
159         return formattedNumber;
160     }
161 
getIsoDefaultCountryNumber(Context context)162     private static String getIsoDefaultCountryNumber(Context context) {
163         String countryIso = getTelephonyManager(context).getSimCountryIso().toUpperCase(Locale.US);
164         if (countryIso.length() != 2) {
165             countryIso = Locale.getDefault().getCountry();
166             if (countryIso == null || countryIso.length() != 2) {
167                 countryIso = "US";
168             }
169         }
170 
171         return countryIso;
172     }
173 
174     /**
175      * Creates a new instance of {@link Phonenumber#Phonenumber} base on the given number and sim
176      * card country code. Returns {@code null} if the number in an invalid number.
177      */
178     @Nullable
createI18nPhoneNumber(Context context, String number)179     public static Phonenumber.PhoneNumber createI18nPhoneNumber(Context context, String number) {
180         try {
181             return PhoneNumberUtil.getInstance().parse(number, getIsoDefaultCountryNumber(context));
182         } catch (NumberParseException e) {
183             return null;
184         }
185     }
186 
187     /**
188      * Get the display name and photo uri of the given number (e.g. if it's the voicemail number,
189      * return a string and a uri that represents voicemail, if it's a contact, get the contact's
190      * name and its avatar uri, etc).
191      *
192      * @return Pair of display name and contact's photo uri if found. Voicemail number uses drawable
193      * resource uri and null uri for other cases.
194      */
getDisplayNameAndAvatarUri(Context context, String number)195     public static Pair<String, Uri> getDisplayNameAndAvatarUri(Context context, String number) {
196         if (Log.isLoggable(TAG, Log.DEBUG)) {
197             Log.d(TAG, "getDisplayNameAndAvatarUri: " + number);
198         }
199 
200         if (TextUtils.isEmpty(number)) {
201             return new Pair<>(context.getString(R.string.unknown), null);
202         }
203 
204         if (isVoicemailNumber(context, number)) {
205             return new Pair<>(
206                     context.getString(R.string.voicemail),
207                     makeResourceUri(context, R.drawable.ic_voicemail));
208         }
209 
210         ContentResolver cr = context.getContentResolver();
211         Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
212 
213         Cursor cursor = null;
214         String name = null;
215         String photoUriString = null;
216         try {
217             cursor = cr.query(uri, new String[]{PhoneLookup.DISPLAY_NAME, PhoneLookup.PHOTO_URI},
218                     null, null, null);
219             if (cursor != null && cursor.moveToFirst()) {
220                 name = cursor.getString(0);
221                 photoUriString = cursor.getString(1);
222             }
223         } finally {
224             if (cursor != null) {
225                 cursor.close();
226             }
227         }
228 
229         if (name == null) {
230             name = getFormattedNumber(context, number);
231         }
232 
233         if (name == null) {
234             name = context.getString(R.string.unknown);
235         }
236 
237         if (TextUtils.isEmpty(photoUriString)) {
238             return new Pair<>(name, null);
239         }
240         return new Pair<>(name, Uri.parse(photoUriString));
241     }
242 
243     /**
244      * @return A string representation of the call state that can be presented to a user.
245      */
callStateToUiString(Context context, int state)246     public static String callStateToUiString(Context context, int state) {
247         Resources res = context.getResources();
248         switch (state) {
249             case Call.STATE_ACTIVE:
250                 return res.getString(R.string.call_state_call_active);
251             case Call.STATE_HOLDING:
252                 return res.getString(R.string.call_state_hold);
253             case Call.STATE_NEW:
254             case Call.STATE_CONNECTING:
255                 return res.getString(R.string.call_state_connecting);
256             case Call.STATE_SELECT_PHONE_ACCOUNT:
257             case Call.STATE_DIALING:
258                 return res.getString(R.string.call_state_dialing);
259             case Call.STATE_DISCONNECTED:
260                 return res.getString(R.string.call_state_call_ended);
261             case Call.STATE_RINGING:
262                 return res.getString(R.string.call_state_call_ringing);
263             case Call.STATE_DISCONNECTING:
264                 return res.getString(R.string.call_state_call_ending);
265             default:
266                 throw new IllegalStateException("Unknown Call State: " + state);
267         }
268     }
269 
270     /**
271      * Returns true if the telephony network is available.
272      */
isNetworkAvailable(Context context)273     public static boolean isNetworkAvailable(Context context) {
274         TelephonyManager tm =
275                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
276         return tm.getNetworkType() != TelephonyManager.NETWORK_TYPE_UNKNOWN
277                 && tm.getSimState() == TelephonyManager.SIM_STATE_READY;
278     }
279 
280     /**
281      * Returns true if airplane mode is on.
282      */
isAirplaneModeOn(Context context)283     public static boolean isAirplaneModeOn(Context context) {
284         return Settings.System.getInt(context.getContentResolver(),
285                 Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
286     }
287 
288     /**
289      * Sets a Contact avatar onto the provided {@code icon}. The first letter of the contact's
290      * display name or {@code fallbackDisplayName} will be used as a fallback resource if avatar
291      * loading fails.
292      */
setContactBitmapAsync( Context context, final ImageView icon, @Nullable final Contact contact, @Nullable final String fallbackDisplayName)293     public static void setContactBitmapAsync(
294             Context context,
295             final ImageView icon,
296             @Nullable final Contact contact,
297             @Nullable final String fallbackDisplayName) {
298         Uri avatarUri = contact != null ? contact.getAvatarUri() : null;
299         String displayName = contact != null ? contact.getDisplayName() : fallbackDisplayName;
300 
301         setContactBitmapAsync(context, icon, avatarUri, displayName);
302     }
303 
304     /**
305      * Sets a Contact avatar onto the provided {@code icon}. The first letter of the contact's
306      * display name will be used as a fallback resource if avatar loading fails.
307      */
setContactBitmapAsync( Context context, final ImageView icon, final Uri avatarUri, final String displayName)308     public static void setContactBitmapAsync(
309             Context context,
310             final ImageView icon,
311             final Uri avatarUri,
312             final String displayName) {
313         LetterTileDrawable letterTileDrawable = createLetterTile(context, displayName);
314 
315         if (avatarUri != null) {
316             Glide.with(context)
317                     .load(avatarUri)
318                     .apply(new RequestOptions().centerCrop().error(letterTileDrawable))
319                     .into(icon);
320             return;
321         }
322 
323         // Use the letter tile as avatar if there is no avatar available from content provider.
324         icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
325         icon.setImageDrawable(letterTileDrawable);
326     }
327 
328     /** Create a {@link LetterTileDrawable} for the given display name. */
createLetterTile(Context context, String displayName)329     public static LetterTileDrawable createLetterTile(Context context, String displayName) {
330         LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources());
331         letterTileDrawable.setContactDetails(displayName, displayName);
332         return  letterTileDrawable;
333     }
334 
335     /** Set the given phone number as the primary phone number for its associated contact. */
setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber)336     public static void setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber) {
337         // Update the primary values in the data record.
338         ContentValues values = new ContentValues(1);
339         values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
340         values.put(ContactsContract.Data.IS_PRIMARY, 1);
341 
342         context.getContentResolver().update(
343                 ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, phoneNumber.getId()),
344                 values, null, null);
345     }
346 
347     /** Add a contact to favorite or remove it from favorite. */
setAsFavoriteContact(Context context, Contact contact, boolean isFavorite)348     public static int setAsFavoriteContact(Context context, Contact contact, boolean isFavorite) {
349         if (contact.isStarred() == isFavorite) {
350             return 0;
351         }
352 
353         ContentValues values = new ContentValues(1);
354         values.put(ContactsContract.Contacts.STARRED, isFavorite ? 1 : 0);
355 
356         String where = ContactsContract.Contacts._ID + " = ?";
357         String[] selectionArgs = new String[]{Long.toString(contact.getId())};
358         return context.getContentResolver().update(ContactsContract.Contacts.CONTENT_URI, values,
359                 where, selectionArgs);
360     }
361 
362     /**
363      * Mark missed call log matching given phone number as read. If phone number string is not
364      * valid, it will mark all new missed call log as read.
365      */
markCallLogAsRead(Context context, String phoneNumberString)366     public static void markCallLogAsRead(Context context, String phoneNumberString) {
367         if (context.checkSelfPermission(Manifest.permission.WRITE_CALL_LOG)
368                 != PackageManager.PERMISSION_GRANTED) {
369             Log.w(TAG, "Missing WRITE_CALL_LOG permission; not marking missed calls as read.");
370             return;
371         }
372         ContentValues contentValues = new ContentValues();
373         contentValues.put(CallLog.Calls.NEW, 0);
374         contentValues.put(CallLog.Calls.IS_READ, 1);
375 
376         List<String> selectionArgs = new ArrayList<>();
377         StringBuilder where = new StringBuilder();
378         where.append(CallLog.Calls.NEW);
379         where.append(" = 1 AND ");
380         where.append(CallLog.Calls.TYPE);
381         where.append(" = ?");
382         selectionArgs.add(Integer.toString(CallLog.Calls.MISSED_TYPE));
383         if (!TextUtils.isEmpty(phoneNumberString)) {
384             where.append(" AND ");
385             where.append(CallLog.Calls.NUMBER);
386             where.append(" = ?");
387             selectionArgs.add(phoneNumberString);
388         }
389         String[] selectionArgsArray = new String[0];
390         try {
391             context
392                     .getContentResolver()
393                     .update(
394                             CallLog.Calls.CONTENT_URI,
395                             contentValues,
396                             where.toString(),
397                             selectionArgs.toArray(selectionArgsArray));
398         } catch (IllegalArgumentException e) {
399             Log.e(TAG, "markCallLogAsRead failed", e);
400         }
401     }
402 
makeResourceUri(Context context, int resourceId)403     private static Uri makeResourceUri(Context context, int resourceId) {
404         return new Uri.Builder()
405                 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
406                 .encodedAuthority(context.getBasePackageName())
407                 .appendEncodedPath(String.valueOf(resourceId))
408                 .build();
409     }
410 
411 }
412