From d5e47f6da5b08b13ecdfa7f1edc7e12aeb83fab9 Mon Sep 17 00:00:00 2001 From: Eric Erfanian Date: Wed, 15 Mar 2017 14:41:07 -0700 Subject: Update Dialer source from latest green build. * Refactor voicemail component * Add new enriched calling components Test: treehugger, manual aosp testing Change-Id: I521a0f86327d4b42e14d93927c7d613044ed5942 --- .../incallui/calllocation/impl/AndroidManifest.xml | 26 ++ .../incallui/calllocation/impl/AuthException.java | 25 ++ .../calllocation/impl/CallLocationImpl.java | 67 +++++ .../calllocation/impl/CallLocationModule.java | 29 +++ .../calllocation/impl/DownloadMapImageTask.java | 77 ++++++ .../impl/GoogleLocationSettingHelper.java | 123 +++++++++ .../incallui/calllocation/impl/HttpFetcher.java | 289 +++++++++++++++++++++ .../calllocation/impl/LocationFragment.java | 197 ++++++++++++++ .../incallui/calllocation/impl/LocationHelper.java | 219 ++++++++++++++++ .../calllocation/impl/LocationPresenter.java | 98 +++++++ .../calllocation/impl/LocationUrlBuilder.java | 177 +++++++++++++ .../calllocation/impl/ReverseGeocodeTask.java | 144 ++++++++++ .../calllocation/impl/TrafficStatsTags.java | 29 +++ .../impl/res/layout/location_fragment.xml | 134 ++++++++++ .../calllocation/impl/res/values/dimens.xml | 6 + .../calllocation/impl/res/values/strings.xml | 15 ++ .../calllocation/impl/res/values/styles.xml | 28 ++ 17 files changed, 1683 insertions(+) create mode 100644 java/com/android/incallui/calllocation/impl/AndroidManifest.xml create mode 100644 java/com/android/incallui/calllocation/impl/AuthException.java create mode 100644 java/com/android/incallui/calllocation/impl/CallLocationImpl.java create mode 100644 java/com/android/incallui/calllocation/impl/CallLocationModule.java create mode 100644 java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java create mode 100644 java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java create mode 100644 java/com/android/incallui/calllocation/impl/HttpFetcher.java create mode 100644 java/com/android/incallui/calllocation/impl/LocationFragment.java create mode 100644 java/com/android/incallui/calllocation/impl/LocationHelper.java create mode 100644 java/com/android/incallui/calllocation/impl/LocationPresenter.java create mode 100644 java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java create mode 100644 java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java create mode 100644 java/com/android/incallui/calllocation/impl/TrafficStatsTags.java create mode 100644 java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml create mode 100644 java/com/android/incallui/calllocation/impl/res/values/dimens.xml create mode 100644 java/com/android/incallui/calllocation/impl/res/values/strings.xml create mode 100644 java/com/android/incallui/calllocation/impl/res/values/styles.xml (limited to 'java/com/android/incallui/calllocation/impl') diff --git a/java/com/android/incallui/calllocation/impl/AndroidManifest.xml b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml new file mode 100644 index 000000000..550c5808c --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/java/com/android/incallui/calllocation/impl/AuthException.java b/java/com/android/incallui/calllocation/impl/AuthException.java new file mode 100644 index 000000000..26def2fc9 --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/AuthException.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +/** For detecting backend authorization errors */ +public class AuthException extends Exception { + + public AuthException(String detailMessage) { + super(detailMessage); + } +} diff --git a/java/com/android/incallui/calllocation/impl/CallLocationImpl.java b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java new file mode 100644 index 000000000..20f5ffb0f --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/CallLocationImpl.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +import android.content.Context; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import com.android.dialer.common.Assert; +import com.android.incallui.calllocation.CallLocation; +import javax.inject.Inject; + +/** Uses Google Play Services to show the user's location during an emergency call. */ +public class CallLocationImpl implements CallLocation { + + private LocationHelper locationHelper; + private LocationFragment locationFragment; + + @Inject + public CallLocationImpl() {} + + @MainThread + @Override + public boolean canGetLocation(@NonNull Context context) { + Assert.isMainThread(); + return LocationHelper.canGetLocation(context); + } + + @MainThread + @NonNull + @Override + public Fragment getLocationFragment(@NonNull Context context) { + Assert.isMainThread(); + if (locationFragment == null) { + locationFragment = new LocationFragment(); + locationHelper = new LocationHelper(context); + locationHelper.addLocationListener(locationFragment.getPresenter()); + } + return locationFragment; + } + + @MainThread + @Override + public void close() { + Assert.isMainThread(); + if (locationFragment != null) { + locationHelper.removeLocationListener(locationFragment.getPresenter()); + locationHelper.close(); + locationFragment = null; + locationHelper = null; + } + } +} diff --git a/java/com/android/incallui/calllocation/impl/CallLocationModule.java b/java/com/android/incallui/calllocation/impl/CallLocationModule.java new file mode 100644 index 000000000..73e85554e --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/CallLocationModule.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +import com.android.incallui.calllocation.CallLocation; +import dagger.Binds; +import dagger.Module; + +/** This module provides an instance of call location. */ +@Module +public abstract class CallLocationModule { + + @Binds + public abstract CallLocation bindCallLocation(CallLocationImpl callLocation); +} diff --git a/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java new file mode 100644 index 000000000..801b0d35c --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/DownloadMapImageTask.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +import android.graphics.drawable.Drawable; +import android.location.Location; +import android.net.TrafficStats; +import android.os.AsyncTask; +import com.android.dialer.common.LogUtil; +import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.net.URL; + +class DownloadMapImageTask extends AsyncTask { + + private static final String STATIC_MAP_SRC_NAME = "src"; + + private final WeakReference mUiReference; + + public DownloadMapImageTask(WeakReference uiReference) { + mUiReference = uiReference; + } + + @Override + protected Drawable doInBackground(Location... locations) { + LocationUi ui = mUiReference.get(); + if (ui == null) { + return null; + } + if (locations == null || locations.length == 0) { + LogUtil.e("DownloadMapImageTask.doInBackground", "No location provided"); + return null; + } + + try { + URL mapUrl = new URL(LocationUrlBuilder.getStaticMapUrl(ui.getContext(), locations[0])); + InputStream content = (InputStream) mapUrl.getContent(); + + TrafficStats.setThreadStatsTag(TrafficStatsTags.DOWNLOAD_LOCATION_MAP_TAG); + return Drawable.createFromStream(content, STATIC_MAP_SRC_NAME); + } catch (Exception ex) { + LogUtil.e("DownloadMapImageTask.doInBackground", "Exception!!!", ex); + return null; + } finally { + TrafficStats.clearThreadStatsTag(); + } + } + + @Override + protected void onPostExecute(Drawable mapImage) { + LocationUi ui = mUiReference.get(); + if (ui == null) { + return; + } + + try { + ui.setMap(mapImage); + } catch (Exception ex) { + LogUtil.e("DownloadMapImageTask.onPostExecute", "Exception!!!", ex); + } + } +} diff --git a/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java new file mode 100644 index 000000000..18a80b8ce --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.net.Uri; +import android.provider.Settings.Secure; +import android.provider.Settings.SettingNotFoundException; +import com.android.dialer.common.LogUtil; + +/** + * Helper class to check if Google Location Services is enabled. This class is based on + * https://docs.google.com/a/google.com/document/d/1sGm8pHgGY1QmxbLCwTZuWQASEDN7CFW9EPSZXAuGQfo + */ +public class GoogleLocationSettingHelper { + + /** User has disagreed to use location for Google services. */ + public static final int USE_LOCATION_FOR_SERVICES_OFF = 0; + /** User has agreed to use location for Google services. */ + public static final int USE_LOCATION_FOR_SERVICES_ON = 1; + /** The user has neither agreed nor disagreed to use location for Google services yet. */ + public static final int USE_LOCATION_FOR_SERVICES_NOT_SET = 2; + + private static final String GOOGLE_SETTINGS_AUTHORITY = "com.google.settings"; + private static final Uri GOOGLE_SETTINGS_CONTENT_URI = + Uri.parse("content://" + GOOGLE_SETTINGS_AUTHORITY + "/partner"); + private static final String NAME = "name"; + private static final String VALUE = "value"; + private static final String USE_LOCATION_FOR_SERVICES = "use_location_for_services"; + + /** Determine if Google apps need to conform to the USE_LOCATION_FOR_SERVICES setting. */ + public static boolean isEnforceable(Context context) { + final ResolveInfo ri = + context + .getPackageManager() + .resolveActivity( + new Intent("com.google.android.gsf.GOOGLE_APPS_LOCATION_SETTINGS"), + PackageManager.MATCH_DEFAULT_ONLY); + return ri != null; + } + + /** + * Get the current value for the 'Use value for location' setting. + * + * @return One of {@link #USE_LOCATION_FOR_SERVICES_NOT_SET}, {@link + * #USE_LOCATION_FOR_SERVICES_OFF} or {@link #USE_LOCATION_FOR_SERVICES_ON}. + */ + private static int getUseLocationForServices(Context context) { + final ContentResolver resolver = context.getContentResolver(); + Cursor c = null; + String stringValue = null; + try { + c = + resolver.query( + GOOGLE_SETTINGS_CONTENT_URI, + new String[] {VALUE}, + NAME + "=?", + new String[] {USE_LOCATION_FOR_SERVICES}, + null); + if (c != null && c.moveToNext()) { + stringValue = c.getString(0); + } + } catch (final RuntimeException e) { + LogUtil.e( + "GoogleLocationSettingHelper.getUseLocationForServices", + "Failed to get 'Use My Location' setting", + e); + } finally { + if (c != null) { + c.close(); + } + } + if (stringValue == null) { + return USE_LOCATION_FOR_SERVICES_NOT_SET; + } + int value; + try { + value = Integer.parseInt(stringValue); + } catch (final NumberFormatException nfe) { + value = USE_LOCATION_FOR_SERVICES_NOT_SET; + } + return value; + } + + /** Whether or not the system location setting is enable */ + public static boolean isSystemLocationSettingEnabled(Context context) { + try { + return Secure.getInt(context.getContentResolver(), Secure.LOCATION_MODE) + != Secure.LOCATION_MODE_OFF; + } catch (SettingNotFoundException e) { + LogUtil.e( + "GoogleLocationSettingHelper.isSystemLocationSettingEnabled", + "Failed to get System Location setting", + e); + return false; + } + } + + /** Convenience method that returns true is GLS is ON or if it's not enforceable. */ + public static boolean isGoogleLocationServicesEnabled(Context context) { + return !isEnforceable(context) + || getUseLocationForServices(context) == USE_LOCATION_FOR_SERVICES_ON; + } +} diff --git a/java/com/android/incallui/calllocation/impl/HttpFetcher.java b/java/com/android/incallui/calllocation/impl/HttpFetcher.java new file mode 100644 index 000000000..7bfbaa6ef --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/HttpFetcher.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +import static com.android.dialer.util.DialerUtils.closeQuietly; + +import android.content.Context; +import android.net.Uri; +import android.net.Uri.Builder; +import android.os.SystemClock; +import android.util.Pair; +import com.android.dialer.common.LogUtil; +import com.android.dialer.util.MoreStrings; +import com.google.android.common.http.UrlRules; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** Utility for making http requests. */ +public class HttpFetcher { + + // Phone number + public static final String PARAM_ID = "id"; + // auth token + public static final String PARAM_ACCESS_TOKEN = "access_token"; + private static final String TAG = HttpFetcher.class.getSimpleName(); + + /** + * Send a http request to the given url. + * + * @param urlString The url to request. + * @return The response body as a byte array. Or {@literal null} if status code is not 2xx. + * @throws java.io.IOException when an error occurs. + */ + public static byte[] sendRequestAsByteArray( + Context context, String urlString, String requestMethod, List> headers) + throws IOException, AuthException { + Objects.requireNonNull(urlString); + + URL url = reWriteUrl(context, urlString); + if (url == null) { + return null; + } + + HttpURLConnection conn = null; + InputStream is = null; + boolean isError = false; + final long start = SystemClock.uptimeMillis(); + try { + conn = (HttpURLConnection) url.openConnection(); + setMethodAndHeaders(conn, requestMethod, headers); + int responseCode = conn.getResponseCode(); + LogUtil.i("HttpFetcher.sendRequestAsByteArray", "response code: " + responseCode); + // All 2xx codes are successful. + if (responseCode / 100 == 2) { + is = conn.getInputStream(); + } else { + is = conn.getErrorStream(); + isError = true; + } + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final byte[] buffer = new byte[1024]; + int bytesRead; + + while ((bytesRead = is.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + + if (isError) { + handleBadResponse(url.toString(), baos.toByteArray()); + if (responseCode == 401) { + throw new AuthException("Auth error"); + } + return null; + } + + byte[] response = baos.toByteArray(); + LogUtil.i("HttpFetcher.sendRequestAsByteArray", "received " + response.length + " bytes"); + long end = SystemClock.uptimeMillis(); + LogUtil.i("HttpFetcher.sendRequestAsByteArray", "fetch took " + (end - start) + " ms"); + return response; + } finally { + closeQuietly(is); + if (conn != null) { + conn.disconnect(); + } + } + } + + /** + * Send a http request to the given url. + * + * @return The response body as a InputStream. Or {@literal null} if status code is not 2xx. + * @throws java.io.IOException when an error occurs. + */ + public static InputStream sendRequestAsInputStream( + Context context, String urlString, String requestMethod, List> headers) + throws IOException, AuthException { + Objects.requireNonNull(urlString); + + URL url = reWriteUrl(context, urlString); + if (url == null) { + return null; + } + + HttpURLConnection httpUrlConnection = null; + boolean isSuccess = false; + try { + httpUrlConnection = (HttpURLConnection) url.openConnection(); + setMethodAndHeaders(httpUrlConnection, requestMethod, headers); + int responseCode = httpUrlConnection.getResponseCode(); + LogUtil.i("HttpFetcher.sendRequestAsInputStream", "response code: " + responseCode); + + if (responseCode == 401) { + throw new AuthException("Auth error"); + } else if (responseCode / 100 == 2) { // All 2xx codes are successful. + InputStream is = httpUrlConnection.getInputStream(); + if (is != null) { + is = new HttpInputStreamWrapper(httpUrlConnection, is); + isSuccess = true; + return is; + } + } + + return null; + } finally { + if (httpUrlConnection != null && !isSuccess) { + httpUrlConnection.disconnect(); + } + } + } + + /** + * Set http method and headers. + * + * @param conn The connection to add headers to. + * @param requestMethod request method + * @param headers http headers where the first item in the pair is the key and second item is the + * value. + */ + private static void setMethodAndHeaders( + HttpURLConnection conn, String requestMethod, List> headers) + throws ProtocolException { + conn.setRequestMethod(requestMethod); + if (headers != null) { + for (Pair pair : headers) { + conn.setRequestProperty(pair.first, pair.second); + } + } + } + + private static String obfuscateUrl(String urlString) { + final Uri uri = Uri.parse(urlString); + final Builder builder = + new Builder().scheme(uri.getScheme()).authority(uri.getAuthority()).path(uri.getPath()); + final Set names = uri.getQueryParameterNames(); + for (String name : names) { + if (PARAM_ACCESS_TOKEN.equals(name)) { + builder.appendQueryParameter(name, "token"); + } else { + final String value = uri.getQueryParameter(name); + if (PARAM_ID.equals(name)) { + builder.appendQueryParameter(name, MoreStrings.toSafeString(value)); + } else { + builder.appendQueryParameter(name, value); + } + } + } + return builder.toString(); + } + + /** Same as {@link #getRequestAsString(Context, String, String, List)} with null headers. */ + public static String getRequestAsString(Context context, String urlString) + throws IOException, AuthException { + return getRequestAsString(context, urlString, "GET" /* Default to get. */, null); + } + + /** + * Send a http request to the given url. + * + * @param context The android context. + * @param urlString The url to request. + * @param headers Http headers to pass in the request. {@literal null} is allowed. + * @return The response body as a String. Or {@literal null} if status code is not 2xx. + * @throws java.io.IOException when an error occurs. + */ + public static String getRequestAsString( + Context context, String urlString, String requestMethod, List> headers) + throws IOException, AuthException { + final byte[] byteArr = sendRequestAsByteArray(context, urlString, requestMethod, headers); + if (byteArr == null) { + // Encountered error response... just return. + return null; + } + final String response = new String(byteArr); + LogUtil.i("HttpFetcher.getRequestAsString", "response body: " + response); + return response; + } + + /** + * Lookup up url re-write rules from gServices and apply to the given url. + * + *

https://wiki.corp.google.com/twiki/bin/view/Main/AndroidGservices#URL_Rewriting_Rules + * + * @return The new url. + */ + private static URL reWriteUrl(Context context, String url) { + final UrlRules rules = UrlRules.getRules(context.getContentResolver()); + final UrlRules.Rule rule = rules.matchRule(url); + final String newUrl = rule.apply(url); + + if (newUrl == null) { + if (LogUtil.isDebugEnabled()) { + // Url is blocked by re-write. + LogUtil.i( + "HttpFetcher.reWriteUrl", + "url " + obfuscateUrl(url) + " is blocked. Ignoring request."); + } + return null; + } + + if (LogUtil.isDebugEnabled()) { + LogUtil.i("HttpFetcher.reWriteUrl", "fetching " + obfuscateUrl(newUrl)); + if (!newUrl.equals(url)) { + LogUtil.i( + "HttpFetcher.reWriteUrl", + "Original url: " + obfuscateUrl(url) + ", after re-write: " + obfuscateUrl(newUrl)); + } + } + + URL urlObject = null; + try { + urlObject = new URL(newUrl); + } catch (MalformedURLException e) { + LogUtil.e("HttpFetcher.reWriteUrl", "failed to parse url: " + url, e); + } + return urlObject; + } + + private static void handleBadResponse(String url, byte[] response) { + LogUtil.i("HttpFetcher.handleBadResponse", "Got bad response code from url: " + url); + LogUtil.i("HttpFetcher.handleBadResponse", new String(response)); + } + + /** Disconnect {@link HttpURLConnection} when InputStream is closed */ + private static class HttpInputStreamWrapper extends FilterInputStream { + + final HttpURLConnection mHttpUrlConnection; + final long mStartMillis = SystemClock.uptimeMillis(); + + public HttpInputStreamWrapper(HttpURLConnection conn, InputStream in) { + super(in); + mHttpUrlConnection = conn; + } + + @Override + public void close() throws IOException { + super.close(); + mHttpUrlConnection.disconnect(); + if (LogUtil.isDebugEnabled()) { + long endMillis = SystemClock.uptimeMillis(); + LogUtil.i("HttpFetcher.close", "fetch took " + (endMillis - mStartMillis) + " ms"); + } + } + } +} diff --git a/java/com/android/incallui/calllocation/impl/LocationFragment.java b/java/com/android/incallui/calllocation/impl/LocationFragment.java new file mode 100644 index 000000000..b152cd683 --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/LocationFragment.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +import android.animation.LayoutTransition; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.location.Location; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.ViewAnimator; +import com.android.dialer.common.LogUtil; +import com.android.incallui.baseui.BaseFragment; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * Fragment which shows location during E911 calls, to supplement the user with accurate location + * information in case the user is asked for their location by the emergency responder. + * + *

If location data is inaccurate, stale, or unavailable, this should not be shown. + */ +public class LocationFragment extends BaseFragment + implements LocationPresenter.LocationUi { + + private static final String ADDRESS_DELIMITER = ","; + + // Indexes used to animate fading between views + private static final int LOADING_VIEW_INDEX = 0; + private static final int LOCATION_VIEW_INDEX = 1; + private static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5); + + private ViewAnimator viewAnimator; + private ImageView locationMap; + private TextView addressLine1; + private TextView addressLine2; + private TextView latLongLine; + private Location location; + private ViewGroup locationLayout; + + private boolean isMapSet; + private boolean isAddressSet; + private boolean isLocationSet; + private boolean hasTimeoutStarted; + + private final Handler handler = new Handler(); + private final Runnable dataTimeoutRunnable = + () -> { + LogUtil.i( + "LocationFragment.dataTimeoutRunnable", + "timed out so animate any future layout changes"); + locationLayout.setLayoutTransition(new LayoutTransition()); + showLocationNow(); + }; + + @Override + public LocationPresenter createPresenter() { + return new LocationPresenter(); + } + + @Override + public LocationPresenter.LocationUi getUi() { + return this; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.location_fragment, container, false); + viewAnimator = (ViewAnimator) view.findViewById(R.id.location_view_animator); + locationMap = (ImageView) view.findViewById(R.id.location_map); + addressLine1 = (TextView) view.findViewById(R.id.address_line_one); + addressLine2 = (TextView) view.findViewById(R.id.address_line_two); + latLongLine = (TextView) view.findViewById(R.id.lat_long_line); + locationLayout = (ViewGroup) view.findViewById(R.id.location_layout); + view.setOnClickListener( + v -> { + LogUtil.enterBlock("LocationFragment.onCreateView"); + launchMap(); + }); + return view; + } + + @Override + public void onDestroy() { + super.onDestroy(); + handler.removeCallbacks(dataTimeoutRunnable); + } + + @Override + public void setMap(Drawable mapImage) { + LogUtil.enterBlock("LocationFragment.setMap"); + isMapSet = true; + locationMap.setVisibility(View.VISIBLE); + locationMap.setImageDrawable(mapImage); + displayWhenReady(); + } + + @Override + public void setAddress(String address) { + LogUtil.i("LocationFragment.setAddress", address); + isAddressSet = true; + addressLine1.setVisibility(View.VISIBLE); + addressLine2.setVisibility(View.VISIBLE); + if (TextUtils.isEmpty(address)) { + addressLine1.setText(null); + addressLine2.setText(null); + } else { + + // Split the address after the first delimiter for display, if present. + // For example, "1600 Amphitheatre Parkway, Mountain View, CA 94043" + // => "1600 Amphitheatre Parkway" + // => "Mountain View, CA 94043" + int splitIndex = address.indexOf(ADDRESS_DELIMITER); + if (splitIndex >= 0) { + updateText(addressLine1, address.substring(0, splitIndex).trim()); + updateText(addressLine2, address.substring(splitIndex + 1).trim()); + } else { + updateText(addressLine1, address); + updateText(addressLine2, null); + } + } + displayWhenReady(); + } + + @Override + public void setLocation(Location location) { + LogUtil.i("LocationFragment.setLocation", String.valueOf(location)); + isLocationSet = true; + this.location = location; + + if (location != null) { + latLongLine.setVisibility(View.VISIBLE); + latLongLine.setText( + getContext() + .getString( + R.string.lat_long_format, location.getLatitude(), location.getLongitude())); + } + displayWhenReady(); + } + + private void displayWhenReady() { + // Show the location if all data has loaded, otherwise prime the timeout + if (isMapSet && isAddressSet && isLocationSet) { + showLocationNow(); + } else if (!hasTimeoutStarted) { + handler.postDelayed(dataTimeoutRunnable, TIMEOUT_MILLIS); + hasTimeoutStarted = true; + } + } + + private void showLocationNow() { + handler.removeCallbacks(dataTimeoutRunnable); + if (viewAnimator.getDisplayedChild() != LOCATION_VIEW_INDEX) { + viewAnimator.setDisplayedChild(LOCATION_VIEW_INDEX); + } + } + + @Override + public Context getContext() { + return getActivity(); + } + + private void launchMap() { + if (location != null) { + startActivity( + LocationUrlBuilder.getShowMapIntent( + location, addressLine1.getText(), addressLine2.getText())); + } + } + + private static void updateText(TextView view, String text) { + if (!Objects.equals(text, view.getText())) { + view.setText(text); + } + } +} diff --git a/java/com/android/incallui/calllocation/impl/LocationHelper.java b/java/com/android/incallui/calllocation/impl/LocationHelper.java new file mode 100644 index 000000000..645e9b86a --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/LocationHelper.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +import android.content.Context; +import android.location.Location; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.MainThread; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.util.PermissionsUtil; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; +import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.location.LocationListener; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; +import java.util.ArrayList; +import java.util.List; + +/** Uses the Fused location service to get location and pass updates on to listeners. */ +public class LocationHelper { + + private static final int MIN_UPDATE_INTERVAL_MS = 30 * 1000; + private static final int LAST_UPDATE_THRESHOLD_MS = 60 * 1000; + private static final int LOCATION_ACCURACY_THRESHOLD_METERS = 100; + + private final LocationHelperInternal locationHelperInternal; + private final List listeners = new ArrayList<>(); + + @MainThread + LocationHelper(Context context) { + Assert.isMainThread(); + Assert.checkArgument(canGetLocation(context)); + locationHelperInternal = new LocationHelperInternal(context); + } + + static boolean canGetLocation(Context context) { + if (!PermissionsUtil.hasLocationPermissions(context)) { + LogUtil.i("LocationHelper.canGetLocation", "no location permissions."); + return false; + } + + // Ensure that both system location setting is on and google location services are enabled. + if (!GoogleLocationSettingHelper.isGoogleLocationServicesEnabled(context) + || !GoogleLocationSettingHelper.isSystemLocationSettingEnabled(context)) { + LogUtil.i("LocationHelper.canGetLocation", "location service is disabled."); + return false; + } + return true; + } + + /** + * Whether the location is valid. We consider it valid if it was recorded within the specified + * time threshold of the present and has an accuracy less than the specified distance threshold. + * + * @param location The location to determine the validity of. + * @return {@code true} if the location is valid, and {@code false} otherwise. + */ + static boolean isValidLocation(Location location) { + if (location != null) { + long locationTimeMs = location.getTime(); + long elapsedTimeMs = System.currentTimeMillis() - locationTimeMs; + if (elapsedTimeMs > LAST_UPDATE_THRESHOLD_MS) { + LogUtil.i("LocationHelper.isValidLocation", "stale location, age: " + elapsedTimeMs); + return false; + } + if (location.getAccuracy() > LOCATION_ACCURACY_THRESHOLD_METERS) { + LogUtil.i("LocationHelper.isValidLocation", "poor accuracy: " + location.getAccuracy()); + return false; + } + return true; + } + LogUtil.i("LocationHelper.isValidLocation", "no location"); + return false; + } + + @MainThread + void addLocationListener(LocationListener listener) { + Assert.isMainThread(); + listeners.add(listener); + } + + @MainThread + void removeLocationListener(LocationListener listener) { + Assert.isMainThread(); + listeners.remove(listener); + } + + @MainThread + void close() { + Assert.isMainThread(); + LogUtil.enterBlock("LocationHelper.close"); + listeners.clear(); + + if (locationHelperInternal != null) { + locationHelperInternal.close(); + } + } + + @MainThread + void onLocationChanged(Location location, boolean isConnected) { + Assert.isMainThread(); + LogUtil.i("LocationHelper.onLocationChanged", "location: " + location); + + for (LocationListener listener : listeners) { + listener.onLocationChanged(location); + } + } + + /** + * This class contains all the asynchronous callbacks. It only posts location changes back to the + * outer class on the main thread. + */ + private class LocationHelperInternal + implements ConnectionCallbacks, OnConnectionFailedListener, LocationListener { + + private final GoogleApiClient apiClient; + private final ConnectivityManager connectivityManager; + private final Handler mainThreadHandler = new Handler(); + + @MainThread + LocationHelperInternal(Context context) { + Assert.isMainThread(); + apiClient = + new GoogleApiClient.Builder(context) + .addApi(LocationServices.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + + LogUtil.i("LocationHelperInternal", "Connecting to location service..."); + apiClient.connect(); + + connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + + void close() { + if (apiClient.isConnected()) { + LogUtil.i("LocationHelperInternal", "disconnecting"); + LocationServices.FusedLocationApi.removeLocationUpdates(apiClient, this); + apiClient.disconnect(); + } + } + + @Override + public void onConnected(Bundle bundle) { + LogUtil.enterBlock("LocationHelperInternal.onConnected"); + LocationRequest locationRequest = + LocationRequest.create() + .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY) + .setInterval(MIN_UPDATE_INTERVAL_MS) + .setFastestInterval(MIN_UPDATE_INTERVAL_MS); + + LocationServices.FusedLocationApi.requestLocationUpdates(apiClient, locationRequest, this) + .setResultCallback( + new ResultCallback() { + @Override + public void onResult(Status status) { + if (status.getStatus().isSuccess()) { + onLocationChanged(LocationServices.FusedLocationApi.getLastLocation(apiClient)); + } + } + }); + } + + @Override + public void onConnectionSuspended(int i) { + // Do nothing. + } + + @Override + public void onConnectionFailed(ConnectionResult result) { + // Do nothing. + } + + @Override + public void onLocationChanged(Location location) { + // Post new location on main thread + mainThreadHandler.post( + new Runnable() { + @Override + public void run() { + LocationHelper.this.onLocationChanged(location, isConnected()); + } + }); + } + + /** @return Whether the phone is connected to data. */ + private boolean isConnected() { + if (connectivityManager == null) { + return false; + } + NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + return networkInfo != null && networkInfo.isConnectedOrConnecting(); + } + } +} diff --git a/java/com/android/incallui/calllocation/impl/LocationPresenter.java b/java/com/android/incallui/calllocation/impl/LocationPresenter.java new file mode 100644 index 000000000..a56fd3b3c --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/LocationPresenter.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.location.Location; +import android.os.AsyncTask; +import com.android.dialer.common.LogUtil; +import com.android.incallui.baseui.Presenter; +import com.android.incallui.baseui.Ui; +import com.google.android.gms.location.LocationListener; +import java.lang.ref.WeakReference; +import java.util.Objects; + +/** + * Presenter for the {@code LocationFragment}. + * + *

Performs lookup for the address and map image to show. + */ +public class LocationPresenter extends Presenter + implements LocationListener { + + private Location mLastLocation; + private AsyncTask mDownloadMapTask; + private AsyncTask mReverseGeocodeTask; + + LocationPresenter() {} + + @Override + public void onUiReady(LocationUi ui) { + LogUtil.i("LocationPresenter.onUiReady", ""); + super.onUiReady(ui); + updateLocation(mLastLocation, true); + } + + @Override + public void onUiUnready(LocationUi ui) { + LogUtil.i("LocationPresenter.onUiUnready", ""); + super.onUiUnready(ui); + + if (mDownloadMapTask != null) { + mDownloadMapTask.cancel(true); + } + if (mReverseGeocodeTask != null) { + mReverseGeocodeTask.cancel(true); + } + } + + @Override + public void onLocationChanged(Location location) { + LogUtil.i("LocationPresenter.onLocationChanged", ""); + updateLocation(location, false); + } + + private void updateLocation(Location location, boolean forceUpdate) { + LogUtil.i("LocationPresenter.updateLocation", "location: " + location); + if (forceUpdate || !Objects.equals(mLastLocation, location)) { + mLastLocation = location; + if (LocationHelper.isValidLocation(location)) { + LocationUi ui = getUi(); + mDownloadMapTask = new DownloadMapImageTask(new WeakReference<>(ui)).execute(location); + mReverseGeocodeTask = new ReverseGeocodeTask(new WeakReference<>(ui)).execute(location); + if (ui != null) { + ui.setLocation(location); + } else { + LogUtil.i("LocationPresenter.updateLocation", "no Ui"); + } + } + } + } + + /** UI interface */ + public interface LocationUi extends Ui { + + void setAddress(String address); + + void setMap(Drawable mapImage); + + void setLocation(Location location); + + Context getContext(); + } +} diff --git a/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java new file mode 100644 index 000000000..a57bdf613 --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/LocationUrlBuilder.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.location.Location; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import java.util.Locale; + +class LocationUrlBuilder { + + // Static Map API path constants. + private static final String HTTPS_SCHEME = "https"; + private static final String MAPS_API_DOMAIN = "maps.googleapis.com"; + private static final String MAPS_PATH = "maps"; + private static final String API_PATH = "api"; + private static final String STATIC_MAP_PATH = "staticmap"; + private static final String GEOCODE_PATH = "geocode"; + private static final String GEOCODE_OUTPUT_TYPE = "json"; + + // Static Map API parameter constants. + private static final String KEY_PARAM_KEY = "key"; + private static final String CENTER_PARAM_KEY = "center"; + private static final String ZOOM_PARAM_KEY = "zoom"; + private static final String SCALE_PARAM_KEY = "scale"; + private static final String SIZE_PARAM_KEY = "size"; + private static final String MARKERS_PARAM_KEY = "markers"; + + private static final String ZOOM_PARAM_VALUE = Integer.toString(16); + + private static final String LAT_LONG_DELIMITER = ","; + + private static final String MARKER_DELIMITER = "|"; + private static final String MARKER_STYLE_DELIMITER = ":"; + private static final String MARKER_STYLE_COLOR = "color"; + private static final String MARKER_STYLE_COLOR_RED = "red"; + + private static final String LAT_LNG_PARAM_KEY = "latlng"; + + private static final String ANDROID_API_KEY_VALUE = "AIzaSyAXdDnif6B7sBYxU8hzw9qAp3pRPVHs060"; + private static final String BROWSER_API_KEY_VALUE = "AIzaSyBfLlvWYndiQ3RFEHli65qGQH36QIxdyCI"; + + /** + * Generates the URL to a static map image for the given location. + * + *

This image has the following characteristics: + * + *

- It is centered at the given latitude and longitutde. - It is scaled according to the + * device's pixel density. - There is a red marker at the given latitude and longitude. + * + *

Source: https://developers.google.com/maps/documentation/staticmaps/ + * + * @param contxt The context. + * @param Location A location. + * @return The URL of a static map image url of the given location. + */ + public static String getStaticMapUrl(Context context, Location location) { + final Uri.Builder builder = new Uri.Builder(); + Resources res = context.getResources(); + String size = + res.getDimensionPixelSize(R.dimen.location_map_width) + + "x" + + res.getDimensionPixelSize(R.dimen.location_map_height); + + builder + .scheme(HTTPS_SCHEME) + .authority(MAPS_API_DOMAIN) + .appendPath(MAPS_PATH) + .appendPath(API_PATH) + .appendPath(STATIC_MAP_PATH) + .appendQueryParameter(CENTER_PARAM_KEY, getFormattedLatLng(location)) + .appendQueryParameter(ZOOM_PARAM_KEY, ZOOM_PARAM_VALUE) + .appendQueryParameter(SIZE_PARAM_KEY, size) + .appendQueryParameter(SCALE_PARAM_KEY, Float.toString(res.getDisplayMetrics().density)) + .appendQueryParameter(MARKERS_PARAM_KEY, getMarkerUrlParamValue(location)) + .appendQueryParameter(KEY_PARAM_KEY, ANDROID_API_KEY_VALUE); + + return builder.build().toString(); + } + + /** + * Generates the URL for a request to reverse geocode the given location. + * + *

Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding + * + * @param Location A location. + */ + public static String getReverseGeocodeUrl(Location location) { + final Uri.Builder builder = new Uri.Builder(); + + builder + .scheme(HTTPS_SCHEME) + .authority(MAPS_API_DOMAIN) + .appendPath(MAPS_PATH) + .appendPath(API_PATH) + .appendPath(GEOCODE_PATH) + .appendPath(GEOCODE_OUTPUT_TYPE) + .appendQueryParameter(LAT_LNG_PARAM_KEY, getFormattedLatLng(location)) + .appendQueryParameter(KEY_PARAM_KEY, BROWSER_API_KEY_VALUE); + + return builder.build().toString(); + } + + public static Intent getShowMapIntent( + Location location, @Nullable CharSequence addressLine1, @Nullable CharSequence addressLine2) { + + String latLong = getFormattedLatLng(location); + String url = String.format(Locale.US, "geo: %s?q=%s", latLong, latLong); + + // Add a map label + if (addressLine1 != null) { + if (addressLine2 != null) { + url += + String.format(Locale.US, "(%s, %s)", addressLine1.toString(), addressLine2.toString()); + } else { + url += String.format(Locale.US, "(%s)", addressLine1.toString()); + } + } else { + // TODO: i18n + url += + String.format( + Locale.US, + "(Latitude: %f, Longitude: %f)", + location.getLatitude(), + location.getLongitude()); + } + + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.setPackage("com.google.android.apps.maps"); + return intent; + } + + /** + * Returns a comma-separated latitude and longitude pair, formatted for use as a URL parameter + * value. + * + * @param location A location. + * @return The comma-separated latitude and longitude pair of that location. + */ + @VisibleForTesting + static String getFormattedLatLng(Location location) { + return location.getLatitude() + LAT_LONG_DELIMITER + location.getLongitude(); + } + + /** + * Returns the URL parameter value for the marker, specifying its style and position. + * + * @param location A location. + * @return The URL parameter value for the marker. + */ + @VisibleForTesting + static String getMarkerUrlParamValue(Location location) { + return MARKER_STYLE_COLOR + + MARKER_STYLE_DELIMITER + + MARKER_STYLE_COLOR_RED + + MARKER_DELIMITER + + getFormattedLatLng(location); + } +} diff --git a/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java new file mode 100644 index 000000000..eb5957b05 --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/ReverseGeocodeTask.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +import android.location.Location; +import android.net.TrafficStats; +import android.os.AsyncTask; +import com.android.dialer.common.LogUtil; +import com.android.incallui.calllocation.impl.LocationPresenter.LocationUi; +import java.lang.ref.WeakReference; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +class ReverseGeocodeTask extends AsyncTask { + + // Below are the JSON keys for the reverse geocode response. + // Source: https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding + private static final String JSON_KEY_RESULTS = "results"; + private static final String JSON_KEY_ADDRESS = "formatted_address"; + private static final String JSON_KEY_ADDRESS_COMPONENTS = "address_components"; + private static final String JSON_KEY_PREMISE = "premise"; + private static final String JSON_KEY_TYPES = "types"; + private static final String JSON_KEY_LONG_NAME = "long_name"; + private static final String JSON_KEY_SHORT_NAME = "short_name"; + + private WeakReference mUiReference; + + public ReverseGeocodeTask(WeakReference uiReference) { + mUiReference = uiReference; + } + + @Override + protected String doInBackground(Location... locations) { + LocationUi ui = mUiReference.get(); + if (ui == null) { + return null; + } + if (locations == null || locations.length == 0) { + LogUtil.e("ReverseGeocodeTask.onLocationChanged", "No location provided"); + return null; + } + + try { + String address = null; + String url = LocationUrlBuilder.getReverseGeocodeUrl(locations[0]); + + TrafficStats.setThreadStatsTag(TrafficStatsTags.REVERSE_GEOCODE_TAG); + String jsonResponse = HttpFetcher.getRequestAsString(ui.getContext(), url); + + // Parse the JSON response for the formatted address of the first result. + JSONObject responseObject = new JSONObject(jsonResponse); + if (responseObject != null) { + JSONArray results = responseObject.optJSONArray(JSON_KEY_RESULTS); + if (results != null && results.length() > 0) { + JSONObject topResult = results.optJSONObject(0); + if (topResult != null) { + address = topResult.getString(JSON_KEY_ADDRESS); + + // Strip off the Premise component from the address, if present. + JSONArray components = topResult.optJSONArray(JSON_KEY_ADDRESS_COMPONENTS); + if (components != null) { + boolean stripped = false; + for (int i = 0; !stripped && i < components.length(); i++) { + JSONObject component = components.optJSONObject(i); + JSONArray types = component.optJSONArray(JSON_KEY_TYPES); + if (types != null) { + for (int j = 0; !stripped && j < types.length(); j++) { + if (JSON_KEY_PREMISE.equals(types.getString(j))) { + String premise = null; + if (component.has(JSON_KEY_SHORT_NAME) + && address.startsWith(component.getString(JSON_KEY_SHORT_NAME))) { + premise = component.getString(JSON_KEY_SHORT_NAME); + } else if (component.has(JSON_KEY_LONG_NAME) + && address.startsWith(component.getString(JSON_KEY_LONG_NAME))) { + premise = component.getString(JSON_KEY_SHORT_NAME); + } + if (premise != null) { + int index = address.indexOf(',', premise.length()); + if (index > 0 && index < address.length()) { + address = address.substring(index + 1).trim(); + } + stripped = true; + break; + } + } + } + } + } + } + + // Strip off the country, if its USA. Note: unfortunately the country in the formatted + // address field doesn't match the country in the address component fields (USA != US) + // so we can't easily strip off the country for all cases, thus this hack. + if (address.endsWith(", USA")) { + address = address.substring(0, address.length() - 5); + } + } + } + } + + return address; + } catch (AuthException ex) { + LogUtil.e("ReverseGeocodeTask.onLocationChanged", "AuthException", ex); + return null; + } catch (JSONException ex) { + LogUtil.e("ReverseGeocodeTask.onLocationChanged", "JSONException", ex); + return null; + } catch (Exception ex) { + LogUtil.e("ReverseGeocodeTask.onLocationChanged", "Exception!!!", ex); + return null; + } finally { + TrafficStats.clearThreadStatsTag(); + } + } + + @Override + protected void onPostExecute(String address) { + LocationUi ui = mUiReference.get(); + if (ui == null) { + return; + } + + try { + ui.setAddress(address); + } catch (Exception ex) { + LogUtil.e("ReverseGeocodeTask.onPostExecute", "Exception!!!", ex); + } + } +} diff --git a/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java new file mode 100644 index 000000000..02cc2e083 --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/TrafficStatsTags.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 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.incallui.calllocation.impl; + +/** Constants used for logging */ +public class TrafficStatsTags { + + /** + * Must be greater than {@link com.android.contacts.common.util.TrafficStatsTags#TAG_MAX}, to + * respect the namespace of the tags in ContactsCommon. + */ + public static final int DOWNLOAD_LOCATION_MAP_TAG = 0xd000; + + public static final int REVERSE_GEOCODE_TAG = 0xd001; +} diff --git a/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml new file mode 100644 index 000000000..a6bd07542 --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/res/layout/location_fragment.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/incallui/calllocation/impl/res/values/dimens.xml b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml new file mode 100644 index 000000000..1f4181607 --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + + 92dp + 92dp + diff --git a/java/com/android/incallui/calllocation/impl/res/values/strings.xml b/java/com/android/incallui/calllocation/impl/res/values/strings.xml new file mode 100644 index 000000000..ef7c1624c --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/res/values/strings.xml @@ -0,0 +1,15 @@ + + + + + Emergency Location Map + + + You are here + + %f, %f + + + Finding your location + + diff --git a/java/com/android/incallui/calllocation/impl/res/values/styles.xml b/java/com/android/incallui/calllocation/impl/res/values/styles.xml new file mode 100644 index 000000000..866a4edb6 --- /dev/null +++ b/java/com/android/incallui/calllocation/impl/res/values/styles.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + -- cgit v1.2.3