1/*
2 * Copyright (C) 2015 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 */
16package com.android.car.dialer.telecom;
17
18import android.content.ContentResolver;
19import android.content.ContentUris;
20import android.content.Context;
21import android.content.res.Resources;
22import android.database.Cursor;
23import android.graphics.Bitmap;
24import android.graphics.BitmapFactory;
25import android.graphics.BitmapFactory.Options;
26import android.graphics.Rect;
27import android.net.Uri;
28import android.provider.ContactsContract;
29import android.provider.ContactsContract.CommonDataKinds.Phone;
30import android.provider.ContactsContract.PhoneLookup;
31import android.provider.Settings;
32import android.support.annotation.Nullable;
33import android.support.annotation.WorkerThread;
34import android.telecom.Call;
35import android.telephony.PhoneNumberUtils;
36import android.telephony.TelephonyManager;
37import android.text.TextUtils;
38import android.text.format.DateUtils;
39import android.util.Log;
40import android.widget.ImageView;
41import com.android.car.apps.common.CircleBitmapDrawable;
42import com.android.car.apps.common.LetterTileDrawable;
43import com.android.car.dialer.R;
44
45import java.io.InputStream;
46import java.util.Locale;
47
48public class TelecomUtils {
49    private final static String TAG = "Em.TelecomUtils";
50
51    private static final String[] CONTACT_ID_PROJECTION = new String[] {
52            ContactsContract.PhoneLookup.DISPLAY_NAME,
53            ContactsContract.PhoneLookup.TYPE,
54            ContactsContract.PhoneLookup.LABEL,
55            ContactsContract.PhoneLookup._ID
56    };
57
58    private static String sVoicemailNumber;
59    private static TelephonyManager sTelephonyManager;
60
61    @WorkerThread
62    public static Bitmap getContactPhotoFromNumber(ContentResolver contentResolver, String number) {
63        if (number == null) {
64            return null;
65        }
66
67        int id = getContactIdFromNumber(contentResolver, number);
68        if (id == 0) {
69            return null;
70        }
71        return getContactPhotoFromId(contentResolver, id);
72    }
73
74    /**
75     * Return the contact id for the given contact id
76     * @param id the contact id to get the photo for
77     * @return the contact photo if it is found, null otherwise.
78     */
79    public static Bitmap getContactPhotoFromId(ContentResolver contentResolver, long id) {
80        Uri photoUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
81        InputStream photoDataStream = ContactsContract.Contacts.openContactPhotoInputStream(
82                contentResolver, photoUri, true);
83
84        Options options = new Options();
85        options.inPreferQualityOverSpeed = true;
86        // Scaling will be handled by later. We shouldn't scale multiple times to avoid
87        // quality lost due to multiple potential scaling up and down.
88        options.inScaled = false;
89
90        Rect nullPadding = null;
91        Bitmap photo = BitmapFactory.decodeStream(photoDataStream, nullPadding, options);
92        if (photo != null) {
93            photo.setDensity(Bitmap.DENSITY_NONE);
94        }
95        return photo;
96    }
97
98    /**
99     * Return the contact id for the given phone number.
100     * @param number Caller phone number
101     * @return the contact id if it is found, 0 otherwise.
102     */
103    public static int getContactIdFromNumber(ContentResolver cr, String number) {
104        if (number == null || number.isEmpty()) {
105            return 0;
106        }
107
108        Uri uri = Uri.withAppendedPath(
109                ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
110                Uri.encode(number));
111        Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null);
112
113        try {
114            if (cursor != null && cursor.moveToFirst()) {
115                int id = cursor.getInt(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID));
116                return id;
117            }
118        }
119        finally {
120            if (cursor != null) {
121                cursor.close();
122            }
123        }
124        return 0;
125    }
126
127    /**
128     * Return the label for the given phone number.
129     * @param number Caller phone number
130     * @return the label if it is found, 0 otherwise.
131     */
132    public static CharSequence getTypeFromNumber(Context context, String number) {
133        if (Log.isLoggable(TAG, Log.DEBUG)) {
134            Log.d(TAG, "getTypeFromNumber, number: " + number);
135        }
136        String defaultLabel = "";
137        if (number == null || number.isEmpty()) {
138            return defaultLabel;
139        }
140
141        ContentResolver cr = context.getContentResolver();
142        Resources res = context.getResources();
143        Uri uri = Uri.withAppendedPath(
144                PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
145        Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null);
146
147        try {
148            if (cursor != null && cursor.moveToFirst()) {
149                int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
150                int type = cursor.getInt(typeColumn);
151                int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
152                String label = cursor.getString(labelColumn);
153                CharSequence typeLabel =
154                        Phone.getTypeLabel(res, type, label);
155                return typeLabel;
156            }
157        }
158        finally {
159            if (cursor != null) {
160                cursor.close();
161            }
162        }
163        return defaultLabel;
164    }
165
166    public static String getVoicemailNumber(Context context) {
167        if (sVoicemailNumber == null) {
168            sVoicemailNumber = getTelephonyManager(context).getVoiceMailNumber();
169        }
170        return sVoicemailNumber;
171    }
172
173    public static TelephonyManager getTelephonyManager(Context context) {
174        if (sTelephonyManager == null) {
175            sTelephonyManager =
176                    (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
177        }
178        return sTelephonyManager;
179    }
180
181    public static String getFormattedNumber(Context context, String number) {
182        if (Log.isLoggable(TAG, Log.DEBUG)) {
183            Log.d(TAG, "getFormattedNumber: " + number);
184        }
185        if (number == null) {
186            return "";
187        }
188
189        String countryIso = getTelephonyManager(context).getSimCountryIso().toUpperCase(Locale.US);
190        if (countryIso.length() != 2) {
191            countryIso = Locale.getDefault().getCountry();
192            if (countryIso == null || countryIso.length() != 2) {
193                countryIso = "US";
194            }
195        }
196        if (Log.isLoggable(TAG, Log.DEBUG)) {
197            Log.d(TAG, "PhoneNumberUtils.formatNumberToE16, number: "
198                    + number + ", country: " + countryIso);
199        }
200        String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso);
201        String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso);
202        formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber;
203        if (Log.isLoggable(TAG, Log.DEBUG)) {
204            Log.d(TAG, "getFormattedNumber, result: " + formattedNumber);
205        }
206        return formattedNumber;
207    }
208
209    public static String getDisplayName(Context context, UiCall call) {
210        // A call might get created before its children are added. In that case, the display name
211        // would go from "Unknown" to "Conference call" therefore we don't want to cache it.
212        if (call.hasChildren()) {
213            return context.getString(R.string.conference_call);
214        }
215
216        return getDisplayName(context, call.getNumber(), call.getGatewayInfoOriginalAddress());
217    }
218
219    public static String getDisplayName(Context context, String number) {
220        return getDisplayName(context, number, null);
221    }
222
223    private static String getDisplayName(Context context, String number, Uri gatewayOriginalAddress) {
224        if (Log.isLoggable(TAG, Log.DEBUG)) {
225            Log.d(TAG, "getDisplayName: " + number
226                    + ", gatewayOriginalAddress: " + gatewayOriginalAddress);
227        }
228
229        if (TextUtils.isEmpty(number)) {
230            return context.getString(R.string.unknown);
231        }
232        ContentResolver cr = context.getContentResolver();
233        String name;
234        if (number.equals(getVoicemailNumber(context))) {
235            name = context.getResources().getString(R.string.voicemail);
236        } else {
237            name = getContactNameFromNumber(cr, number);
238        }
239
240        if (name == null) {
241            name = getFormattedNumber(context, number);
242        }
243        if (name == null && gatewayOriginalAddress != null) {
244            name = gatewayOriginalAddress.getSchemeSpecificPart();
245        }
246        if (name == null) {
247            name = context.getString(R.string.unknown);
248        }
249        return name;
250    }
251
252    private static String getContactNameFromNumber(ContentResolver cr, String number) {
253        Uri uri = Uri.withAppendedPath(
254                ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
255
256        Cursor cursor = null;
257        String name = null;
258        try {
259            cursor = cr.query(uri,
260                    new String[] {ContactsContract.PhoneLookup.DISPLAY_NAME}, null, null, null);
261            if (cursor != null && cursor.moveToFirst()) {
262                name = cursor.getString(0);
263            }
264        } finally {
265            if (cursor != null) {
266                cursor.close();
267            }
268        }
269        return name;
270    }
271
272    /**
273     * @return A formatted string that has information about the phone call
274     * Possible strings:
275     * "Mobile · Dialing"
276     * "Mobile · 1:05"
277     * "Bluetooth disconnected"
278     */
279    public static String getCallInfoText(Context context, UiCall call, CharSequence label) {
280        String text;
281        if (call.getState() == Call.STATE_ACTIVE) {
282            long duration = System.currentTimeMillis() - call.getConnectTimeMillis();
283            String durationString = DateUtils.formatElapsedTime(duration / 1000);
284            if (!TextUtils.isEmpty(durationString) && !TextUtils.isEmpty(label)) {
285                text = context.getString(R.string.phone_label_with_info, label, durationString);
286            } else if (!TextUtils.isEmpty(durationString)) {
287                text = durationString;
288            } else if (!TextUtils.isEmpty(label)) {
289                text = (String) label;
290            } else {
291                text = "";
292            }
293        } else {
294            String state = callStateToUiString(context, call.getState());
295            if (!TextUtils.isEmpty(label)) {
296                text = context.getString(R.string.phone_label_with_info, label, state);
297            } else {
298                text = state;
299            }
300        }
301        return text;
302    }
303
304    /**
305     * @return A string representation of the call state that can be presented to a user.
306     */
307    public static String callStateToUiString(Context context, int state) {
308        Resources res = context.getResources();
309        switch(state) {
310            case Call.STATE_ACTIVE:
311                return res.getString(R.string.call_state_call_active);
312            case Call.STATE_HOLDING:
313                return res.getString(R.string.call_state_hold);
314            case Call.STATE_NEW:
315            case Call.STATE_CONNECTING:
316                return res.getString(R.string.call_state_connecting);
317            case Call.STATE_SELECT_PHONE_ACCOUNT:
318            case Call.STATE_DIALING:
319                return res.getString(R.string.call_state_dialing);
320            case Call.STATE_DISCONNECTED:
321                return res.getString(R.string.call_state_call_ended);
322            case Call.STATE_RINGING:
323                return res.getString(R.string.call_state_call_ringing);
324            case Call.STATE_DISCONNECTING:
325                return res.getString(R.string.call_state_call_ending);
326            default:
327                throw new IllegalStateException("Unknown Call State: " + state);
328        }
329    }
330
331    public static boolean isNetworkAvailable(Context context) {
332        TelephonyManager tm =
333                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
334        return tm.getNetworkType() != TelephonyManager.NETWORK_TYPE_UNKNOWN &&
335                tm.getSimState() == TelephonyManager.SIM_STATE_READY;
336    }
337
338    public static boolean isAirplaneModeOn(Context context) {
339        return Settings.System.getInt(context.getContentResolver(),
340                Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
341    }
342
343    /**
344     * Sets a Contact bitmap on the provided image taking into account fail cases.
345     * It will attempt to load a Bitmap from the Contacts store, otherwise it will paint
346     * a the first letter of the contact name.
347     *
348     * @param number A key to have a consisten color per phone number.
349     * @return A worker task if a new one was needed to load the bitmap.
350     */
351    @Nullable public static ContactBitmapWorker setContactBitmapAsync(Context context,
352            final ImageView icon, final @Nullable String name, final String number) {
353        return ContactBitmapWorker.loadBitmap(context.getContentResolver(), icon, number,
354                new ContactBitmapWorker.BitmapWorkerListener() {
355                    @Override
356                    public void onBitmapLoaded(@Nullable Bitmap bitmap) {
357                        Resources r = icon.getResources();
358                        if (bitmap != null) {
359                            icon.setScaleType(ImageView.ScaleType.CENTER_CROP);
360                            icon.setImageDrawable(new CircleBitmapDrawable(r, bitmap));
361                        } else {
362                            icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
363                            LetterTileDrawable letterTileDrawable = new LetterTileDrawable(r);
364                            letterTileDrawable.setContactDetails(name, number);
365                            letterTileDrawable.setIsCircular(true);
366                            icon.setImageDrawable(letterTileDrawable);
367                        }
368                    }
369                });
370    }
371
372}
373