1/*
2 * Copyright (C) 2014 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.dialer.util;
17
18import android.app.AlertDialog;
19import android.content.ActivityNotFoundException;
20import android.content.Context;
21import android.content.DialogInterface;
22import android.content.DialogInterface.OnClickListener;
23import android.content.Intent;
24import android.content.SharedPreferences;
25import android.graphics.Point;
26import android.os.Build.VERSION;
27import android.os.Build.VERSION_CODES;
28import android.os.Bundle;
29import android.preference.PreferenceManager;
30import android.support.annotation.NonNull;
31import android.support.v4.content.ContextCompat;
32import android.telecom.TelecomManager;
33import android.telephony.TelephonyManager;
34import android.text.BidiFormatter;
35import android.text.TextDirectionHeuristics;
36import android.view.View;
37import android.view.inputmethod.InputMethodManager;
38import android.widget.Toast;
39import com.android.dialer.common.Assert;
40import com.android.dialer.common.LogUtil;
41import com.android.dialer.telecom.TelecomUtil;
42import java.io.File;
43import java.util.Iterator;
44import java.util.Random;
45
46/** General purpose utility methods for the Dialer. */
47public class DialerUtils {
48
49  /**
50   * Prefix on a dialed number that indicates that the call should be placed through the Wireless
51   * Priority Service.
52   */
53  private static final String WPS_PREFIX = "*272";
54
55  public static final String FILE_PROVIDER_CACHE_DIR = "my_cache";
56
57  private static final Random RANDOM = new Random();
58
59  /**
60   * Attempts to start an activity and displays a toast with the default error message if the
61   * activity is not found, instead of throwing an exception.
62   *
63   * @param context to start the activity with.
64   * @param intent to start the activity with.
65   */
66  public static void startActivityWithErrorToast(Context context, Intent intent) {
67    startActivityWithErrorToast(context, intent, R.string.activity_not_available);
68  }
69
70  /**
71   * Attempts to start an activity and displays a toast with a provided error message if the
72   * activity is not found, instead of throwing an exception.
73   *
74   * @param context to start the activity with.
75   * @param intent to start the activity with.
76   * @param msgId Resource ID of the string to display in an error message if the activity is not
77   *     found.
78   */
79  public static void startActivityWithErrorToast(
80      final Context context, final Intent intent, int msgId) {
81    try {
82      if ((Intent.ACTION_CALL.equals(intent.getAction()))) {
83        // All dialer-initiated calls should pass the touch point to the InCallUI
84        Point touchPoint = TouchPointManager.getInstance().getPoint();
85        if (touchPoint.x != 0 || touchPoint.y != 0) {
86          Bundle extras;
87          // Make sure to not accidentally clobber any existing extras
88          if (intent.hasExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS)) {
89            extras = intent.getParcelableExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
90          } else {
91            extras = new Bundle();
92          }
93          extras.putParcelable(TouchPointManager.TOUCH_POINT, touchPoint);
94          intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
95        }
96
97        if (shouldWarnForOutgoingWps(context, intent.getData().getSchemeSpecificPart())) {
98          LogUtil.i(
99              "DialUtils.startActivityWithErrorToast",
100              "showing outgoing WPS dialog before placing call");
101          AlertDialog.Builder builder = new AlertDialog.Builder(context);
102          builder.setMessage(R.string.outgoing_wps_warning);
103          builder.setPositiveButton(
104              R.string.dialog_continue,
105              new OnClickListener() {
106                @Override
107                public void onClick(DialogInterface dialog, int which) {
108                  placeCallOrMakeToast(context, intent);
109                }
110              });
111          builder.setNegativeButton(android.R.string.cancel, null);
112          builder.create().show();
113        } else {
114          placeCallOrMakeToast(context, intent);
115        }
116      } else {
117        context.startActivity(intent);
118      }
119    } catch (ActivityNotFoundException e) {
120      Toast.makeText(context, msgId, Toast.LENGTH_SHORT).show();
121    }
122  }
123
124  private static void placeCallOrMakeToast(Context context, Intent intent) {
125    final boolean hasCallPermission = TelecomUtil.placeCall(context, intent);
126    if (!hasCallPermission) {
127      // TODO: Make calling activity show request permission dialog and handle
128      // callback results appropriately.
129      Toast.makeText(context, "Cannot place call without Phone permission", Toast.LENGTH_SHORT)
130          .show();
131    }
132  }
133
134  /**
135   * Returns whether the user should be warned about an outgoing WPS call. This checks if there is a
136   * currently active call over LTE. Regardless of the country or carrier, the radio will drop an
137   * active LTE call if a WPS number is dialed, so this warning is necessary.
138   */
139  private static boolean shouldWarnForOutgoingWps(Context context, String number) {
140    if (number != null && number.startsWith(WPS_PREFIX)) {
141      TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
142      boolean isOnVolte =
143          VERSION.SDK_INT >= VERSION_CODES.N
144              && telephonyManager.getVoiceNetworkType() == TelephonyManager.NETWORK_TYPE_LTE;
145      boolean hasCurrentActiveCall =
146          telephonyManager.getCallState() == TelephonyManager.CALL_STATE_OFFHOOK;
147      return isOnVolte && hasCurrentActiveCall;
148    }
149    return false;
150  }
151
152  /**
153   * Closes an {@link AutoCloseable}, silently ignoring any checked exceptions. Does nothing if
154   * null.
155   *
156   * @param closeable to close.
157   */
158  public static void closeQuietly(AutoCloseable closeable) {
159    if (closeable != null) {
160      try {
161        closeable.close();
162      } catch (RuntimeException rethrown) {
163        throw rethrown;
164      } catch (Exception ignored) {
165      }
166    }
167  }
168
169  /**
170   * Joins a list of {@link CharSequence} into a single {@link CharSequence} seperated by ", ".
171   *
172   * @param list List of char sequences to join.
173   * @return Joined char sequences.
174   */
175  public static CharSequence join(Iterable<CharSequence> list) {
176    StringBuilder sb = new StringBuilder();
177    final BidiFormatter formatter = BidiFormatter.getInstance();
178    final CharSequence separator = ", ";
179
180    Iterator<CharSequence> itr = list.iterator();
181    boolean firstTime = true;
182    while (itr.hasNext()) {
183      if (firstTime) {
184        firstTime = false;
185      } else {
186        sb.append(separator);
187      }
188      // Unicode wrap the elements of the list to respect RTL for individual strings.
189      sb.append(
190          formatter.unicodeWrap(itr.next().toString(), TextDirectionHeuristics.FIRSTSTRONG_LTR));
191    }
192
193    // Unicode wrap the joined value, to respect locale's RTL ordering for the whole list.
194    return formatter.unicodeWrap(sb.toString());
195  }
196
197  public static void showInputMethod(View view) {
198    final InputMethodManager imm =
199        (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
200    if (imm != null) {
201      imm.showSoftInput(view, 0);
202    }
203  }
204
205  public static void hideInputMethod(View view) {
206    final InputMethodManager imm =
207        (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
208    if (imm != null) {
209      imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
210    }
211  }
212
213  /**
214   * Create a File in the cache directory that Dialer's FileProvider knows about so they can be
215   * shared to other apps.
216   */
217  public static File createShareableFile(Context context) {
218    long fileId = Math.abs(RANDOM.nextLong());
219    File parentDir = new File(context.getCacheDir(), FILE_PROVIDER_CACHE_DIR);
220    if (!parentDir.exists()) {
221      parentDir.mkdirs();
222    }
223    return new File(parentDir, String.valueOf(fileId));
224  }
225
226  /**
227   * Returns default preference for context accessing device protected storage. This is used when
228   * directBoot is enabled (before device unlocked after boot) since the default shared preference
229   * used normally is not available at this moment for N devices. Returns regular default shared
230   * preference for pre-N devices.
231   */
232  @NonNull
233  public static SharedPreferences getDefaultSharedPreferenceForDeviceProtectedStorageContext(
234      @NonNull Context context) {
235    Assert.isNotNull(context);
236    Context deviceProtectedContext =
237        ContextCompat.isDeviceProtectedStorage(context)
238            ? context
239            : ContextCompat.createDeviceProtectedStorageContext(context);
240    // ContextCompat.createDeviceProtectedStorageContext(context) returns null on pre-N, thus fall
241    // back to regular default shared preference for pre-N devices since devices protected context
242    // is not available.
243    return PreferenceManager.getDefaultSharedPreferences(
244        deviceProtectedContext != null ? deviceProtectedContext : context);
245  }
246}
247