diff options
Diffstat (limited to 'java')
139 files changed, 3315 insertions, 501 deletions
diff --git a/java/com/android/contacts/common/MoreContactUtils.java b/java/com/android/contacts/common/MoreContactUtils.java index 26241b34f..9749cabfd 100644 --- a/java/com/android/contacts/common/MoreContactUtils.java +++ b/java/com/android/contacts/common/MoreContactUtils.java @@ -65,7 +65,7 @@ public class MoreContactUtils { // TODO: Move this to PhoneDataItem.shouldCollapse override private static boolean shouldCollapsePhoneNumbers(String number1, String number2) { - // Work around to address b/20724444. We want to distinguish between #555, *555 and 555. + // Work around to address a bug. We want to distinguish between #555, *555 and 555. // This makes no attempt to distinguish between 555 and 55*5, since 55*5 is an improbable // number. PhoneNumberUtil already distinguishes between 555 and 55#5. if (number1.contains("#") != number2.contains("#") @@ -119,7 +119,7 @@ public class MoreContactUtils { // +14155551212 // 4155551212 // - // From b/7519057, case 2 needs to be equal. But also that bug, case 3 + // From a bug, case 2 needs to be equal. But also that bug, case 3 // below should not be equal. // // case 3) diff --git a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java index 172fb11e3..bb0d3c625 100644 --- a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java +++ b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java @@ -24,7 +24,7 @@ import java.lang.reflect.Field; public class TelecomManagerCompat { // TODO(mdooley): remove once this is available in android.telecom.Call - // b/33779976 + // a bug public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS"; diff --git a/java/com/android/contacts/common/list/AutoScrollListView.java b/java/com/android/contacts/common/list/AutoScrollListView.java index 601abf528..8d6455f7f 100644 --- a/java/com/android/contacts/common/list/AutoScrollListView.java +++ b/java/com/android/contacts/common/list/AutoScrollListView.java @@ -116,7 +116,7 @@ public class AutoScrollListView extends ListView { protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); - // Workaround for b/31160338 and b/32778636. + // Workaround for a bug and a bug. if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N || android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) { layoutChildren(); diff --git a/java/com/android/contacts/common/list/ContactEntryListFragment.java b/java/com/android/contacts/common/list/ContactEntryListFragment.java index 94551a8c8..c807ea815 100644 --- a/java/com/android/contacts/common/list/ContactEntryListFragment.java +++ b/java/com/android/contacts/common/list/ContactEntryListFragment.java @@ -825,7 +825,7 @@ public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter @Override public void onResume() { super.onResume(); - // Restore the selection of the list view. See b/19982820. + // Restore the selection of the list view. See a bug. // This has to be done manually because if the list view has its emptyView set, // the scrolling state will be reset when clearPartitions() is called on the adapter. mListView.setSelectionFromTop(mListViewTopIndex, mListViewTopOffset); diff --git a/java/com/android/contacts/common/res/drawable/back_arrow.xml b/java/com/android/contacts/common/res/drawable/back_arrow.xml new file mode 100644 index 000000000..34fa3d7fc --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/back_arrow.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:autoMirrored="true" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/> +</vector> diff --git a/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml index 2aa97722d..9e9be95e3 100644 --- a/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml +++ b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> -<!-- layoutDirection set to ltr as a workaround to a framework bug (b/22010411) causing view with +<!-- layoutDirection set to ltr as a workaround to a framework bug (a bug) causing view with layout_centerInParent inside a RelativeLayout to expand to screen width when RTL is active --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" diff --git a/java/com/android/contacts/common/res/layout/search_bar_expanded.xml b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml index f0179ad30..ccea3f76f 100644 --- a/java/com/android/contacts/common/res/layout/search_bar_expanded.xml +++ b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml @@ -28,7 +28,7 @@ android:layout_centerVertical="true" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="@string/action_menu_back_from_search" - android:src="@drawable/quantum_ic_arrow_back_vd_theme_24" + android:src="@drawable/back_arrow" android:tint="@color/contactscommon_actionbar_background_color"/> <EditText diff --git a/java/com/android/dialer/about/res/raw/third_party_license_metadata b/java/com/android/dialer/about/res/raw/third_party_license_metadata index b20ef89d7..49c22731f 100755 --- a/java/com/android/dialer/about/res/raw/third_party_license_metadata +++ b/java/com/android/dialer/about/res/raw/third_party_license_metadata @@ -30,13 +30,13 @@ 321603:11358 OpenCensus 332972:11358 Volley 344341:10695 bubble -355050:10402 gRPC Java -365471:10173 libphonenumber -375663:10699 shortcutbadger -386378:16013 Android SDK -402410:1096 Animal Sniffer -403516:4771 Glide -408299:1602 JSR 305 -409919:12847 jibercsclient -422777:18982 mime4j -441776:12847 rcsclientlib +355050:11358 gRPC Java +366427:10173 libphonenumber +376619:10699 shortcutbadger +387334:16013 Android SDK +403366:1096 Animal Sniffer +404472:4771 Glide +409255:1602 JSR 305 +410875:12847 jibercsclient +423733:18982 mime4j +442732:12847 rcsclientlib diff --git a/java/com/android/dialer/about/res/raw/third_party_licenses b/java/com/android/dialer/about/res/raw/third_party_licenses index 443c510a0..64f7dc780 100755 --- a/java/com/android/dialer/about/res/raw/third_party_licenses +++ b/java/com/android/dialer/about/res/raw/third_party_licenses @@ -6424,13 +6424,14 @@ gRPC Java: Apache License Version 2.0, January 2004 - https://www.apache.org/licenses/ + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. @@ -6509,6 +6510,7 @@ gRPC Java: granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: @@ -6526,6 +6528,7 @@ gRPC Java: the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one @@ -6543,7 +6546,9 @@ gRPC Java: You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, @@ -6594,13 +6599,24 @@ gRPC Java: END OF TERMS AND CONDITIONS - Copyright 2015-2017 gRPC authors. + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] 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 - https://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java index b96b127e2..5f4b62c22 100644 --- a/java/com/android/dialer/app/DialtactsActivity.java +++ b/java/com/android/dialer/app/DialtactsActivity.java @@ -1163,7 +1163,7 @@ public class DialtactsActivity extends TransactionSafeActivity LogUtil.i("DialtactsActivity.enterSearchUi", "smart dial: %b", smartDialSearch); if (mStateSaved || getFragmentManager().isDestroyed()) { // Weird race condition where fragment is doing work after the activity is destroyed - // due to talkback being on (b/10209937). Just return since we can't do any + // due to talkback being on (a bug). Just return since we can't do any // constructive here. LogUtil.i( "DialtactsActivity.enterSearchUi", diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java index 016bce322..589029827 100644 --- a/java/com/android/dialer/app/calllog/CallLogAdapter.java +++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java @@ -361,7 +361,7 @@ public class CallLogAdapter extends GroupingListAdapter "mExpandCollapseListener.onClick", "%s is temporarily unavailable, requesting capabilities", LogUtil.sanitizePhoneNumber(viewHolder.number)); - // Refresh the capabilities when temporarily unavailable, see go/ec-temp-unavailable. + // Refresh the capabilities when temporarily unavailable. // Similarly to when we request capabilities the first time, the 'Share and call' button // won't pop in with the new capabilities. Instead the row needs to be collapsed and // expanded again. diff --git a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java index 78ec7a695..c0d30f53a 100644 --- a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java +++ b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java @@ -103,7 +103,7 @@ public class CallLogAsyncTaskUtil { ContentValues values = new ContentValues(); values.put(Voicemails.DELETED, "1"); context.getContentResolver().update(voicemailUri, values, null, null); - // TODO(b/35440541): check which source package is changed. Don't need + // TODO(a bug): check which source package is changed. Don't need // to upload changes on foreign voicemails, they will get a PROVIDER_CHANGED uploadVoicemailLocalChangesToServer(context); } diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java index d5dfb06dc..fa73bb251 100644 --- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java +++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java @@ -536,8 +536,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder CallIntentBuilder.increaseLightbringerCallButtonAppearInCollapsedCallLogItemCount(); primaryActionButtonView.setTag(IntentProvider.getDuoVideoIntentProvider(number)); } else { - primaryActionButtonView.setTag( - IntentProvider.getReturnVideoCallIntentProvider(number, accountHandle)); + primaryActionButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number)); } primaryActionButtonView.setContentDescription( TextUtils.expandTemplate( @@ -793,7 +792,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder if (show) { if (!isLoaded) { - // b/31268128 for some unidentified reason showActions() can be called before the item is + // a bug for some unidentified reason showActions() can be called before the item is // loaded, causing NPE on uninitialized fields. Just log and return here, showActions() will // be called again once the item is loaded. LogUtil.e( diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java index fff68d4c4..8e09cf8df 100644 --- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java +++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java @@ -470,7 +470,7 @@ public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { private PendingIntent createCallLogPendingIntent(@Nullable Uri callUri) { Intent contentIntent = DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_HISTORY); - // TODO (b/35486204): scroll to call + // TODO (a bug): scroll to call contentIntent.setData(callUri); return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT); } diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java b/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java index 4fc956fa8..f9cb4bf6c 100644 --- a/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java +++ b/java/com/android/dialer/app/calllog/VisualVoicemailNotifier.java @@ -274,7 +274,7 @@ final class VisualVoicemailNotifier { @NonNull Context context, @Nullable NewCall voicemail) { Intent intent = DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL); - // TODO (b/35486204): scroll to this voicemail + // TODO (a bug): scroll to this voicemail if (voicemail != null) { intent.setData(voicemail.voicemailUri); } diff --git a/java/com/android/dialer/app/list/RemoveView.java b/java/com/android/dialer/app/list/RemoveView.java index 1d566c5a0..244f2da24 100644 --- a/java/com/android/dialer/app/list/RemoveView.java +++ b/java/com/android/dialer/app/list/RemoveView.java @@ -68,7 +68,7 @@ public class RemoveView extends FrameLayout { switch (action) { case DragEvent.ACTION_DRAG_ENTERED: // TODO: This is temporary solution and should be removed once accessibility for - // drag and drop is supported by framework(b/26871588). + // drag and drop is supported by framework(a bug). sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT); setAppearanceHighlighted(); break; diff --git a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml index 0729d7293..9e08f5f5d 100644 --- a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml +++ b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml @@ -27,6 +27,11 @@ android:name="com.android.dialer.app.settings.DialerSettingsActivity" android:parentActivityName="com.android.dialer.app.DialtactsActivity" android:theme="@style/SettingsStyle"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT"/> + <data android:scheme="header"/> + </intent-filter> </activity> <!-- The entrance point for Phone UI. @@ -41,7 +46,7 @@ android:resizeableActivity="true" android:theme="@style/DialtactsActivityTheme" android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"> - <!-- LINT.IfChange --> + <intent-filter> <action android:name="android.intent.action.DIAL"/> @@ -107,7 +112,7 @@ <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.TAB"/> </intent-filter> - <!-- LINT.ThenChange(//depot/google3/third_party/java_src/android_app/dialer/java/com/android/dialer/dialtacts/impl/AndroidManifest.xml) --> + <meta-data android:name="com.android.keyguard.layout" diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item.xml b/java/com/android/dialer/app/res/layout/call_log_list_item.xml index 4382008d4..e0f9e63b4 100644 --- a/java/com/android/dialer/app/res/layout/call_log_list_item.xml +++ b/java/com/android/dialer/app/res/layout/call_log_list_item.xml @@ -162,6 +162,8 @@ android:layout_height="wrap_content" android:textColor="@color/call_log_voicemail_transcript_color" android:textSize="@dimen/call_log_voicemail_transcription_text_size" + android:focusable="true" + android:nextFocusDown="@+id/voicemail_transcription_branding" android:textIsSelectable="true"/> <TextView @@ -170,6 +172,8 @@ android:layout_height="wrap_content" android:textColor="@color/call_log_voicemail_transcript_branding_color" android:textSize="@dimen/call_log_voicemail_transcription_text_size" + android:focusable="true" + android:nextFocusUp="@id/voicemail_transcription" android:paddingTop="2dp"/> </LinearLayout> diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java index 706f0985a..89c69ca45 100644 --- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java +++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java @@ -18,6 +18,7 @@ package com.android.dialer.app.settings; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -51,11 +52,28 @@ public class DialerSettingsActivity extends AppCompatPreferenceActivity { protected SharedPreferences mPreferences; private boolean migrationStatusOnBuildHeaders; + private List<Header> headers; @Override protected void onCreate(Bundle savedInstanceState) { + LogUtil.enterBlock("DialerSettingsActivity.onCreate"); super.onCreate(savedInstanceState); mPreferences = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext()); + + Intent intent = getIntent(); + Uri data = intent.getData(); + if (data != null) { + String headerToOpen = data.getSchemeSpecificPart(); + if (headerToOpen != null && headers != null) { + for (Header header : headers) { + if (headerToOpen.equals(header.fragment)) { + LogUtil.i("DialerSettingsActivity.onCreate", "switching to header: " + headerToOpen); + switchToHeader(header); + break; + } + } + } + } } @Override @@ -72,6 +90,9 @@ public class DialerSettingsActivity extends AppCompatPreferenceActivity { @Override public void onBuildHeaders(List<Header> target) { + // Keep a reference to the list of headers (since PreferenceActivity.getHeaders() is @Hide) + headers = target; + if (showDisplayOptions()) { Header displayOptionsHeader = new Header(); displayOptionsHeader.titleRes = R.string.display_options_title; diff --git a/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java b/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java index bc6ffb5a7..39ef3fab8 100644 --- a/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java +++ b/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java @@ -92,7 +92,7 @@ public class VoicemailErrorManager implements CallLogQueryHandler.Listener, Voic } } alertItem.updateStatus(statuses, this); - // TODO(twyen): b/30668323 support error from multiple sources. + // TODO(twyen): a bug support error from multiple sources. return; } diff --git a/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml b/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml index 65d043034..bb6c55f5c 100644 --- a/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml +++ b/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml @@ -1,5 +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. +--> + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.dialer.app.voicemail.error"> <uses-permission android:name="android.permission.CALL_PHONE"/> + + <application> + <receiver android:name=".PackageReplacedReceiver" android:exported="false"> + <intent-filter> + <action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> + </intent-filter> + </receiver> + </application> + </manifest> diff --git a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java index 79e038332..9c8b1469e 100644 --- a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java +++ b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java @@ -129,7 +129,7 @@ public class OmtpVoicemailMessageCreator { VoicemailErrorMessage.createRetryAction(context, status)); } - // This should be an assertion error, but there's a bug in NYC-DR (b/31069259) that will + // This should be an assertion error, but there's a bug in NYC-DR (a bug) that will // sometimes give status mixed from multiple SIMs. There's no meaningful message to be displayed // from it, so just suppress the message. LogUtil.e("OmtpVoicemailMessageCreator.create", "Unhandled status: " + status); diff --git a/java/com/android/dialer/app/voicemail/error/PackageReplacedReceiver.java b/java/com/android/dialer/app/voicemail/error/PackageReplacedReceiver.java new file mode 100644 index 000000000..64d72b18f --- /dev/null +++ b/java/com/android/dialer/app/voicemail/error/PackageReplacedReceiver.java @@ -0,0 +1,109 @@ +/* + * 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.dialer.app.voicemail.error; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.preference.PreferenceManager; +import android.provider.CallLog.Calls; +import android.provider.VoicemailContract.Voicemails; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutor.Worker; +import com.android.dialer.common.concurrent.DialerExecutorComponent; + +/** Receives MY_PACKAGE_REPLACED to check for legacy voicemail users. */ +public class PackageReplacedReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + LogUtil.enterBlock("PackageReplacedReceiver.onReceive"); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if (!prefs.contains(VoicemailTosMessageCreator.PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY)) { + setVoicemailFeatureVersionAsync(context); + } + } + + private void setVoicemailFeatureVersionAsync(Context context) { + LogUtil.enterBlock("PackageReplacedReceiver.setVoicemailFeatureVersionAsync"); + + // Check if user is already using voicemail (ie do they have any voicemails), and set the + // acknowledged feature value accordingly. + PendingResult pendingResult = goAsync(); + DialerExecutorComponent.get(context) + .dialerExecutorFactory() + .createNonUiTaskBuilder(new ExistingVoicemailCheck(context)) + .onSuccess( + output -> { + LogUtil.i("PackageReplacedReceiver.setVoicemailFeatureVersionAsync", "success"); + pendingResult.finish(); + }) + .onFailure( + throwable -> { + LogUtil.i("PackageReplacedReceiver.setVoicemailFeatureVersionAsync", "failure"); + pendingResult.finish(); + }) + .build() + .executeParallel(null); + } + + private static class ExistingVoicemailCheck implements Worker<Void, Void> { + private static final String[] PROJECTION = new String[] {Voicemails._ID}; + + private final Context context; + + ExistingVoicemailCheck(Context context) { + this.context = context; + } + + @TargetApi(android.os.Build.VERSION_CODES.M) // used for try with resources + @Override + public Void doInBackground(Void arg) throws Throwable { + LogUtil.i("PackageReplacedReceiver.ExistingVoicemailCheck.doInBackground", ""); + + // Check the database for existing voicemails. + boolean hasVoicemails = false; + Uri uri = Voicemails.buildSourceUri(context.getPackageName()); + String whereClause = Calls.TYPE + " = " + Calls.VOICEMAIL_TYPE; + try (Cursor cursor = + context.getContentResolver().query(uri, PROJECTION, whereClause, null, null)) { + if (cursor == null) { + LogUtil.e( + "PackageReplacedReceiver.ExistingVoicemailCheck.doInBackground", + "failed to check for existing voicemails"); + } else if (cursor.moveToNext()) { + hasVoicemails = true; + } + } + + LogUtil.i( + "PackageReplacedReceiver.ExistingVoicemailCheck.doInBackground", + "has voicemails: " + hasVoicemails); + int version = hasVoicemails ? VoicemailTosMessageCreator.LEGACY_VOICEMAIL_FEATURE_VERSION : 0; + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putInt(VoicemailTosMessageCreator.PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY, version) + .apply(); + return null; + } + } +} diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java index bbba7ac76..b357f9a4d 100644 --- a/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java +++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java @@ -82,7 +82,7 @@ public class VoicemailStatusCorruptionHandler { // If visual voicemail is enabled, the CONFIGURATION_STATE should be either OK, PIN_NOT_SET, // or other failure code. CONFIGURATION_STATE_NOT_CONFIGURED means that the client has been - // shut down improperly (b/32371710). The client should be reset or the VVM tab will be + // shut down improperly (a bug). The client should be reset or the VVM tab will be // missing. if (Status.CONFIGURATION_STATE_NOT_CONFIGURED == status.configurationState && visualVoicemailEnabled) { diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java index 63ebd1959..96850ad02 100644 --- a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java +++ b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessageCreator.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Build; import android.preference.PreferenceManager; import android.support.annotation.Nullable; @@ -40,6 +41,7 @@ import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action; import com.android.dialer.common.LogUtil; import com.android.dialer.compat.telephony.TelephonyManagerCompat; import com.android.dialer.configprovider.ConfigProviderBindings; +import com.android.dialer.constants.Constants; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; import com.android.voicemail.VisualVoicemailTypeExtensions; @@ -52,14 +54,21 @@ import java.util.Locale; * terms of service for Verizon and for other carriers. */ public class VoicemailTosMessageCreator { - // Flag to check which version of the Verizon ToS that the user has accepted. - public static final String VVM3_TOS_VERSION_ACCEPTED_KEY = "vvm3_tos_version_accepted"; + // Preference key to check which version of the Verizon ToS that the user has accepted. + static final String PREF_VVM3_TOS_VERSION_ACCEPTED_KEY = "vvm3_tos_version_accepted"; - // Flag to check which version of the Google Dialer ToS that the user has accepted. - public static final String DIALER_TOS_VERSION_ACCEPTED_KEY = "dialer_tos_version_accepted"; + // Preference key to check which version of the Google Dialer ToS that the user has accepted. + static final String PREF_DIALER_TOS_VERSION_ACCEPTED_KEY = "dialer_tos_version_accepted"; - public static final int CURRENT_VVM3_TOS_VERSION = 2; - public static final int CURRENT_DIALER_TOS_VERSION = 1; + // Preference key to check which feature version the user has acknowledged + static final String PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY = + "dialer_feature_version_acknowledged"; + + static final int CURRENT_VVM3_TOS_VERSION = 2; + static final int CURRENT_DIALER_TOS_VERSION = 1; + static final int LEGACY_VOICEMAIL_FEATURE_VERSION = 1; // original visual voicemail + static final int TRANSCRIPTION_VOICEMAIL_FEATURE_VERSION = 2; // adds voicemail transcription + static final int CURRENT_VOICEMAIL_FEATURE_VERSION = TRANSCRIPTION_VOICEMAIL_FEATURE_VERSION; private static final String ISO639_SPANISH = "es"; @@ -81,25 +90,28 @@ public class VoicemailTosMessageCreator { @Nullable VoicemailErrorMessage maybeCreateTosMessage() { - if (hasAcceptedTos()) { + if (!canShowTos()) { return null; - } - - if (!shouldShowTos()) { + } else if (shouldShowTos()) { + logTosCreatedImpression(); + return getTosMessage(); + } else if (shouldShowPromo()) { + return getPromoMessage(); + } else { return null; } + } - logTosCreatedImpression(); - + private VoicemailErrorMessage getTosMessage() { return new VoicemailTosMessage( - getTosTitle(), - getTosMessage(), + getNewUserTosTitle(), + getNewUserTosMessageText(), new Action( getDeclineText(), new OnClickListener() { @Override public void onClick(View v) { - LogUtil.i("VoicemailTosMessageCreator.maybeShowTosMessage", "decline clicked"); + LogUtil.i("VoicemailTosMessageCreator.getTosMessage", "decline clicked"); PhoneAccountHandle handle = new PhoneAccountHandle( ComponentName.unflattenFromString(status.phoneAccountComponentName), @@ -113,8 +125,10 @@ public class VoicemailTosMessageCreator { new OnClickListener() { @Override public void onClick(View v) { - LogUtil.i("VoicemailTosMessageCreator.maybeShowTosMessage", "accept clicked"); + LogUtil.i("VoicemailTosMessageCreator.getTosMessage", "accept clicked"); recordTosAcceptance(); + // Accepting the TOS also acknowledges the latest features + recordFeatureAcknowledgement(); logTosAcceptedImpression(); statusReader.refresh(); } @@ -124,15 +138,65 @@ public class VoicemailTosMessageCreator { .setImageResourceId(R.drawable.voicemail_tos_image); } - private boolean shouldShowTos() { + private VoicemailErrorMessage getPromoMessage() { + return new VoicemailTosMessage( + getExistingUserTosTitle(), + getExistingUserTosMessageText(), + new Action( + context.getString(R.string.dialer_terms_and_conditions_existing_user_setings), + new OnClickListener() { + @Override + public void onClick(View v) { + LogUtil.i("VoicemailTosMessageCreator.getPromoMessage", "open settings"); + Intent intent = + new Intent(Intent.ACTION_VIEW) + .setComponent( + new ComponentName(context, Constants.get().getSettingsActivity())) + .setData( + Uri.fromParts( + "header", + VoicemailComponent.get(context) + .getVoicemailClient() + .getSettingsFragment(), + null)); + context.startActivity(intent); + } + }), + new Action( + context.getString(R.string.dialer_terms_and_conditions_existing_user_ack), + new OnClickListener() { + @Override + public void onClick(View v) { + LogUtil.i("VoicemailTosMessageCreator.getPromoMessage", "acknowledge clicked"); + // Feature acknowledgement also means accepting TOS + recordTosAcceptance(); + recordFeatureAcknowledgement(); + statusReader.refresh(); + } + }, + true /* raised */)) + .setModal(true) + .setImageResourceId(R.drawable.voicemail_tos_image); + } + + private boolean canShowTos() { if (!isValidVoicemailType(status.type)) { - LogUtil.i("VoicemailTosMessageCreator.shouldShowTos", "unsupported type: " + status.type); + LogUtil.i("VoicemailTosMessageCreator.canShowTos", "unsupported type: " + status.type); return false; } if (status.getPhoneAccountHandle() == null || status.getPhoneAccountHandle().getComponentName() == null) { - LogUtil.i("VoicemailTosMessageCreator.shouldShowTos", "invalid phone account"); + LogUtil.i("VoicemailTosMessageCreator.canShowTos", "invalid phone account"); + return false; + } + + return true; + } + + private boolean shouldShowTos() { + if (hasAcceptedTos()) { + LogUtil.i("VoicemailTosMessageCreator.shouldShowTos", "already accepted TOS"); return false; } @@ -141,7 +205,7 @@ public class VoicemailTosMessageCreator { return true; } - if (isVoicemailTranscriptionEnabled()) { + if (isVoicemailTranscriptionEnabled() && !isLegacyVoicemailUser()) { LogUtil.i( "VoicemailTosMessageCreator.shouldShowTos", "showing TOS for Google transcription users"); return true; @@ -150,6 +214,23 @@ public class VoicemailTosMessageCreator { return false; } + private boolean shouldShowPromo() { + if (hasAcknowledgedFeatures()) { + LogUtil.i( + "VoicemailTosMessageCreator.shouldShowPromo", "already acknowledeged latest features"); + return false; + } + + if (isVoicemailTranscriptionEnabled()) { + LogUtil.i( + "VoicemailTosMessageCreator.shouldShowPromo", + "showing promo for Google transcription users"); + return true; + } + + return false; + } + private static boolean isValidVoicemailType(String type) { if (type == null) { return false; @@ -181,6 +262,7 @@ public class VoicemailTosMessageCreator { "showing decline ToS dialog, status=" + status); final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class); AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.terms_and_conditions_decline_dialog_title); builder.setMessage(getTosDeclinedDialogMessageId()); builder.setPositiveButton( getTosDeclinedDialogDowngradeId(), @@ -249,24 +331,49 @@ public class VoicemailTosMessageCreator { private boolean hasAcceptedTos() { if (isVvm3()) { - return preferences.getInt(VVM3_TOS_VERSION_ACCEPTED_KEY, 0) >= CURRENT_VVM3_TOS_VERSION; + return preferences.getInt(PREF_VVM3_TOS_VERSION_ACCEPTED_KEY, 0) >= CURRENT_VVM3_TOS_VERSION; } else { - return preferences.getInt(DIALER_TOS_VERSION_ACCEPTED_KEY, 0) >= CURRENT_DIALER_TOS_VERSION; + return preferences.getInt(PREF_DIALER_TOS_VERSION_ACCEPTED_KEY, 0) + >= CURRENT_DIALER_TOS_VERSION; } } private void recordTosAcceptance() { if (isVvm3()) { - preferences.edit().putInt(VVM3_TOS_VERSION_ACCEPTED_KEY, CURRENT_VVM3_TOS_VERSION).apply(); + preferences + .edit() + .putInt(PREF_VVM3_TOS_VERSION_ACCEPTED_KEY, CURRENT_VVM3_TOS_VERSION) + .apply(); } else { preferences .edit() - .putInt(DIALER_TOS_VERSION_ACCEPTED_KEY, CURRENT_DIALER_TOS_VERSION) + .putInt(PREF_DIALER_TOS_VERSION_ACCEPTED_KEY, CURRENT_DIALER_TOS_VERSION) .apply(); } VoicemailComponent.get(context).getVoicemailClient().onTosAccepted(context); } + private boolean hasAcknowledgedFeatures() { + if (isVvm3()) { + return true; + } + + return preferences.getInt(PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY, 0) + >= CURRENT_VOICEMAIL_FEATURE_VERSION; + } + + private void recordFeatureAcknowledgement() { + preferences + .edit() + .putInt(PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY, CURRENT_VOICEMAIL_FEATURE_VERSION) + .apply(); + } + + private boolean isLegacyVoicemailUser() { + return preferences.getInt(PREF_DIALER_FEATURE_VERSION_ACKNOWLEDGED_KEY, 0) + == LEGACY_VOICEMAIL_FEATURE_VERSION; + } + private void logTosCreatedImpression() { if (isVvm3()) { Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_V2_CREATED); @@ -299,17 +406,30 @@ public class VoicemailTosMessageCreator { : context.getString(R.string.verizon_terms_and_conditions_1_1_english, policyUrl); } - private CharSequence getDialerTos() { + private CharSequence getVvmDialerTos() { if (!isVoicemailTranscriptionEnabled()) { return ""; } - if (isVvm3()) { - return context.getString(R.string.dialer_terms_and_conditions_for_verizon_1_0); - } else { - String learnMoreText = context.getString(R.string.dialer_terms_and_conditions_learn_more); - return context.getString(R.string.dialer_terms_and_conditions_1_0, learnMoreText); + return context.getString(R.string.dialer_terms_and_conditions_for_verizon_1_0); + } + + private CharSequence getNewUserDialerTos() { + if (!isVoicemailTranscriptionEnabled()) { + return ""; + } + + String learnMoreText = context.getString(R.string.dialer_terms_and_conditions_learn_more); + return context.getString(R.string.dialer_terms_and_conditions_1_0, learnMoreText); + } + + private CharSequence getExistingUserDialerTos() { + if (!isVoicemailTranscriptionEnabled()) { + return ""; } + + String learnMoreText = context.getString(R.string.dialer_terms_and_conditions_learn_more); + return context.getString(R.string.dialer_terms_and_conditions_existing_user, learnMoreText); } private CharSequence getAcceptText() { @@ -336,13 +456,19 @@ public class VoicemailTosMessageCreator { } } - private CharSequence getTosTitle() { + private CharSequence getNewUserTosTitle() { return isVvm3() ? context.getString(R.string.verizon_terms_and_conditions_title) : context.getString(R.string.dialer_terms_and_conditions_title); } - private CharSequence getTosMessage() { + private CharSequence getExistingUserTosTitle() { + return isVvm3() + ? context.getString(R.string.verizon_terms_and_conditions_title) + : context.getString(R.string.dialer_terms_and_conditions_existing_user_title); + } + + private CharSequence getNewUserTosMessageText() { SpannableString spannableTos; if (isVvm3()) { // For verizon the TOS consist of three pieces: google dialer TOS, Verizon TOS message and @@ -350,7 +476,7 @@ public class VoicemailTosMessageCreator { CharSequence vvm3Details = getVvm3Tos(); CharSequence tos = context.getString( - R.string.verizon_terms_and_conditions_message, getDialerTos(), vvm3Details); + R.string.verizon_terms_and_conditions_message, getVvmDialerTos(), vvm3Details); spannableTos = new SpannableString(tos); // Set the text style for the details part of the TOS int start = spannableTos.length() - vvm3Details.length(); @@ -365,7 +491,7 @@ public class VoicemailTosMessageCreator { } else { // The TOS for everyone else, there are no details, but change to center alignment. CharSequence tos = - context.getString(R.string.dialer_terms_and_conditions_message, getDialerTos()); + context.getString(R.string.dialer_terms_and_conditions_message, getNewUserDialerTos()); spannableTos = new SpannableString(tos); spannableTos.setSpan( new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), @@ -375,11 +501,27 @@ public class VoicemailTosMessageCreator { // Add 'Learn more' link for dialer TOS String learnMore = context.getString(R.string.dialer_terms_and_conditions_learn_more); - String linkUrl = context.getString(R.string.dialer_terms_and_conditions_learn_more_url); - return addLink(spannableTos, learnMore, linkUrl); + return addLink(spannableTos, learnMore, getLearnMoreUrl()); } } + private CharSequence getExistingUserTosMessageText() { + SpannableString spannableTos; + // Change to center alignment. + CharSequence tos = + context.getString(R.string.dialer_terms_and_conditions_message, getExistingUserDialerTos()); + spannableTos = new SpannableString(tos); + spannableTos.setSpan( + new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), + 0, + tos.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + // Add 'Learn more' link for dialer TOS + String learnMore = context.getString(R.string.dialer_terms_and_conditions_learn_more); + return addLink(spannableTos, learnMore, getLearnMoreUrl()); + } + private SpannableString addLink(SpannableString spannable, String linkText, String linkUrl) { if (TextUtils.isEmpty(linkUrl) || TextUtils.isEmpty(linkText)) { return spannable; @@ -398,6 +540,13 @@ public class VoicemailTosMessageCreator { return spannable; } + private String getLearnMoreUrl() { + return ConfigProviderBindings.get(context) + .getString( + "voicemail_transcription_learn_more_url", + context.getString(R.string.dialer_terms_and_conditions_learn_more_url)); + } + private int getTosDeclinedDialogMessageId() { return isVvm3() ? R.string.verizon_terms_and_conditions_decline_dialog_message diff --git a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java index 8e8106b44..748b8142a 100644 --- a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java +++ b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java @@ -39,7 +39,7 @@ import com.android.dialer.logging.Logger; public class Vvm3VoicemailMessageCreator { // Copied from com.android.phone.vvm.omtp.protocol.Vvm3EventHandler - // TODO(b/28380841): unbundle VVM client so we can access these values directly + // TODO(a bug): unbundle VVM client so we can access these values directly public static final int VMS_DNS_FAILURE = -9001; public static final int VMG_DNS_FAILURE = -9002; public static final int SPG_DNS_FAILURE = -9003; diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml index 184a81fec..4e143a59a 100644 --- a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml +++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml @@ -73,6 +73,7 @@ android:paddingEnd="16dp" android:paddingTop="10dp" android:paddingBottom="10dp" + android:background="#ffffffff" android:orientation="horizontal"> <TextView android:id="@+id/voicemail_tos_button_decline" @@ -82,7 +83,7 @@ android:layout_height="wrap_content" android:text="@string/verizon_terms_and_conditions_decline_english"/> <android.support.v4.widget.Space - android:layout_width="0dp" + android:layout_width="8dp" android:layout_height="match_parent" android:layout_weight="1"/> <TextView diff --git a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml index eb4877a4b..05082d8d9 100644 --- a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml +++ b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml @@ -140,6 +140,8 @@ <string name="verizon_terms_and_conditions_message"><xliff:g>%1$s</xliff:g> By turning on visual voicemail you agree to the Verizon Wireless terms and conditions:\n\n<xliff:g>%2$s</xliff:g></string> <string name="dialer_terms_and_conditions_title">Turn on visual voicemail</string> + + <string name="dialer_terms_and_conditions_existing_user_title">New! Read your voicemail</string> <string name="dialer_terms_and_conditions_message"><xliff:g>%s</xliff:g></string> <string translatable="false" name="verizon_terms_and_conditions_1.1_english"> @@ -173,14 +175,16 @@ Si no acepta todos estos términos y condiciones, no use el buzón de voz visual See and listen to your messages, without having to call voicemail. Transcripts of your voicemail are provided by Google’s free transcription service. <xliff:g>%s</xliff:g> </string> + <string name="dialer_terms_and_conditions_existing_user"> + Transcripts of your voicemail are now provided by Google’s free transcription service. %s + </string> + <string name="dialer_terms_and_conditions_for_verizon_1.0"> See and listen to your messages, without having to call voicemail. </string> <string name="dialer_terms_and_conditions_learn_more">Learn more</string> - - <!-- TODO(mdooley): STOP SHIP, get real url, b/65734734 --> - <string translatable="false" name="dialer_terms_and_conditions_learn_more_url">https://www.google.com</string> + <string translatable="false" name="dialer_terms_and_conditions_learn_more_url">https://support.google.com/phoneapp/answer/2811844?hl=en%26ref_topic=7539039</string> <string translatable="false" name="verizon_terms_and_conditions_policy_url">http://www.verizon.com/about/privacy/policy/</string> @@ -194,11 +198,16 @@ Si no acepta todos estos términos y condiciones, no use el buzón de voz visual <string translatable="false" name="dialer_terms_and_conditions_decline_english">No Thanks</string> <string translatable="false" name="dialer_terms_and_conditions_decline_spanish">Rechazar</string> + <string name="dialer_terms_and_conditions_existing_user_ack">Ok, got it</string> + <string name="dialer_terms_and_conditions_existing_user_setings">Settings</string> + + <string name="terms_and_conditions_decline_dialog_title">Disable visual voicemail?</string> + <string name="verizon_terms_and_conditions_decline_dialog_message">Visual voicemail will be disabled if the terms and conditions are declined.</string> - <string name="verizon_terms_and_conditions_decline_dialog_downgrade">Disable visual voicemail</string> + <string name="verizon_terms_and_conditions_decline_dialog_downgrade">Disable</string> - <string name="dialer_terms_and_conditions_decline_dialog_message">Visual voicemail will be disabled if the terms and conditions are declined.</string> - <string name="dialer_terms_and_conditions_decline_dialog_downgrade">Disable visual voicemail</string> + <string name="dialer_terms_and_conditions_decline_dialog_message">Visual voicemail will be disabled if you turn off visual voicemail.</string> + <string name="dialer_terms_and_conditions_decline_dialog_downgrade">Disable</string> <string name="verizon_terms_and_conditions_decline_set_pin_dialog_message">Voicemail will only be accessible by calling *86. Set a new voicemail PIN to proceed.</string> <string name="verizon_terms_and_conditions_decline_set_pin_dialog_set_pin">Set PIN</string> diff --git a/java/com/android/dialer/app/voicemail/error/res/values/styles.xml b/java/com/android/dialer/app/voicemail/error/res/values/styles.xml index 938c77a01..bf70240b6 100644 --- a/java/com/android/dialer/app/voicemail/error/res/values/styles.xml +++ b/java/com/android/dialer/app/voicemail/error/res/values/styles.xml @@ -20,32 +20,32 @@ <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">48dp</item> <item name="android:gravity">end|center_vertical</item> - <item name="android:paddingStart">8dp</item> - <item name="android:paddingEnd">8dp</item> <item name="android:layout_marginStart">8dp</item> <item name="android:layout_marginEnd">8dp</item> + <item name="android:padding">8dp</item> <item name="android:textColor">@color/dialer_theme_color</item> <item name="android:fontFamily">"sans-serif-medium"</item> <item name="android:focusable">true</item> <item name="android:singleLine">true</item> <item name="android:textAllCaps">true</item> <item name="android:textSize">14sp</item> + <item name="android:minHeight">48dp</item> </style> <style name="ErrorActionDeclineStyle"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">48dp</item> <item name="android:gravity">end|center_vertical</item> - <item name="android:paddingStart">8dp</item> - <item name="android:paddingEnd">8dp</item> <item name="android:layout_marginStart">8dp</item> <item name="android:layout_marginEnd">8dp</item> - <item name="android:textColor">#80000000</item> + <item name="android:padding">8dp</item> + <item name="android:textColor">#757575</item> <item name="android:fontFamily">"sans-serif-medium"</item> <item name="android:focusable">true</item> <item name="android:singleLine">true</item> <item name="android:textAllCaps">true</item> <item name="android:textSize">14sp</item> + <item name="android:minHeight">48dp</item> </style> <style name="RaisedErrorActionStyle" parent="Widget.AppCompat.Button.Colored"> diff --git a/java/com/android/dialer/app/widget/SearchEditTextLayout.java b/java/com/android/dialer/app/widget/SearchEditTextLayout.java index e7a707feb..ebd62080b 100644 --- a/java/com/android/dialer/app/widget/SearchEditTextLayout.java +++ b/java/com/android/dialer/app/widget/SearchEditTextLayout.java @@ -51,7 +51,6 @@ public class SearchEditTextLayout extends FrameLayout { private View mCollapsedSearchBox; private View mVoiceSearchButtonView; private View mOverflowButtonView; - private View mBackButtonView; private View mClearButtonView; private ValueAnimator mAnimator; @@ -95,11 +94,6 @@ public class SearchEditTextLayout extends FrameLayout { mCollapsedSearchBox = findViewById(R.id.search_box_start_search); mVoiceSearchButtonView = findViewById(R.id.voice_search_button); mOverflowButtonView = findViewById(R.id.dialtacts_options_menu_button); - mBackButtonView = findViewById(R.id.search_back_button); - mBackButtonView - .getResources() - .getDrawable(R.drawable.quantum_ic_arrow_back_vd_theme_24, null) - .setAutoMirrored(true); mClearButtonView = findViewById(R.id.search_close_button); // Convert a long click into a click to expand the search box. Touch events are also diff --git a/java/com/android/dialer/assisteddialing/ConcreteCreator.java b/java/com/android/dialer/assisteddialing/ConcreteCreator.java index 1790b8f3f..9dc197c89 100644 --- a/java/com/android/dialer/assisteddialing/ConcreteCreator.java +++ b/java/com/android/dialer/assisteddialing/ConcreteCreator.java @@ -71,7 +71,7 @@ public final class ConcreteCreator { } if (!PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean(context.getString(R.string.assisted_dialing_setting_toggle_key), false)) { + .getBoolean(context.getString(R.string.assisted_dialing_setting_toggle_key), true)) { LogUtil.i("ConcreteCreator.createNewAssistedDialingMediator", "disabled by local setting"); return new AssistedDialingMediatorStub(); diff --git a/java/com/android/dialer/assisteddialing/Constraints.java b/java/com/android/dialer/assisteddialing/Constraints.java index c4b5278cb..3766a6daa 100644 --- a/java/com/android/dialer/assisteddialing/Constraints.java +++ b/java/com/android/dialer/assisteddialing/Constraints.java @@ -25,6 +25,8 @@ import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.util.ArraySet; import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; @@ -216,6 +218,8 @@ final class Constraints { // framework return Optional.of(phoneNumberUtil.parseAndKeepRawInput(numberToParse, userHomeCountryCode)); } catch (NumberParseException e) { + Logger.get(context) + .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_PARSING_FAILURE); LogUtil.i("Constraints.parsePhoneNumber", "could not parse the number"); return Optional.empty(); } @@ -227,6 +231,8 @@ final class Constraints { if (parsedPhoneNumber.get().hasCountryCode() && parsedPhoneNumber.get().getCountryCodeSource() != CountryCodeSource.FROM_DEFAULT_COUNTRY) { + Logger.get(context) + .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_COUNTRY_CODE); LogUtil.i( "Constraints.isNotInternationalNumber", "phone number already provided the country code"); return false; @@ -244,6 +250,8 @@ final class Constraints { if (parsedPhoneNumber.get().hasExtension() && !TextUtils.isEmpty(parsedPhoneNumber.get().getExtension())) { + Logger.get(context) + .logImpression(DialerImpression.Type.ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_EXTENSION); LogUtil.i("Constraints.doesNotHaveExtension", "phone number has an extension"); return false; } diff --git a/java/com/android/dialer/assisteddialing/ui/res/xml/assisted_dialing_setting.xml b/java/com/android/dialer/assisteddialing/ui/res/xml/assisted_dialing_setting.xml index 8e3c62dde..806edfcf9 100644 --- a/java/com/android/dialer/assisteddialing/ui/res/xml/assisted_dialing_setting.xml +++ b/java/com/android/dialer/assisteddialing/ui/res/xml/assisted_dialing_setting.xml @@ -18,9 +18,9 @@ xmlns:android="http://schemas.android.com/apk/res/android"> <SwitchPreference - android:defaultValue="false" + android:defaultValue="true" android:key="@string/assisted_dialing_setting_toggle_key" android:title="@string/assisted_dialing_setting_title" android:summary="@string/assisted_dialing_setting_summary" /> -</PreferenceScreen>
\ No newline at end of file +</PreferenceScreen> diff --git a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java index 62bb9f39b..09fd5f0a8 100644 --- a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java +++ b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java @@ -262,7 +262,7 @@ public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler { } /* - * TODO(maxwelb): b/27779827, non-e164 numbers can be blocked in the new form of blocking. As a + * TODO(maxwelb): a bug, non-e164 numbers can be blocked in the new form of blocking. As a * temporary workaround, determine which column of the database to query based on whether the * number is e164 or not. */ diff --git a/java/com/android/dialer/buildtype/bugfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/bugfood/BuildTypeAccessorImpl.java deleted file mode 100644 index 45d72e05c..000000000 --- a/java/com/android/dialer/buildtype/bugfood/BuildTypeAccessorImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.dialer.buildtype; - -import com.android.dialer.proguard.UsedByReflection; - -/** Gets the build type. */ -@UsedByReflection(value = "BuildType.java") -public class BuildTypeAccessorImpl implements BuildTypeAccessor { - - @Override - @BuildType.Type - public int getBuildType() { - return BuildType.BUGFOOD; - } -} diff --git a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java deleted file mode 100644 index e1f2cdc79..000000000 --- a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.dialer.buildtype; - -import com.android.dialer.proguard.UsedByReflection; - -/** Gets the build type. */ -@UsedByReflection(value = "BuildType.java") -public class BuildTypeAccessorImpl implements BuildTypeAccessor { - - @Override - @BuildType.Type - public int getBuildType() { - return BuildType.DOGFOOD; - } -} diff --git a/java/com/android/dialer/buildtype/fishfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/fishfood/BuildTypeAccessorImpl.java deleted file mode 100644 index e5ad9015f..000000000 --- a/java/com/android/dialer/buildtype/fishfood/BuildTypeAccessorImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.dialer.buildtype; - -import com.android.dialer.proguard.UsedByReflection; - -/** Gets the build type. */ -@UsedByReflection(value = "BuildType.java") -public class BuildTypeAccessorImpl implements BuildTypeAccessor { - - @Override - @BuildType.Type - public int getBuildType() { - return BuildType.FISHFOOD; - } -} diff --git a/java/com/android/dialer/buildtype/test/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/test/BuildTypeAccessorImpl.java deleted file mode 100644 index 80a1cb728..000000000 --- a/java/com/android/dialer/buildtype/test/BuildTypeAccessorImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.dialer.buildtype; - -import com.android.dialer.proguard.UsedByReflection; - -/** Gets the build type. */ -@UsedByReflection(value = "BuildType.java") -public class BuildTypeAccessorImpl implements BuildTypeAccessor { - - @Override - @BuildType.Type - public int getBuildType() { - return BuildType.TEST; - } -} diff --git a/java/com/android/dialer/callcomposer/CallComposerActivity.java b/java/com/android/dialer/callcomposer/CallComposerActivity.java index 83fe2d9de..60826fd5d 100644 --- a/java/com/android/dialer/callcomposer/CallComposerActivity.java +++ b/java/com/android/dialer/callcomposer/CallComposerActivity.java @@ -242,7 +242,7 @@ public class CallComposerActivity extends AppCompatActivity } private void onCopyAndResizeImageFailure(Throwable throwable) { - // TODO(b/34279096) - gracefully handle message failure + // TODO(a bug) - gracefully handle message failure LogUtil.e("CallComposerActivity.onCopyAndResizeImageFailure", "copy Failed", throwable); } diff --git a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java index 01e067440..2e6a28c33 100644 --- a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java +++ b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java @@ -137,7 +137,7 @@ public class GalleryComposerFragment extends CallComposerFragment }) .onFailure( throwable -> { - // TODO(b/34279096) - gracefully handle message failure + // TODO(a bug) - gracefully handle message failure LogUtil.e( "GalleryComposerFragment.onFailure", "data preparation failed", throwable); }) @@ -303,7 +303,7 @@ public class GalleryComposerFragment extends CallComposerFragment if (url != null) { copyAndResizeImage.executeParallel(Uri.parse(url)); } else { - // TODO(b/34279096) - gracefully handle message failure + // TODO(a bug) - gracefully handle message failure } } } diff --git a/java/com/android/dialer/callcomposer/camera/CameraPreview.java b/java/com/android/dialer/callcomposer/camera/CameraPreview.java index 6581ad67b..eaea78961 100644 --- a/java/com/android/dialer/callcomposer/camera/CameraPreview.java +++ b/java/com/android/dialer/callcomposer/camera/CameraPreview.java @@ -65,7 +65,7 @@ public class CameraPreview { } // Opening camera is very expensive. Most of the ANR reports seem to be related to the camera. - // So we delay until the camera is actually needed. See b/23287938 + // So we delay until the camera is actually needed. See a bug private void maybeOpenCamera() { boolean visible = mHost.getView().getVisibility() == View.VISIBLE; if (mTabHasBeenShown && visible && PermissionsUtil.hasCameraPermissions(getContext())) { diff --git a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java index a713c55a4..b98dce76a 100644 --- a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java +++ b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java @@ -57,7 +57,7 @@ public class CallDetailsEntryViewHolder extends ViewHolder { private final ImageView multimediaImage; - // TODO(maxwelb): Display this when location is stored - b/36160042 + // TODO(maxwelb): Display this when location is stored - a bug @SuppressWarnings("unused") private final TextView multimediaAttachmentsNumber; diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java index 39a806568..68298e356 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java @@ -148,7 +148,7 @@ public class AnnotatedCallLogContentProvider extends ContentProvider { @Nullable @Override public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { - // Javadoc states values is not nullable, even though it is annotated as such (b/38123194)! + // Javadoc states values is not nullable, even though it is annotated as such (a bug)! Assert.checkArgument(values != null); SQLiteDatabase database = databaseHelper.getWritableDatabase(); @@ -228,7 +228,7 @@ public class AnnotatedCallLogContentProvider extends ContentProvider { @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { - // Javadoc states values is not nullable, even though it is annotated as such (b/38123194)! + // Javadoc states values is not nullable, even though it is annotated as such (a bug)! Assert.checkArgument(values != null); SQLiteDatabase database = databaseHelper.getWritableDatabase(); diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java index 589fe6384..a3180a0b9 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java @@ -44,6 +44,7 @@ class AnnotatedCallLogDatabaseHelper extends SQLiteOpenHelper { .append(AnnotatedCallLog.PHOTO_URI + " string, ") .append(AnnotatedCallLog.PHOTO_ID + " integer, ") .append(AnnotatedCallLog.LOOKUP_URI + " string, ") + .append(AnnotatedCallLog.DURATION + " integer, ") .append(AnnotatedCallLog.NUMBER_TYPE_LABEL + " string, ") .append(AnnotatedCallLog.IS_READ + " integer, ") .append(AnnotatedCallLog.NEW + " integer, ") @@ -55,7 +56,9 @@ class AnnotatedCallLogDatabaseHelper extends SQLiteOpenHelper { .append(AnnotatedCallLog.FEATURES + " integer, ") .append(AnnotatedCallLog.IS_BUSINESS + " integer, ") .append(AnnotatedCallLog.IS_VOICEMAIL + " integer, ") - .append(AnnotatedCallLog.CALL_TYPE + " integer") + .append(AnnotatedCallLog.TRANSCRIPTION + " integer, ") + .append(AnnotatedCallLog.CALL_TYPE) + .append(" integer") .append(");") .toString(); diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java index e79ffd090..832a9c2c0 100644 --- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java +++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java @@ -218,6 +218,20 @@ public class AnnotatedCallLogContract { /** The MIME type of a {@link android.content.ContentProvider#getType(Uri)} single entry. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/annotated_call_log"; + + /** + * See {@link android.provider.CallLog.Calls#DURATION}. + * + * <p>TYPE: INTEGER (int) + */ + public static final String DURATION = "duration"; + + /** + * See {@link android.provider.CallLog.Calls#TRANSCRIPTION}. + * + * <p>TYPE: TEXT + */ + public static final String TRANSCRIPTION = "transcription"; } /** diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index 681a86da7..5c73933a9 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -216,6 +216,8 @@ public class SystemCallLogDataSource implements CallLogDataSource { Calls.CACHED_LOOKUP_URI, Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_LABEL, + Calls.DURATION, + Calls.TRANSCRIPTION, Calls.IS_READ, Calls.NEW, Calls.GEOCODED_LOCATION, @@ -252,6 +254,8 @@ public class SystemCallLogDataSource implements CallLogDataSource { int cachedLookupUriColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_LOOKUP_URI); int cachedNumberTypeColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_TYPE); int cachedNumberLabelColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_LABEL); + int durationsColumn = cursor.getColumnIndexOrThrow(Calls.DURATION); + int transcriptionColumn = cursor.getColumnIndexOrThrow(Calls.TRANSCRIPTION); int isReadColumn = cursor.getColumnIndexOrThrow(Calls.IS_READ); int newColumn = cursor.getColumnIndexOrThrow(Calls.NEW); int geocodedLocationColumn = cursor.getColumnIndexOrThrow(Calls.GEOCODED_LOCATION); @@ -276,6 +280,8 @@ public class SystemCallLogDataSource implements CallLogDataSource { String cachedLookupUri = cursor.getString(cachedLookupUriColumn); int cachedNumberType = cursor.getInt(cachedNumberTypeColumn); String cachedNumberLabel = cursor.getString(cachedNumberLabelColumn); + int duration = cursor.getInt(durationsColumn); + String transcription = cursor.getString(transcriptionColumn); int isRead = cursor.getInt(isReadColumn); int isNew = cursor.getInt(newColumn); String geocodedLocation = cursor.getString(geocodedLocationColumn); @@ -321,6 +327,8 @@ public class SystemCallLogDataSource implements CallLogDataSource { populatePhoneAccountLabelAndColor( appContext, contentValues, phoneAccountComponentName, phoneAccountId); contentValues.put(AnnotatedCallLog.FEATURES, features); + contentValues.put(AnnotatedCallLog.DURATION, duration); + contentValues.put(AnnotatedCallLog.TRANSCRIPTION, transcription); if (existingAnnotatedCallLogIds.contains(id)) { mutations.update(id, contentValues); diff --git a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java index fbc789900..cadce4d48 100644 --- a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java +++ b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java @@ -51,7 +51,7 @@ public class TelephonyManagerCompat { private static final String SECRET_CODE_ACTION = "android.provider.Telephony.SECRET_CODE"; - // TODO(erfanian): b/63995261 Replace with the platform/telecom constant when available. + // TODO(erfanian): a bug Replace with the platform/telecom constant when available. /** * Indicates that the call being placed originated from a known contact. * @@ -59,7 +59,7 @@ public class TelephonyManagerCompat { */ public static final String ALLOW_ASSISTED_DIAL = "android.telecom.extra.ALLOW_ASSISTED_DIAL"; - // TODO(erfanian): b/63995261 Replace with the platform/telecom constant when available. + // TODO(erfanian): a bug Replace with the platform/telecom constant when available. /** * Indicates that an outgoing call has undergone assisted dialing. * @@ -68,7 +68,7 @@ public class TelephonyManagerCompat { */ public static final String IS_ASSISTED_DIALED = "android.telecom.extra.IS_ASSISTED_DIALED"; - // TODO(erfanian): b/63995261 Replace with the platform/telecom API when available. + // TODO(erfanian): a bug Replace with the platform/telecom API when available. /** Additional information relating to the assisted dialing transformation. */ public static final String ASSISTED_DIALING_EXTRAS = "android.telecom.extra.ASSISTED_DIALING_EXTRAS"; diff --git a/java/com/android/dialer/constants/Constants.java b/java/com/android/dialer/constants/Constants.java index f9d07e31d..a0c11f5a8 100644 --- a/java/com/android/dialer/constants/Constants.java +++ b/java/com/android/dialer/constants/Constants.java @@ -59,5 +59,8 @@ public abstract class Constants { public abstract String getUserAgent(Context context); + @NonNull + public abstract String getSettingsActivity(); + protected Constants() {} } diff --git a/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java b/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java index 38fd24b8a..c332e8281 100644 --- a/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java +++ b/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java @@ -46,4 +46,10 @@ public class ConstantsImpl extends Constants { public String getUserAgent(Context context) { return null; } + + @NonNull + @Override + public String getSettingsActivity() { + return "com.android.dialer.app.settings.DialerSettingsActivity"; + } } diff --git a/java/com/android/dialer/constants/googledialer/ConstantsImpl.java b/java/com/android/dialer/constants/googledialer/ConstantsImpl.java index e151344ba..003f748af 100644 --- a/java/com/android/dialer/constants/googledialer/ConstantsImpl.java +++ b/java/com/android/dialer/constants/googledialer/ConstantsImpl.java @@ -58,4 +58,10 @@ public class ConstantsImpl extends Constants { return userAgent.toString(); } + + @NonNull + @Override + public String getSettingsActivity() { + return "com.google.android.apps.dialer.settings.GoogleDialerSettingsActivity"; + } } diff --git a/java/com/android/dialer/contactphoto/ContactPhotoManagerImpl.java b/java/com/android/dialer/contactphoto/ContactPhotoManagerImpl.java index 5dbdf5e48..a225072fd 100644 --- a/java/com/android/dialer/contactphoto/ContactPhotoManagerImpl.java +++ b/java/com/android/dialer/contactphoto/ContactPhotoManagerImpl.java @@ -931,7 +931,7 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback { /** * Maximum number of photos to preload. If the cache size is 2Mb and the expected average size - * of a photo is 4kb, then this number should be 2Mb/4kb = 500. + * of a photo is 4kb, then this number should be 2Ma bugkb = 500. */ private static final int MAX_PHOTOS_TO_PRELOAD = 100; diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java index 6dd7cf462..9a2581221 100644 --- a/java/com/android/dialer/database/DialerDatabaseHelper.java +++ b/java/com/android/dialer/database/DialerDatabaseHelper.java @@ -1028,7 +1028,7 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { * Ignores contacts that have an unreasonably long lookup key. These are likely to be the result * of multiple (> 50) merged raw contacts, and are likely to cause OutOfMemoryExceptions within * SQLite, or cause memory allocation problems later on when iterating through the cursor set - * (see b/13133579) + * (see a bug) */ String SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE = "length(" + Phone.LOOKUP_KEY + ") < 1000"; diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java index 9f68978b5..681193c52 100644 --- a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java +++ b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java @@ -274,7 +274,7 @@ public interface EnrichedCallManager { * when dialer is not in the foreground, and can not start {@link * com.android.dialer.app.calllog.CallLogNotificationsService} to handle the event. The * pendingResult allows dialer to hold on to resources when the event is handled in a - * background thread. TODO(b/67015768): migrate CallLogNotificationsService to a + * background thread. TODO(a bug): migrate CallLogNotificationsService to a * JobIntentService so it can be used in the background. * @throws IllegalStateException if there's no session for the given id */ diff --git a/java/com/android/dialer/interactions/PhoneNumberInteraction.java b/java/com/android/dialer/interactions/PhoneNumberInteraction.java index 9692dae0f..ac744cce7 100644 --- a/java/com/android/dialer/interactions/PhoneNumberInteraction.java +++ b/java/com/android/dialer/interactions/PhoneNumberInteraction.java @@ -15,6 +15,7 @@ */ package com.android.dialer.interactions; +import android.Manifest.permission; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; @@ -227,15 +228,10 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> { // It's possible for a shortcut to have been created, and then permissions revoked. To avoid a // crash when the user tries to use such a shortcut, check for this condition and ask the user // for the permission. - String[] deniedPhonePermissions = - PermissionsUtil.getPermissionsCurrentlyDenied( - mContext, PermissionsUtil.allPhoneGroupPermissionsUsedInDialer); - if (deniedPhonePermissions.length > 0) { - LogUtil.i( - "PhoneNumberInteraction.startInteraction", - "Need phone permissions: " + Arrays.toString(deniedPhonePermissions)); + if (!PermissionsUtil.hasPhonePermissions(mContext)) { + LogUtil.i("PhoneNumberInteraction.startInteraction", "Need phone permissions: CALL_PHONE"); ActivityCompat.requestPermissions( - (Activity) mContext, deniedPhonePermissions, REQUEST_CALL_PHONE); + (Activity) mContext, new String[] {permission.CALL_PHONE}, REQUEST_CALL_PHONE); return; } diff --git a/java/com/android/dialer/logging/contact_source.proto b/java/com/android/dialer/logging/contact_source.proto index 3a24da1d0..96ef9e18e 100644 --- a/java/com/android/dialer/logging/contact_source.proto +++ b/java/com/android/dialer/logging/contact_source.proto @@ -15,7 +15,7 @@ message ContactSource { // time they made the spam report, which could be different from the // number's status at the time they made or received the call. // Type definitions are from the CachedContactInfo interface in - // google3/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java + // CachedNumberLookupService.java enum Type { UNKNOWN_SOURCE_TYPE = 0; diff --git a/java/com/android/dialer/logging/dialer_impression.proto b/java/com/android/dialer/logging/dialer_impression.proto index 098391877..cafcae3e3 100644 --- a/java/com/android/dialer/logging/dialer_impression.proto +++ b/java/com/android/dialer/logging/dialer_impression.proto @@ -13,7 +13,6 @@ message DialerImpression { // It's perfectly acceptable for this enum to be large // Values should be from 1000 to 100000. enum Type { - UNKNOWN_AOSP_EVENT_TYPE = 1000; // User opened the app @@ -25,88 +24,94 @@ message DialerImpression { // User pressed the speaker phone button again IN_CALL_SCREEN_TURN_ON_WIRED_OR_EARPIECE = 1003; - // Number not identified as spam and the user tapped the block/report spam button in the - // call log + // Number not identified as spam and the user tapped the block/report spam + // button in the call log CALL_LOG_BLOCK_REPORT_SPAM = 1004; - // Number identified as spam and the user tapped on the block number call log item + // Number identified as spam and the user tapped on the block number call + // log item CALL_LOG_BLOCK_NUMBER = 1005; // User tapped on the unblock number in the call log - // This does not deal with whether the user reported this spam or not while initially blocking - // For that refer to REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER. If the user had not reported it as - // spam they then have the option of directly unblocking the number, a success of which is - // logged in USER_ACTION_UNBLOCKED_NUMBER + // This does not deal with whether the user reported this spam or not while + // initially blocking For that refer to + // REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER. If the user had not reported it as + // spam they then have the option of directly unblocking the number, a + // success of which is logged in USER_ACTION_UNBLOCKED_NUMBER CALL_LOG_UNBLOCK_NUMBER = 1006; // Number was identified as spam, and the user tapped that it was not spam CALL_LOG_REPORT_AS_NOT_SPAM = 1007; - // Confirmation dialog in which the user confirmed that the number was not spam + // Confirmation dialog in which the user confirmed that the number was not + // spam DIALOG_ACTION_CONFIRM_NUMBER_NOT_SPAM = 1008; // User unblocked a number and also acknowledged that the number is not spam - // This happens when the user had initially blocked a number and also claimed the number was - // spam and had now proceeded to undo that. + // This happens when the user had initially blocked a number and also + // claimed the number was spam and had now proceeded to undo that. REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER = 1009 - ; + ; - // A number that was identified as spam and the user proceeded to block it. However this - // impression was to make sure that while blocking the number the user also acknowledged that - // they were going to be reporting this as spam. There is no option for the user in this case - // to not report it as spam and block it only. The only flow is: - // system identified number as spam -> user wants to block it -> confirmation dialog shows up - // asking user to acknowledge they want to block and report as spam -> user acknowledges and - // this is when this impression is sent + // A number that was identified as spam and the user proceeded to block it. + // However this impression was to make sure that while blocking the number + // the user also acknowledged that they were going to be reporting this as + // spam. There is no option for the user in this case to not report it as + // spam and block it only. The only flow is: system identified number as + // spam -> user wants to block it -> confirmation dialog shows up asking + // user to acknowledge they want to block and report as spam -> user + // acknowledges and this is when this impression is sent DIALOG_ACTION_CONFIRM_NUMBER_SPAM_INDIRECTLY_VIA_BLOCK_NUMBER = 1010; - // User reported the number as spam by tick marking on report spam when blocking - // the number via call log. This is for case where the user wants to block a number and also - // report it as spam - REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = 1011 + // User reported the number as spam by tick marking on report spam when + // blocking the number via call log. This is for case where the user wants + // to block a number and also report it as spam + REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = + 1011 - ; + ; // User made it to the last step and actually blocked the number USER_ACTION_BLOCKED_NUMBER = 1012 - ; + ; // User made it to the last step and actually unblocked the number USER_ACTION_UNBLOCKED_NUMBER = 1013; - // User blocked a number, does not guarantee if the number was reported as spam or not - // To compute the number of blocked numbers that were reported as not spam and yet blocked - // Subtract this value from SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM. It would be + // User blocked a number, does not guarantee if the number was reported as + // spam or not To compute the number of blocked numbers that were reported + // as not spam and yet blocked Subtract this value from + // SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM. It would be // interesting to see how this value compares with // SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER = 1014; - // Displays the dialog for first time spam calls with actions "Not spam", "Block", and - // "Dismiss". + // Displays the dialog for first time spam calls with actions "Not spam", + // "Block", and "Dismiss". SPAM_AFTER_CALL_NOTIFICATION_SHOW_SPAM_DIALOG = 1015; - // Displays the dialog for the first time unknown calls with actions "Add contact", - // "Block/report spam", and "Dismiss". + // Displays the dialog for the first time unknown calls with actions "Add + // contact", "Block/report spam", and "Dismiss". SPAM_AFTER_CALL_NOTIFICATION_SHOW_NON_SPAM_DIALOG = 1016; // User added the number to contacts from the after call notification SPAM_AFTER_CALL_NOTIFICATION_ADD_TO_CONTACTS = 1019 - ; + ; // User marked the number as spam on the after call notification flow SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM = 1020 - ; + ; SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_NOT_SPAM_AND_BLOCKED = 1021; // User reported the number as not spam SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM = 1022 - ; + ; // User dismissed the spam notification SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_SPAM_DIALOG = 1024; @@ -114,21 +119,22 @@ message DialerImpression { // User dismissed the non spam notification SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_NON_SPAM_DIALOG = 1025; - // From the service instead of an activity logs the number of times the number was marked as - // Spam by the user (e.g from the feedback prompt) + // From the service instead of an activity logs the number of times the + // number was marked as Spam by the user (e.g from the feedback prompt) SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_SPAM = 1026; - // From the service instead of an activity logs the number of times the number was marked as - // Not Spam by the user (e.g from the feedback prompt) + // From the service instead of an activity logs the number of times the + // number was marked as Not Spam by the user (e.g from the feedback prompt) SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_NOT_SPAM = 1027; // User is in a active call i.e either incoming or outgoing - // This is mainly so we can assign an impression event to a call event i.e so that we may be - // able to stitch different types of events if they make sense e.g user pressed a speaker button - // and we want to associate that to a call event + // This is mainly so we can assign an impression event to a call event i.e + // so that we may be able to stitch different types of events if they make + // sense e.g user pressed a speaker button and we want to associate that to + // a call event USER_PARTICIPATED_IN_A_CALL = 1028 - ; + ; // Incoming call is a spam call INCOMING_SPAM_CALL = 1029; @@ -155,60 +161,64 @@ message DialerImpression { // User has clicked the change PIN action in the voicemail tab VOICEMAIL_ALERT_SET_PIN_CLICKED = 1046; - // User was not able to or did not participate in the call e.g missed calls, rejected calls + // User was not able to or did not participate in the call e.g missed calls, + // rejected calls USER_DID_NOT_PARTICIPATE_IN_CALL = 1047; // User deleted a call log entry USER_DELETED_CALL_LOG_ITEM = 1048 - ; + ; // User tapped on "Send a message" CALL_LOG_SEND_MESSAGE = 1049 - ; + ; // User tapped on "Add to contact" CALL_LOG_ADD_TO_CONTACT = 1050 - ; + ; // User tapped on "Create new contact" CALL_LOG_CREATE_NEW_CONTACT = 1051 - ; + ; // User deleted an entry from the voicemail tab VOICEMAIL_DELETE_ENTRY = 1052 - ; + ; - // Voicemail call log entry was expanded. Could be either if the user tapped the voicemail - // call log entry or pressed the play button when the voicemail call log entry was not expanded + // Voicemail call log entry was expanded. Could be either if the user tapped + // the voicemail call log entry or pressed the play button when the + // voicemail call log entry was not expanded VOICEMAIL_EXPAND_ENTRY = 1053 - ; + ; - // The play button for voicemail call log entry was tapped directly (i.e when the voicemail - // call log entry was not expanded and the playbutton was tapped) - VOICEMAIL_PLAY_AUDIO_DIRECTLY= 1054 + // The play button for voicemail call log entry was tapped directly (i.e + // when the voicemail call log entry was not expanded and the playbutton was + // tapped) + VOICEMAIL_PLAY_AUDIO_DIRECTLY = 1054 - ; + ; // The play button after expanding the voicemail call log entry was tapped - VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY= 1055 + VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY = 1055 - ; + ; // Incoming call was rejected from the notifications - REJECT_INCOMING_CALL_FROM_NOTIFICATION= 1056 + REJECT_INCOMING_CALL_FROM_NOTIFICATION = 1056 - ; + ; - // Incoming call was rejected from the answer screen including rejecting via sms and talkback - REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN= 1057 + // Incoming call was rejected from the answer screen including rejecting via + // sms and talkback + REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN = 1057 - ; + ; // User tapped block and spam buttons in context menu, same as buttons in // call log drop down @@ -441,7 +451,7 @@ message DialerImpression { MULTISELECT_DELETE_ENTRY_VIA_CONFIRMATION_DIALOG = 1209; MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_BUTTON = 1210; MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_TOUCH = 1211; - MULTISELECT_ROTATE_AND_SHOW_ACTION_MODE= 1212; + MULTISELECT_ROTATE_AND_SHOW_ACTION_MODE = 1212; // Impressions for verizon VVM with backup and transcription ToS VOICEMAIL_VVM3_TOS_V2_CREATED = 1213; @@ -559,5 +569,17 @@ message DialerImpression { CALL_DETAILS_IMS_VIDEO_CALL_BACK = 1284; CALL_DETAILS_LIGHTBRINGER_CALL_BACK = 1285; CALL_DETAILS_VOICE_CALL_BACK = 1286; + + // Assisted Dialing related impressions tracking failures modes within the + // library. + + // Indicates a failure to parse a provided number string in libphonenumber. + ASSISTED_DIALING_CONSTRAINT_PARSING_FAILURE = 1287; + // Indicates that the number attempting to be assisted dialed already + // specified a country code. + ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_COUNTRY_CODE = 1288; + // Indicates that the number attempting to be assisted dialed had an + // extension. + ASSISTED_DIALING_CONSTRAINT_NUMBER_HAS_EXTENSION = 1289; } } diff --git a/java/com/android/dialer/main/impl/AndroidManifest.xml b/java/com/android/dialer/main/impl/AndroidManifest.xml index 8edde507c..6b7475f97 100644 --- a/java/com/android/dialer/main/impl/AndroidManifest.xml +++ b/java/com/android/dialer/main/impl/AndroidManifest.xml @@ -32,7 +32,7 @@ android:theme="@style/NuiMainActivityTheme" android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"> - <!-- LINT.IfChange --> + <intent-filter> <action android:name="android.intent.action.DIAL"/> @@ -98,7 +98,7 @@ <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.TAB"/> </intent-filter> - <!-- LINT.ThenChange(//depot/google3/third_party/java_src/android_app/dialer/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml) --> + <meta-data android:name="com.android.keyguard.layout" diff --git a/java/com/android/dialer/notification/NotificationChannelManager.java b/java/com/android/dialer/notification/NotificationChannelManager.java index 93caed503..790aac36f 100644 --- a/java/com/android/dialer/notification/NotificationChannelManager.java +++ b/java/com/android/dialer/notification/NotificationChannelManager.java @@ -133,7 +133,7 @@ public final class NotificationChannelManager { new NotificationChannel( NotificationChannelId.ONGOING_CALL, context.getText(R.string.notification_channel_ongoing_call), - NotificationManager.IMPORTANCE_MAX); + NotificationManager.IMPORTANCE_DEFAULT); channel.setShowBadge(false); channel.enableLights(false); channel.enableVibration(false); diff --git a/java/com/android/dialer/notification/VoicemailChannelUtils.java b/java/com/android/dialer/notification/VoicemailChannelUtils.java index e2d0f3a21..374619ade 100644 --- a/java/com/android/dialer/notification/VoicemailChannelUtils.java +++ b/java/com/android/dialer/notification/VoicemailChannelUtils.java @@ -16,6 +16,7 @@ package com.android.dialer.notification; +import android.Manifest.permission; import android.annotation.TargetApi; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -25,6 +26,8 @@ import android.os.Build.VERSION_CODES; import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.RequiresPermission; +import android.support.annotation.VisibleForTesting; import android.support.v4.os.BuildCompat; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; @@ -34,6 +37,7 @@ import android.text.TextUtils; import android.util.ArraySet; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.util.PermissionsUtil; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -41,9 +45,10 @@ import java.util.Set; /** Utilities for working with voicemail channels. */ @TargetApi(VERSION_CODES.O) /* package */ final class VoicemailChannelUtils { - private static final String GLOBAL_VOICEMAIL_CHANNEL_ID = "phone_voicemail"; + @VisibleForTesting static final String GLOBAL_VOICEMAIL_CHANNEL_ID = "phone_voicemail"; private static final String PER_ACCOUNT_VOICEMAIL_CHANNEL_ID_PREFIX = "phone_voicemail_account_"; + @SuppressWarnings("MissingPermission") // isSingleSimDevice() returns true if no permission static Set<String> getAllChannelIds(@NonNull Context context) { Assert.checkArgument(BuildCompat.isAtLeastO()); Assert.isNotNull(context); @@ -59,6 +64,7 @@ import java.util.Set; return result; } + @SuppressWarnings("MissingPermission") // isSingleSimDevice() returns true if no permission static void createAllChannels(@NonNull Context context) { Assert.checkArgument(BuildCompat.isAtLeastO()); Assert.isNotNull(context); @@ -127,24 +133,38 @@ import java.util.Set; */ private static void createGlobalVoicemailChannel(@NonNull Context context) { NotificationChannel channel = newChannel(context, GLOBAL_VOICEMAIL_CHANNEL_ID, null); + migrateGlobalVoicemailSoundSettings(context, channel); + context.getSystemService(NotificationManager.class).createNotificationChannel(channel); + } + @SuppressWarnings("MissingPermission") // checked with PermissionsUtil + private static void migrateGlobalVoicemailSoundSettings( + Context context, NotificationChannel channel) { + if (!PermissionsUtil.hasReadPhoneStatePermissions(context)) { + LogUtil.i( + "VoicemailChannelUtils.migrateGlobalVoicemailSoundSettings", + "missing phone permission, not migrating sound settings"); + return; + } TelecomManager telecomManager = context.getSystemService(TelecomManager.class); PhoneAccountHandle handle = telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL); if (handle == null) { LogUtil.i( - "VoicemailChannelUtils.createGlobalVoicemailChannel", + "VoicemailChannelUtils.migrateGlobalVoicemailSoundSettings", "phone account is null, not migrating sound settings"); - } else if (!isChannelAllowedForAccount(context, handle)) { + return; + } + if (!isChannelAllowedForAccount(context, handle)) { LogUtil.i( - "VoicemailChannelUtils.createGlobalVoicemailChannel", + "VoicemailChannelUtils.migrateGlobalVoicemailSoundSettings", "phone account is not eligable, not migrating sound settings"); - } else { - migrateVoicemailSoundSettings(context, channel, handle); + return; } - context.getSystemService(NotificationManager.class).createNotificationChannel(channel); + migrateVoicemailSoundSettings(context, channel, handle); } + @RequiresPermission(permission.READ_PHONE_STATE) private static List<PhoneAccountHandle> getAllEligableAccounts(@NonNull Context context) { List<PhoneAccountHandle> handles = new ArrayList<>(); TelecomManager telecomManager = context.getSystemService(TelecomManager.class); @@ -210,6 +230,9 @@ import java.util.Set; } private static boolean isSingleSimDevice(@NonNull Context context) { + if (!PermissionsUtil.hasReadPhoneStatePermissions(context)) { + return true; + } return context.getSystemService(TelephonyManager.class).getPhoneCount() <= 1; } diff --git a/java/com/android/dialer/oem/CequintCallerIdManager.java b/java/com/android/dialer/oem/CequintCallerIdManager.java index 7b6ddbc3a..df624e06a 100644 --- a/java/com/android/dialer/oem/CequintCallerIdManager.java +++ b/java/com/android/dialer/oem/CequintCallerIdManager.java @@ -166,7 +166,7 @@ public class CequintCallerIdManager { Assert.isWorkerThread(); Assert.isNotNull(number); - // Cequint is using custom arguments for content provider. See more details in b/35766080. + // Cequint is using custom arguments for content provider. See more details in a bug. try (Cursor cursor = context.getContentResolver().query(uri, EMPTY_PROJECTION, number, flags, null)) { if (cursor != null && cursor.moveToFirst()) { diff --git a/java/com/android/dialer/oem/MotorolaUtils.java b/java/com/android/dialer/oem/MotorolaUtils.java index 5f5bde61a..a2757d3ce 100644 --- a/java/com/android/dialer/oem/MotorolaUtils.java +++ b/java/com/android/dialer/oem/MotorolaUtils.java @@ -102,7 +102,7 @@ public class MotorolaUtils { * @return true if the input is consumed and the intent is launched */ public static boolean handleSpecialCharSequence(Context context, String input) { - // TODO(b/35395377): Add check for Motorola devices. + // TODO(a bug): Add check for Motorola devices. return MotorolaHiddenMenuKeySequence.handleCharSequence(context, input); } diff --git a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java index f501792f9..8e08fb20d 100644 --- a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java +++ b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java @@ -159,7 +159,7 @@ public class ContactInfoHelper { // ENTERPRISE_CONTENT_FILTER_URI in M doesn't support directory lookup uri = PhoneLookup.CONTENT_FILTER_URI; } else { - // b/25900607 in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice. + // a bug in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice. number = Uri.encode(number); } } diff --git a/java/com/android/dialer/phonenumberproto/dialer_phone_number.proto b/java/com/android/dialer/phonenumberproto/dialer_phone_number.proto index 97b7519dd..cd2ed50e0 100644 --- a/java/com/android/dialer/phonenumberproto/dialer_phone_number.proto +++ b/java/com/android/dialer/phonenumberproto/dialer_phone_number.proto @@ -97,8 +97,7 @@ message DialerInternalPhoneNumber { // leading zeros. // // Clients who use the parsing or conversion functionality of the i18n phone - // number libraries (go/phonenumbers) will have these fields set if necessary - // automatically. + // number libraries will have these fields set if necessary automatically. optional bool italian_leading_zero = 4; optional int32 number_of_leading_zeros = 8 [default = 1]; diff --git a/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java b/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java index a53676b9a..40a338588 100644 --- a/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java +++ b/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java @@ -21,11 +21,13 @@ import android.database.Cursor; import android.net.Uri; import android.os.Trace; import android.provider.CallLog; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.telecom.PhoneAccountHandle; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.text.TextUtils; +import android.util.SparseIntArray; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.compat.CompatUtils; @@ -43,6 +45,71 @@ public class PhoneNumberHelper { private static final Set<String> LEGACY_UNKNOWN_NUMBERS = new HashSet<>(Arrays.asList("-1", "-2", "-3")); + /** The phone keypad letter mapping (see ITU E.161 or ISO/IEC 9995-8.) */ + private static final SparseIntArray KEYPAD_MAP = new SparseIntArray(); + + static { + KEYPAD_MAP.put('a', '2'); + KEYPAD_MAP.put('b', '2'); + KEYPAD_MAP.put('c', '2'); + KEYPAD_MAP.put('A', '2'); + KEYPAD_MAP.put('B', '2'); + KEYPAD_MAP.put('C', '2'); + + KEYPAD_MAP.put('d', '3'); + KEYPAD_MAP.put('e', '3'); + KEYPAD_MAP.put('f', '3'); + KEYPAD_MAP.put('D', '3'); + KEYPAD_MAP.put('E', '3'); + KEYPAD_MAP.put('F', '3'); + + KEYPAD_MAP.put('g', '4'); + KEYPAD_MAP.put('h', '4'); + KEYPAD_MAP.put('i', '4'); + KEYPAD_MAP.put('G', '4'); + KEYPAD_MAP.put('H', '4'); + KEYPAD_MAP.put('I', '4'); + + KEYPAD_MAP.put('j', '5'); + KEYPAD_MAP.put('k', '5'); + KEYPAD_MAP.put('l', '5'); + KEYPAD_MAP.put('J', '5'); + KEYPAD_MAP.put('K', '5'); + KEYPAD_MAP.put('L', '5'); + + KEYPAD_MAP.put('m', '6'); + KEYPAD_MAP.put('n', '6'); + KEYPAD_MAP.put('o', '6'); + KEYPAD_MAP.put('M', '6'); + KEYPAD_MAP.put('N', '6'); + KEYPAD_MAP.put('O', '6'); + + KEYPAD_MAP.put('p', '7'); + KEYPAD_MAP.put('q', '7'); + KEYPAD_MAP.put('r', '7'); + KEYPAD_MAP.put('s', '7'); + KEYPAD_MAP.put('P', '7'); + KEYPAD_MAP.put('Q', '7'); + KEYPAD_MAP.put('R', '7'); + KEYPAD_MAP.put('S', '7'); + + KEYPAD_MAP.put('t', '8'); + KEYPAD_MAP.put('u', '8'); + KEYPAD_MAP.put('v', '8'); + KEYPAD_MAP.put('T', '8'); + KEYPAD_MAP.put('U', '8'); + KEYPAD_MAP.put('V', '8'); + + KEYPAD_MAP.put('w', '9'); + KEYPAD_MAP.put('x', '9'); + KEYPAD_MAP.put('y', '9'); + KEYPAD_MAP.put('z', '9'); + KEYPAD_MAP.put('W', '9'); + KEYPAD_MAP.put('X', '9'); + KEYPAD_MAP.put('Y', '9'); + KEYPAD_MAP.put('Z', '9'); + } + /** Returns true if it is possible to place a call to the given number. */ public static boolean canPlaceCallsTo(CharSequence number, int presentation) { return presentation == CallLog.Calls.PRESENTATION_ALLOWED @@ -60,29 +127,59 @@ public class PhoneNumberHelper { * <li>their corresponding raw numbers are both global phone numbers (i.e., they can be accepted * by {@link PhoneNumberUtils#isGlobalPhoneNumber(String)}) and {@link * PhoneNumberUtils#compare(String, String)} deems them as "identical enough"; OR - * <li>at least one of the raw numbers is not a global phone number and the two raw numbers are - * exactly the same. + * <li>neither of the raw numbers is a global phone number and they are identical. * </ul> * - * The raw number of a phone number is obtained by first translating any alphabetic letters - * ([A-Za-z]) into the equivalent numeric digits and then removing all separators. See {@link - * PhoneNumberUtils#convertKeypadLettersToDigits(String)} and {@link - * PhoneNumberUtils#stripSeparators(String)}. + * See {@link #convertAndStrip(String)} for how a raw number is obtained. */ public static boolean compare(@Nullable String number1, @Nullable String number2) { if (number1 == null || number2 == null) { return Objects.equals(number1, number2); } - String rawNumber1 = - PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(number1)); - String rawNumber2 = - PhoneNumberUtils.stripSeparators(PhoneNumberUtils.convertKeypadLettersToDigits(number2)); + String rawNumber1 = convertAndStrip(number1); + String rawNumber2 = convertAndStrip(number2); + + boolean isGlobalPhoneNumber1 = PhoneNumberUtils.isGlobalPhoneNumber(rawNumber1); + boolean isGlobalPhoneNumber2 = PhoneNumberUtils.isGlobalPhoneNumber(rawNumber2); + + if (isGlobalPhoneNumber1 && isGlobalPhoneNumber2) { + return PhoneNumberUtils.compare(rawNumber1, rawNumber2); + } + if (!isGlobalPhoneNumber1 && !isGlobalPhoneNumber2) { + return rawNumber1.equals(rawNumber2); + } + return false; + } + + /** + * Translating any alphabetic letters ([A-Za-z]) in the given phone number into the equivalent + * numeric digits and then removing all separators. The caller should ensure the number passed to + * this method is not null. + */ + private static String convertAndStrip(@NonNull String number) { + int len = number.length(); + if (len == 0) { + return number; + } + + StringBuilder ret = new StringBuilder(len); + for (int i = 0; i < len; i++) { + char c = number.charAt(i); + + // If the char isn't in KEYPAD_MAP, leave it alone for now. + c = (char) KEYPAD_MAP.get(c, c); + + // Append the char to the result if it's a digit or non-separator. + int digit = Character.digit(c, 10); + if (digit != -1) { + ret.append(digit); + } else if (PhoneNumberUtils.isNonSeparator(c)) { + ret.append(c); + } + } - return PhoneNumberUtils.isGlobalPhoneNumber(rawNumber1) - && PhoneNumberUtils.isGlobalPhoneNumber(rawNumber2) - ? PhoneNumberUtils.compare(rawNumber1, rawNumber2) - : rawNumber1.equals(rawNumber2); + return ret.toString(); } /** diff --git a/java/com/android/dialer/proguard/proguard_base.flags b/java/com/android/dialer/proguard/proguard_base.flags index 7b5794ec7..6d5d373fd 100644 --- a/java/com/android/dialer/proguard/proguard_base.flags +++ b/java/com/android/dialer/proguard/proguard_base.flags @@ -1,4 +1,3 @@ -# Copied from http://google3/java/com/google/android/apps/common/proguard/base.flags # This file is intended to contain proguard options that *nobody* would ever # not want, in *any* configuration - they ensure basic correctness, and have diff --git a/java/com/android/dialer/proguard/proguard_release.flags b/java/com/android/dialer/proguard/proguard_release.flags index 1429740f4..5c7aa83f3 100644 --- a/java/com/android/dialer/proguard/proguard_release.flags +++ b/java/com/android/dialer/proguard/proguard_release.flags @@ -1,4 +1,3 @@ -# Copied from http://google3/java/com/google/android/apps/common/proguard/release.flags # Used for building release binaries. Obfuscates, optimizes, and shrinks. diff --git a/java/com/android/dialer/searchfragment/common/Projections.java b/java/com/android/dialer/searchfragment/common/Projections.java index aaf9e80f1..63fac4ca0 100644 --- a/java/com/android/dialer/searchfragment/common/Projections.java +++ b/java/com/android/dialer/searchfragment/common/Projections.java @@ -42,7 +42,7 @@ public class Projections { public static final int COMPANY_NAME = 12; public static final int NICKNAME = 13; - public static final String[] DATA_PROJECTION = + public static final String[] CP2_PROJECTION = new String[] { Data._ID, // 0 Phone.TYPE, // 1 @@ -59,4 +59,20 @@ public class Projections { Organization.COMPANY, // 12 Nickname.NAME // 13 }; + + public static final String[] DATA_PROJECTION = + new String[] { + Data._ID, // 0 + Phone.TYPE, // 1 + Phone.LABEL, // 2 + Phone.NUMBER, // 3 + Data.DISPLAY_NAME_PRIMARY, // 4 + Data.PHOTO_ID, // 5 + Data.PHOTO_THUMBNAIL_URI, // 6 + Data.LOOKUP_KEY, // 7 + Data.CARRIER_PRESENCE, // 8 + Data.CONTACT_ID, // 9 + Data.MIMETYPE, // 10 + Data.SORT_KEY_PRIMARY, // 11 + }; } diff --git a/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java b/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java index 9a0ca0088..84c22a2cf 100644 --- a/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java +++ b/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java @@ -31,7 +31,6 @@ import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.support.v4.util.ArraySet; import android.text.TextUtils; -import com.android.dialer.common.Assert; import com.android.dialer.searchfragment.common.Projections; import com.android.dialer.searchfragment.common.QueryFilteringUtil; import java.lang.annotation.Retention; @@ -71,7 +70,7 @@ final class ContactFilterCursor implements Cursor { } /** - * @param cursor with projection {@link Projections#DATA_PROJECTION}. + * @param cursor with projection {@link Projections#CP2_PROJECTION}. * @param query to filter cursor results. */ ContactFilterCursor(Cursor cursor, @Nullable String query) { @@ -123,8 +122,7 @@ final class ContactFilterCursor implements Cursor { // Sort by display name, then build new cursor from coalesced contacts. // We sort the contacts so that they are displayed to the user in lexicographic order. Collections.sort(coalescedContacts, (o1, o2) -> o1.displayName().compareTo(o2.displayName())); - MatrixCursor newCursor = - new MatrixCursor(Projections.DATA_PROJECTION, coalescedContacts.size()); + MatrixCursor newCursor = new MatrixCursor(Projections.CP2_PROJECTION, coalescedContacts.size()); for (Cp2Contact contact : coalescedContacts) { newCursor.addRow(contact.toCursorRow()); } @@ -139,11 +137,13 @@ final class ContactFilterCursor implements Cursor { if (contact.mimeType().equals(Phone.CONTENT_ITEM_TYPE)) { phoneContacts.add(contact); } else if (contact.mimeType().equals(Organization.CONTENT_ITEM_TYPE)) { - Assert.checkArgument(TextUtils.isEmpty(companyName)); - companyName = contact.companyName(); + // Since a contact can have more than one company name but they aren't visible to the user + // in our search UI, we can lazily concatenate them together to make them all searchable. + companyName += " " + contact.companyName(); } else if (contact.mimeType().equals(Nickname.CONTENT_ITEM_TYPE)) { - Assert.checkArgument(TextUtils.isEmpty(nickName)); - nickName = contact.nickName(); + // Since a contact can have more than one nickname but they aren't visible to the user + // in our search UI, we can lazily concatenate them together to make them all searchable. + nickName += " " + contact.nickName(); } } diff --git a/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java b/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java index f199f679b..8e5e3e781 100644 --- a/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java +++ b/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java @@ -111,7 +111,7 @@ public abstract class Cp2Contact { } public Object[] toCursorRow() { - Object[] row = new Object[Projections.DATA_PROJECTION.length]; + Object[] row = new Object[Projections.CP2_PROJECTION.length]; row[Projections.ID] = phoneId(); row[Projections.PHONE_TYPE] = phoneType(); row[Projections.PHONE_LABEL] = phoneLabel(); diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java index f1230c6d9..d3abbffca 100644 --- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java +++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java @@ -36,7 +36,7 @@ public final class SearchContactsCursorLoader extends CursorLoader { super( context, Data.CONTENT_URI, - Projections.DATA_PROJECTION, + Projections.CP2_PROJECTION, whereStatement(), null, Phone.SORT_KEY_PRIMARY + " ASC"); diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java index 254453508..ef1b4fc19 100644 --- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java +++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java @@ -412,7 +412,7 @@ public final class NewSearchFragment extends Fragment // Currently, setting up multiple FakeContentProviders doesn't work and results in this fragment // being untestable while it can query multiple datasources. This is a temporary fix. - // TODO(b/64099602): Remove this method and test this fragment with multiple data sources + // TODO(a bug): Remove this method and test this fragment with multiple data sources @VisibleForTesting public void setRemoteDirectoriesDisabled(boolean disabled) { remoteDirectoriesDisabledForTesting = disabled; diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java index eb472732f..64175bebc 100644 --- a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java +++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java @@ -44,7 +44,8 @@ public final class RemoteContactsCursorLoader extends CursorLoader { Uri.withAppendedPath(Phone.CONTENT_URI, "filter_enterprise"); private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000"; - private static final String MAX_RESULTS = "20"; + private static final String PHONE_NUMBER_NOT_NULL = Phone.NUMBER + " IS NOT NULL"; + private static final String MAX_RESULTS = "10"; private final String query; private final List<Directory> directories; @@ -55,7 +56,7 @@ public final class RemoteContactsCursorLoader extends CursorLoader { context, null, Projections.DATA_PROJECTION, - IGNORE_NUMBER_TOO_LONG_CLAUSE, + IGNORE_NUMBER_TOO_LONG_CLAUSE + " AND " + PHONE_NUMBER_NOT_NULL, null, Phone.SORT_KEY_PRIMARY); this.query = query; diff --git a/java/com/android/dialer/searchfragment/testing/TestCursorSchema.java b/java/com/android/dialer/searchfragment/testing/TestCursorSchema.java index 375fdb50c..9117f72cd 100644 --- a/java/com/android/dialer/searchfragment/testing/TestCursorSchema.java +++ b/java/com/android/dialer/searchfragment/testing/TestCursorSchema.java @@ -23,7 +23,7 @@ public final class TestCursorSchema { /** * If new rows are added to {@link - * com.android.dialer.searchfragment.common.Projections#DATA_PROJECTION}, this schema should be + * com.android.dialer.searchfragment.common.Projections#CP2_PROJECTION}, this schema should be * updated. */ // TODO(67909522): remove these extra columns and remove all references to "Phone." diff --git a/java/com/android/dialer/simulator/Simulator.java b/java/com/android/dialer/simulator/Simulator.java index 4812fa5d6..bfa202c5c 100644 --- a/java/com/android/dialer/simulator/Simulator.java +++ b/java/com/android/dialer/simulator/Simulator.java @@ -31,7 +31,7 @@ public interface Simulator { ActionProvider getActionProvider(Context context); /** The type of conference to emulate. */ - // TODO(b/67785540): add VoLTE and CDMA conference call + // TODO(a bug): add VoLTE and CDMA conference call @Retention(RetentionPolicy.SOURCE) @IntDef({ CONFERENCE_TYPE_GSM, diff --git a/java/com/android/dialer/strictmode/impl/SystemDialerStrictMode.java b/java/com/android/dialer/strictmode/impl/SystemDialerStrictMode.java index b974ab18a..09fdf5cda 100644 --- a/java/com/android/dialer/strictmode/impl/SystemDialerStrictMode.java +++ b/java/com/android/dialer/strictmode/impl/SystemDialerStrictMode.java @@ -53,7 +53,7 @@ final class SystemDialerStrictMode implements DialerStrictMode { // Because Android resets StrictMode policies after Application.onCreate is done, we set it // again right after. // See cl/105932355 for the discussion. - // See b/36951662 for the public bug. + // See a bug for the public bug. Handler handler = new Handler(Looper.myLooper()); handler.postAtFrontOfQueue(() -> setRecommendedMainThreadPolicy(THREAD_DEATH_PENALTY)); } @@ -93,7 +93,7 @@ final class SystemDialerStrictMode implements DialerStrictMode { .detectLeakedSqlLiteObjects(); if (Build.VERSION.SDK_INT >= 26) { vmPolicyBuilder.detectContentUriWithoutPermission(); - // TODO(azlatin): Enable detecting untagged sockets once: b/64840386 is fixed. + // TODO(azlatin): Enable detecting untagged sockets once: a bug is fixed. // vmPolicyBuilder.detectUntaggedSockets(); } StrictMode.setVmPolicy(vmPolicyBuilder.build()); diff --git a/java/com/android/dialer/theme/res/values/colors.xml b/java/com/android/dialer/theme/res/values/colors.xml index 3c8cabbc5..f44a7ccc6 100644 --- a/java/com/android/dialer/theme/res/values/colors.xml +++ b/java/com/android/dialer/theme/res/values/colors.xml @@ -38,6 +38,7 @@ <!-- Primary text color in the Phone app --> <color name="dialer_primary_text_color">#333333</color> + <color name="dialer_primary_text_color_white">#ffffff</color> <color name="dialer_edit_text_hint_color">#DE78909C</color> <!-- Secondary text color in the Phone app --> @@ -69,4 +70,7 @@ <!-- Color of call type icons in call log, e.g. voicemail, video, WiFi, HD etc. --> <color name="call_type_icon_color">#89000000</color> + + <!-- Color for bubble --> + <color name="dialer_end_call_button_color">#FFDF0000</color> </resources> diff --git a/java/com/android/dialer/util/PermissionsUtil.java b/java/com/android/dialer/util/PermissionsUtil.java index cb973680d..02ea91093 100644 --- a/java/com/android/dialer/util/PermissionsUtil.java +++ b/java/com/android/dialer/util/PermissionsUtil.java @@ -77,6 +77,10 @@ public class PermissionsUtil { return hasPermission(context, permission.CALL_PHONE); } + public static boolean hasReadPhoneStatePermissions(Context context) { + return hasPermission(context, permission.READ_PHONE_STATE); + } + public static boolean hasContactsReadPermissions(Context context) { return hasPermission(context, permission.READ_CONTACTS); } diff --git a/java/com/android/dialer/util/ViewUtil.java b/java/com/android/dialer/util/ViewUtil.java index 81a32f985..211b3ed12 100644 --- a/java/com/android/dialer/util/ViewUtil.java +++ b/java/com/android/dialer/util/ViewUtil.java @@ -19,16 +19,19 @@ package com.android.dialer.util; import android.content.ContentResolver; import android.content.Context; import android.graphics.Paint; +import android.graphics.Point; import android.os.PowerManager; import android.provider.Settings; import android.provider.Settings.Global; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.TypedValue; +import android.view.Display; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.ViewTreeObserver.OnPreDrawListener; +import android.view.WindowManager; import android.widget.TextView; import java.util.Locale; @@ -139,4 +142,45 @@ public class ViewUtil { return Settings.Global.getFloat(contentResolver, Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0 || powerManager.isPowerSaveMode(); } + + /** + * Get navigation bar height by calculating difference between app usable size and real screen + * size. Note that this won't work in multi-window mode so it's caller's responsibility to check + * if the app is in multi-window mode before using this. + * + * @param context Context + * @return Navigation bar height + */ + public static int getNavigationBarHeight(Context context) { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = windowManager.getDefaultDisplay(); + Point appUsableSize = getAppUsableScreenSize(display); + Point realScreenSize = getRealScreenSize(display); + + // Navigation bar on the right. + if (appUsableSize.x < realScreenSize.x) { + return appUsableSize.y; + } + + // Navigation bar at the bottom. + if (appUsableSize.y < realScreenSize.y) { + return realScreenSize.y - appUsableSize.y; + } + + // Navigation bar is not present. + return 0; + } + + private static Point getAppUsableScreenSize(Display display) { + Point size = new Point(); + display.getSize(size); + return size; + } + + private static Point getRealScreenSize(Display display) { + Point size = new Point(); + display.getRealSize(size); + + return size; + } } diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java index d50be1ab6..ca0b5dcb0 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java @@ -21,22 +21,25 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.android.dialer.common.LogUtil; +import com.android.dialer.time.Clock; /** {@link RecyclerView.Adapter} for the new voicemail call log fragment. */ final class NewVoicemailAdapter extends RecyclerView.Adapter<NewVoicemailViewHolder> { private final Cursor cursor; + private final Clock clock; /** @param cursor whose projection is {@link VoicemailCursorLoader.VOICEMAIL_COLUMNS} */ - NewVoicemailAdapter(Cursor cursor) { + NewVoicemailAdapter(Cursor cursor, Clock clock) { this.cursor = cursor; + this.clock = clock; } @Override public NewVoicemailViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); View view = inflater.inflate(R.layout.new_voicemail_entry, viewGroup, false); - NewVoicemailViewHolder newVoicemailViewHolder = new NewVoicemailViewHolder(view); + NewVoicemailViewHolder newVoicemailViewHolder = new NewVoicemailViewHolder(view, clock); return newVoicemailViewHolder; } diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java b/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java index 1912e1e3f..9c1fd8b85 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java @@ -54,7 +54,7 @@ public final class NewVoicemailFragment extends Fragment implements LoaderCallba public void onLoadFinished(Loader<Cursor> loader, Cursor data) { LogUtil.i("NewVoicemailFragment.onCreateLoader", "cursor size is %d", data.getCount()); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(new NewVoicemailAdapter(data)); + recyclerView.setAdapter(new NewVoicemailAdapter(data, System::currentTimeMillis)); } @Override diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java index daa24c86c..8016563ce 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java @@ -19,11 +19,13 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; import android.view.View; import android.widget.QuickContactBadge; import android.widget.TextView; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.lettertile.LetterTileDrawable; +import com.android.dialer.time.Clock; import com.android.dialer.voicemail.model.VoicemailEntry; /** {@link RecyclerView.ViewHolder} for the new voicemail tab. */ @@ -31,18 +33,37 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder { private final Context context; private final TextView primaryTextView; + private final TextView secondaryTextView; + private final TextView transcriptionTextView; private final QuickContactBadge quickContactBadge; + private final Clock clock; - NewVoicemailViewHolder(View view) { + NewVoicemailViewHolder(View view, Clock clock) { super(view); this.context = view.getContext(); - primaryTextView = (TextView) view.findViewById(R.id.primary_text); + primaryTextView = view.findViewById(R.id.primary_text); + secondaryTextView = view.findViewById(R.id.secondary_text); + transcriptionTextView = view.findViewById(R.id.transcription_text); quickContactBadge = view.findViewById(R.id.quick_contact_photo); + this.clock = clock; } void bind(Cursor cursor) { VoicemailEntry voicemailEntry = VoicemailCursorLoader.toVoicemailEntry(cursor); primaryTextView.setText(VoicemailEntryText.buildPrimaryVoicemailText(context, voicemailEntry)); + secondaryTextView.setText( + VoicemailEntryText.buildSecondaryVoicemailText(context, clock, voicemailEntry)); + + String voicemailTranscription = voicemailEntry.transcription(); + + if (TextUtils.isEmpty(voicemailTranscription)) { + transcriptionTextView.setVisibility(View.GONE); + transcriptionTextView.setText(null); + } else { + transcriptionTextView.setVisibility(View.VISIBLE); + transcriptionTextView.setText(voicemailTranscription); + } + setPhoto(voicemailEntry); } diff --git a/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java b/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java index 5a4176542..e371e5ebf 100644 --- a/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java +++ b/java/com/android/dialer/voicemail/listui/VoicemailCursorLoader.java @@ -39,8 +39,10 @@ final class VoicemailCursorLoader extends CursorLoader { AnnotatedCallLog.PHOTO_URI, AnnotatedCallLog.PHOTO_ID, AnnotatedCallLog.LOOKUP_URI, + AnnotatedCallLog.DURATION, AnnotatedCallLog.GEOCODED_LOCATION, - AnnotatedCallLog.CALL_TYPE + AnnotatedCallLog.CALL_TYPE, + AnnotatedCallLog.TRANSCRIPTION }; // Indexes for VOICEMAIL_COLUMNS @@ -52,8 +54,10 @@ final class VoicemailCursorLoader extends CursorLoader { private static final int PHOTO_URI = 5; private static final int PHOTO_ID = 6; private static final int LOOKUP_URI = 7; - private static final int GEOCODED_LOCATION = 8; - private static final int CALL_TYPE = 9; + private static final int DURATION = 8; + private static final int GEOCODED_LOCATION = 9; + private static final int CALL_TYPE = 10; + private static final int TRANSCRIPTION = 11; // TODO(zachh): Optimize indexes VoicemailCursorLoader(Context context) { @@ -84,6 +88,8 @@ final class VoicemailCursorLoader extends CursorLoader { .setPhotoUri(cursor.getString(PHOTO_URI)) .setPhotoId(cursor.getLong(PHOTO_ID)) .setLookupUri(cursor.getString(LOOKUP_URI)) + .setDuration(cursor.getLong(DURATION)) + .setTranscription(cursor.getString(TRANSCRIPTION)) .setGeocodedLocation(cursor.getString(GEOCODED_LOCATION)) .setCallType(cursor.getInt(CALL_TYPE)) .build(); diff --git a/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java b/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java index cf2fef253..f59220105 100644 --- a/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java +++ b/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java @@ -18,7 +18,11 @@ package com.android.dialer.voicemail.listui; import android.content.Context; import android.text.TextUtils; +import com.android.dialer.calllogutils.CallLogDates; +import com.android.dialer.common.LogUtil; +import com.android.dialer.time.Clock; import com.android.dialer.voicemail.model.VoicemailEntry; +import java.util.concurrent.TimeUnit; /** * Computes the primary text for voicemail entries. @@ -39,4 +43,66 @@ public class VoicemailEntryText { } return primaryText.toString(); } + + /** + * Uses the new date and location formatting rules to format the location and date in the new + * voicemail tab. + * + * <p>Rules: $Location • Date + * + * <p>Examples: + * + * <p>Jun 20 San Francisco • Now + * + * <p>Markham, ON • Jul 27 + * + * <p>Toledo, OH • 12:15 PM + * + * <p>Date rules: if < 1 minute ago: "Now"; else if today: HH:MM(am|pm); else if < 3 days: day; + * else: MON D * + * + * @return $Location • Date + */ + public static String buildSecondaryVoicemailText( + Context context, Clock clock, VoicemailEntry voicemailEntry) { + return secondaryTextPrefix(context, clock, voicemailEntry); + } + + private static String secondaryTextPrefix( + Context context, Clock clock, VoicemailEntry voicemailEntry) { + StringBuilder secondaryText = new StringBuilder(); + String location = voicemailEntry.geocodedLocation(); + if (!TextUtils.isEmpty(location)) { + secondaryText.append(location); + } + if (secondaryText.length() > 0) { + secondaryText.append(" • "); + } + secondaryText.append( + CallLogDates.newCallLogTimestampLabel( + context, clock.currentTimeMillis(), voicemailEntry.timestamp())); + + long duration = voicemailEntry.duration(); + if (duration >= 0) { + secondaryText.append(" • "); + String formattedDuration = getVoicemailDuration(context, voicemailEntry); + secondaryText.append(formattedDuration); + } + return secondaryText.toString(); + } + + private static String getVoicemailDuration(Context context, VoicemailEntry voicemailEntry) { + long minutes = TimeUnit.SECONDS.toMinutes(voicemailEntry.duration()); + long seconds = voicemailEntry.duration() - TimeUnit.MINUTES.toSeconds(minutes); + + // The format for duration is "MM:SS" and we never expect the duration to be > 5 minutes + // However an incorrect duration could be set by the framework/someone to be >99, and in that + // case cap it at 99, for the UI to still be able to display it in "MM:SS" format. + if (minutes > 99) { + LogUtil.w( + "VoicemailEntryText.getVoicemailDuration", "Duration was %d", voicemailEntry.duration()); + minutes = 99; + } + return context.getString(R.string.voicemailDurationFormat, minutes, seconds); + } } diff --git a/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml b/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml index bc1506751..fe009922e 100644 --- a/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml +++ b/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml @@ -72,6 +72,7 @@ android:layout_marginStart="@dimen/call_log_entry_photo_text_margin"/> </LinearLayout> + <!-- TODO(uabdullah): Fix text cropping issue --> <TextView android:id="@+id/transcription_text" style="@style/SecondaryText" diff --git a/java/com/android/dialer/voicemail/listui/res/values/strings.xml b/java/com/android/dialer/voicemail/listui/res/values/strings.xml index 39c368a5a..a7df0ce8f 100644 --- a/java/com/android/dialer/voicemail/listui/res/values/strings.xml +++ b/java/com/android/dialer/voicemail/listui/res/values/strings.xml @@ -14,7 +14,10 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<resources> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <!-- String used to display voicemails from unknown numbers in the voicemail tab. [CHAR LIMIT=30] --> <string name="voicemail_entry_unknown">Unknown</string> + + <!-- Format for duration of voicemails which are displayed when viewing voicemail. For example "01:22" --> + <string name="voicemailDurationFormat"><xliff:g example="10" id="minutes">%1$02d</xliff:g>:<xliff:g example="20" id="seconds">%2$02d</xliff:g></string> </resources>
\ No newline at end of file diff --git a/java/com/android/dialer/voicemail/model/VoicemailEntry.java b/java/com/android/dialer/voicemail/model/VoicemailEntry.java index 00e1757dc..fc548f18c 100644 --- a/java/com/android/dialer/voicemail/model/VoicemailEntry.java +++ b/java/com/android/dialer/voicemail/model/VoicemailEntry.java @@ -31,6 +31,7 @@ public abstract class VoicemailEntry { .setTimestamp(0) .setNumber(DialerPhoneNumber.getDefaultInstance()) .setPhotoId(0) + .setDuration(0) .setCallType(0); } @@ -58,6 +59,11 @@ public abstract class VoicemailEntry { @Nullable public abstract String geocodedLocation(); + public abstract long duration(); + + @Nullable + public abstract String transcription(); + public abstract int callType(); /** Builder for {@link VoicemailEntry}. */ @@ -80,6 +86,10 @@ public abstract class VoicemailEntry { public abstract Builder setLookupUri(@Nullable String lookupUri); + public abstract Builder setDuration(long duration); + + public abstract Builder setTranscription(@Nullable String transcription); + public abstract Builder setGeocodedLocation(@Nullable String geocodedLocation); public abstract Builder setCallType(int callType); diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java index aa17dc4eb..aa96c4fe5 100644 --- a/java/com/android/incallui/CallButtonPresenter.java +++ b/java/com/android/incallui/CallButtonPresenter.java @@ -456,7 +456,7 @@ public class CallButtonPresenter final boolean hasCameraPermission = isVideo && VideoUtils.hasCameraPermissionAndShownPrivacyToast(mContext); - // Disabling local video doesn't seem to work when dialing. See b/30256571. + // Disabling local video doesn't seem to work when dialing. See a bug. final boolean showPauseVideo = isVideo && call.getState() != DialerCall.State.DIALING @@ -500,7 +500,7 @@ public class CallButtonPresenter * @return {@code true} if downgrading to an audio-only call from a video call is supported. */ private boolean isDowngradeToAudioSupported(DialerCall call) { - // TODO(b/33676907): If there is an RCS video share session, return true here + // TODO(a bug): If there is an RCS video share session, return true here return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO); } diff --git a/java/com/android/incallui/CallerInfo.java b/java/com/android/incallui/CallerInfo.java index 809ed594c..4a9cf2181 100644 --- a/java/com/android/incallui/CallerInfo.java +++ b/java/com/android/incallui/CallerInfo.java @@ -510,7 +510,7 @@ public class CallerInfo { Log.e(TAG, "Cannot access VoiceMail.", se); } // TODO: There is no voicemail picture? - // FIXME: FIND ANOTHER ICON + // photoResource = android.R.drawable.badge_voicemail; return this; } diff --git a/java/com/android/incallui/CallerInfoAsyncQuery.java b/java/com/android/incallui/CallerInfoAsyncQuery.java index 8fc9c4f14..f9d8da819 100644 --- a/java/com/android/incallui/CallerInfoAsyncQuery.java +++ b/java/com/android/incallui/CallerInfoAsyncQuery.java @@ -103,7 +103,7 @@ public class CallerInfoAsyncQuery { public void onQueryComplete(int token, Object cookie, CallerInfo ci) { Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete"); // If there are no other directory queries, make sure that the listener is - // notified of this result. see b/27621628 + // notified of this result. see a bug if ((ci != null && ci.contactExists) || !startOtherDirectoriesQuery(token, context, info, listener, cookie)) { if (listener != null && ci != null) { @@ -206,7 +206,7 @@ public class CallerInfoAsyncQuery { // The current implementation of multiple async query runs in single handler thread // in AsyncQueryHandler. // intermediateListener.onQueryComplete is also called from the same caller thread. - // TODO(b/26019872): use thread pool instead of single thread. + // TODO(a bug): use thread pool instead of single thread. for (int i = 0; i < size; i++) { long directoryId = directoryIds[i]; Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId); diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java index 2e3d721df..c3a68c021 100644 --- a/java/com/android/incallui/InCallActivity.java +++ b/java/com/android/incallui/InCallActivity.java @@ -516,6 +516,14 @@ public class InCallActivity extends TransactionSafeFragmentActivity common.showInternationalCallOnWifiDialog(call); } + @Override + public void onMultiWindowModeChanged(boolean isInMultiWindowMode) { + super.onMultiWindowModeChanged(isInMultiWindowMode); + if (!isInMultiWindowMode) { + common.updateNavigationBar(isDialpadVisible()); + } + } + public void setAllowOrientationChange(boolean allowOrientationChange) { if (this.allowOrientationChange == allowOrientationChange) { return; diff --git a/java/com/android/incallui/InCallActivityCommon.java b/java/com/android/incallui/InCallActivityCommon.java index 0a7c2689b..d39ce22b0 100644 --- a/java/com/android/incallui/InCallActivityCommon.java +++ b/java/com/android/incallui/InCallActivityCommon.java @@ -305,6 +305,7 @@ public class InCallActivityCommon { } showDialpadRequest = DIALPAD_REQUEST_NONE; } + updateNavigationBar(isDialpadVisible()); if (showPostCharWaitDialogOnResume) { showPostCharWaitDialog(showPostCharWaitDialogCallId, showPostCharWaitDialogChars); @@ -328,7 +329,7 @@ public class InCallActivityCommon { public void onStop() { // Disconnects call waiting for account when activity is hidden e.g. user press home button. // This is necessary otherwise the pending call will stuck on account choose and no new call - // will be able to create. See b/63600434 for more details. + // will be able to create. See a bug for more details. // Skip this on locked screen since the activity may go over life cycle and start again. if (!isRecreating && !inCallActivity.getSystemService(KeyguardManager.class).isKeyguardLocked()) { @@ -712,6 +713,16 @@ public class InCallActivityCommon { dialog.show(); } + void updateNavigationBar(boolean isDialpadVisible) { + if (!inCallActivity.isInMultiWindowMode()) { + View navigationBarBackground = + inCallActivity.getWindow().findViewById(R.id.navigation_bar_background); + if (navigationBarBackground != null) { + navigationBarBackground.setVisibility(isDialpadVisible ? View.VISIBLE : View.GONE); + } + } + } + public boolean showDialpadFragment(boolean show, boolean animate) { // If the dialpad is already visible, don't animate in. If it's gone, don't animate out. boolean isDialpadVisible = isDialpadVisible(); @@ -772,6 +783,7 @@ public class InCallActivityCommon { dialpadFragmentManager.executePendingTransactions(); Logger.get(inCallActivity).logScreenView(ScreenEvent.Type.INCALL_DIALPAD, inCallActivity); + updateNavigationBar(true /* isDialpadVisible */); } private void performHideDialpadFragment() { @@ -789,6 +801,7 @@ public class InCallActivityCommon { transaction.commitAllowingStateLoss(); fragmentManager.executePendingTransactions(); } + updateNavigationBar(false /* isDialpadVisible */); } public boolean isDialpadVisible() { diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java index 1ba3b5d3a..b8a2baa00 100644 --- a/java/com/android/incallui/InCallPresenter.java +++ b/java/com/android/incallui/InCallPresenter.java @@ -452,6 +452,7 @@ public class InCallPresenter implements CallList.Listener { if (inCallActivity != null) { if (mInCallActivity == null) { + mContext = inCallActivity.getApplicationContext(); updateListeners = true; LogUtil.i("InCallPresenter.updateActivity", "UI Initialized"); } else { @@ -500,7 +501,7 @@ public class InCallPresenter implements CallList.Listener { // (2) All calls could disconnect and then get a new incoming call before the // activity is destroyed. // - // b/1122139 - We previously had a check for mServiceConnected here as well, but there are + // a bug - We previously had a check for mServiceConnected here as well, but there are // cases where we need to recalculate the current state even if the service in not // connected. In particular the case where startOrFinish() is called while the app is // already finish()ing. In that case, we skip updating the state with the knowledge that @@ -730,7 +731,7 @@ public class InCallPresenter implements CallList.Listener { // incall activity for that call will still exist (even if it's not visible). In the case of // an incoming call in that situation, just disconnect that "waiting for account" call and // dismiss the dialog. The same activity will be reused to handle the new incoming call. See - // b/33247755 for more details. + // a bug for more details. DialerCall waitingForAccountCall; if (newState == InCallState.INCOMING && (waitingForAccountCall = callList.getWaitingForAccountCall()) != null) { diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java index 2c45cb375..402e0021f 100644 --- a/java/com/android/incallui/InCallServiceImpl.java +++ b/java/com/android/incallui/InCallServiceImpl.java @@ -38,6 +38,7 @@ import com.android.incallui.call.TelecomAdapter; public class InCallServiceImpl extends InCallService { private ReturnToCallController returnToCallController; + private NewReturnToCallController newReturnToCallController; @Override public void onCallAudioStateChanged(CallAudioState audioState) { @@ -97,6 +98,9 @@ public class InCallServiceImpl extends InCallService { if (ReturnToCallController.isEnabled(this)) { returnToCallController = new ReturnToCallController(this); } + if (NewReturnToCallController.isEnabled(this)) { + newReturnToCallController = new NewReturnToCallController(this); + } IBinder iBinder = super.onBind(intent); Trace.endSection(); @@ -125,6 +129,10 @@ public class InCallServiceImpl extends InCallService { returnToCallController.tearDown(); returnToCallController = null; } + if (newReturnToCallController != null) { + newReturnToCallController.tearDown(); + newReturnToCallController = null; + } Trace.endSection(); } } diff --git a/java/com/android/incallui/NewReturnToCallController.java b/java/com/android/incallui/NewReturnToCallController.java new file mode 100644 index 000000000..cd69ea1be --- /dev/null +++ b/java/com/android/incallui/NewReturnToCallController.java @@ -0,0 +1,283 @@ +/* + * 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; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.telecom.CallAudioState; +import com.android.dialer.common.LogUtil; +import com.android.dialer.configprovider.ConfigProviderBindings; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; +import com.android.dialer.telecom.TelecomUtil; +import com.android.incallui.InCallPresenter.InCallUiListener; +import com.android.incallui.audiomode.AudioModeProvider; +import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener; +import com.android.incallui.call.CallList; +import com.android.incallui.call.CallList.Listener; +import com.android.incallui.call.DialerCall; +import com.android.incallui.speakerbuttonlogic.SpeakerButtonInfo; +import com.android.incallui.speakerbuttonlogic.SpeakerButtonInfo.IconSize; +import com.android.newbubble.NewBubble; +import com.android.newbubble.NewBubble.BubbleExpansionStateListener; +import com.android.newbubble.NewBubble.ExpansionState; +import com.android.newbubble.NewBubbleInfo; +import com.android.newbubble.NewBubbleInfo.Action; +import java.util.ArrayList; +import java.util.List; + +/** + * Listens for events relevant to the return-to-call bubble and updates the bubble's state as + * necessary + */ +public class NewReturnToCallController implements InCallUiListener, Listener, AudioModeListener { + + public static final String RETURN_TO_CALL_EXTRA_KEY = "RETURN_TO_CALL_BUBBLE"; + + private final Context context; + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + NewBubble bubble; + + private CallAudioState audioState; + + private final PendingIntent toggleSpeaker; + private final PendingIntent showSpeakerSelect; + private final PendingIntent toggleMute; + private final PendingIntent endCall; + private final PendingIntent fullScreen; + + public static boolean isEnabled(Context context) { + return ConfigProviderBindings.get(context).getBoolean("enable_return_to_call_bubble_v2", false); + } + + public NewReturnToCallController(Context context) { + this.context = context; + + toggleSpeaker = createActionIntent(ReturnToCallActionReceiver.ACTION_TOGGLE_SPEAKER); + showSpeakerSelect = + createActionIntent(ReturnToCallActionReceiver.ACTION_SHOW_AUDIO_ROUTE_SELECTOR); + toggleMute = createActionIntent(ReturnToCallActionReceiver.ACTION_TOGGLE_MUTE); + endCall = createActionIntent(ReturnToCallActionReceiver.ACTION_END_CALL); + + Intent activityIntent = InCallActivity.getIntent(context, false, false, false); + activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activityIntent.putExtra(RETURN_TO_CALL_EXTRA_KEY, true); + fullScreen = + PendingIntent.getActivity( + context, InCallActivity.PENDING_INTENT_REQUEST_CODE_BUBBLE, activityIntent, 0); + + InCallPresenter.getInstance().addInCallUiListener(this); + CallList.getInstance().addListener(this); + AudioModeProvider.getInstance().addListener(this); + audioState = AudioModeProvider.getInstance().getAudioState(); + } + + public void tearDown() { + InCallPresenter.getInstance().removeInCallUiListener(this); + CallList.getInstance().removeListener(this); + AudioModeProvider.getInstance().removeListener(this); + } + + @Override + public void onUiShowing(boolean showing) { + if (showing) { + hide(); + } else { + if (TelecomUtil.isInManagedCall(context)) { + show(); + } + } + } + + private void hide() { + if (bubble != null) { + bubble.hide(); + } else { + LogUtil.i("ReturnToCallController.hide", "hide() called without calling show()"); + } + } + + private void hideAndReset() { + if (bubble != null) { + bubble.hideAndReset(); + } else { + LogUtil.i("ReturnToCallController.reset", "reset() called without calling show()"); + } + } + + private void show() { + if (bubble == null) { + bubble = startBubble(); + } else { + bubble.show(); + } + } + + @VisibleForTesting + public NewBubble startBubble() { + if (!NewBubble.canShowBubbles(context)) { + LogUtil.i("ReturnToCallController.startNewBubble", "can't show bubble, no permission"); + return null; + } + NewBubble returnToCallBubble = NewBubble.createBubble(context, generateBubbleInfo()); + returnToCallBubble.setBubbleExpansionStateListener( + new BubbleExpansionStateListener() { + @Override + public void onBubbleExpansionStateChanged( + @ExpansionState int expansionState, boolean isUserAction) { + if (!isUserAction) { + return; + } + + DialerCall call = CallList.getInstance().getActiveOrBackgroundCall(); + switch (expansionState) { + case ExpansionState.START_EXPANDING: + if (call != null) { + Logger.get(context) + .logCallImpression( + DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND, + call.getUniqueCallId(), + call.getTimeAddedMs()); + } else { + Logger.get(context) + .logImpression(DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND); + } + break; + case ExpansionState.START_COLLAPSING: + if (call != null) { + Logger.get(context) + .logCallImpression( + DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER, + call.getUniqueCallId(), + call.getTimeAddedMs()); + } else { + Logger.get(context).logImpression(DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER); + } + break; + default: + break; + } + } + }); + returnToCallBubble.show(); + return returnToCallBubble; + } + + @Override + public void onIncomingCall(DialerCall call) {} + + @Override + public void onUpgradeToVideo(DialerCall call) {} + + @Override + public void onSessionModificationStateChange(DialerCall call) {} + + @Override + public void onCallListChange(CallList callList) {} + + @Override + public void onDisconnect(DialerCall call) { + if (call.wasParentCall()) { + // It's disconnected after the last child call is disconnected, and we already did everything + // for the last child. + LogUtil.i( + "ReturnToCallController.onDisconnect", "being called for a parent call and do nothing"); + return; + } + if (bubble != null + && bubble.isVisible() + && (!TelecomUtil.isInManagedCall(context) + || CallList.getInstance().getActiveOrBackgroundCall() != null)) { + bubble.showText(context.getText(R.string.incall_call_ended)); + } + // For conference call, we should hideAndReset for the last disconnected child call while the + // parent call is still there. + if (!CallList.getInstance().hasNonParentActiveOrBackgroundCall()) { + hideAndReset(); + } + } + + @Override + public void onWiFiToLteHandover(DialerCall call) {} + + @Override + public void onHandoverToWifiFailed(DialerCall call) {} + + @Override + public void onInternationalCallOnWifi(@NonNull DialerCall call) {} + + @Override + public void onAudioStateChanged(CallAudioState audioState) { + this.audioState = audioState; + if (bubble != null) { + bubble.updateActions(generateActions()); + } + } + + private NewBubbleInfo generateBubbleInfo() { + return NewBubbleInfo.builder() + .setPrimaryColor(context.getResources().getColor(R.color.dialer_theme_color, null)) + .setPrimaryIcon(Icon.createWithResource(context, R.drawable.on_going_call)) + .setStartingYPosition( + context.getResources().getDimensionPixelOffset(R.dimen.return_to_call_initial_offset_y)) + .setActions(generateActions()) + .build(); + } + + @NonNull + private List<Action> generateActions() { + List<Action> actions = new ArrayList<>(); + SpeakerButtonInfo speakerButtonInfo = new SpeakerButtonInfo(audioState, IconSize.SIZE_24_DP); + + actions.add( + Action.builder() + .setIconDrawable(context.getDrawable(R.drawable.quantum_ic_fullscreen_vd_theme_24)) + .setIntent(fullScreen) + .build()); + actions.add( + Action.builder() + .setIconDrawable(context.getDrawable(R.drawable.quantum_ic_mic_off_white_24)) + .setChecked(audioState.isMuted()) + .setIntent(toggleMute) + .build()); + actions.add( + Action.builder() + .setIconDrawable(context.getDrawable(speakerButtonInfo.icon)) + .setName(context.getText(speakerButtonInfo.label)) + .setChecked(speakerButtonInfo.isChecked) + .setIntent(speakerButtonInfo.checkable ? toggleSpeaker : showSpeakerSelect) + .build()); + actions.add( + Action.builder() + .setIconDrawable(context.getDrawable(R.drawable.quantum_ic_call_end_vd_theme_24)) + .setIntent(endCall) + .build()); + return actions; + } + + @NonNull + private PendingIntent createActionIntent(String action) { + Intent toggleSpeaker = new Intent(context, ReturnToCallActionReceiver.class); + toggleSpeaker.setAction(action); + return PendingIntent.getBroadcast(context, 0, toggleSpeaker, 0); + } +} diff --git a/java/com/android/incallui/StatusBarNotifier.java b/java/com/android/incallui/StatusBarNotifier.java index 7ff0040e2..4ce43935e 100644 --- a/java/com/android/incallui/StatusBarNotifier.java +++ b/java/com/android/incallui/StatusBarNotifier.java @@ -376,8 +376,6 @@ public class StatusBarNotifier builder.setColorized(true); builder.setChannelId(NotificationChannelId.ONGOING_CALL); } - // This will be ignored on O+ and handled by the channel - builder.setPriority(Notification.PRIORITY_MAX); break; default: break; @@ -648,7 +646,8 @@ public class StatusBarNotifier return R.drawable.ic_hd_call; } // If ReturnToCall is enabled, use the static icon. The animated one will show in the bubble. - if (ReturnToCallController.isEnabled(mContext)) { + if (ReturnToCallController.isEnabled(mContext) + || NewReturnToCallController.isEnabled(mContext)) { return R.drawable.quantum_ic_call_vd_theme_24; } else { return R.drawable.on_going_call; @@ -714,7 +713,7 @@ public class StatusBarNotifier if (resId == R.string.notification_incoming_call_wifi_template || resId == R.string.notification_ongoing_call_wifi_template) { - // TODO(b/64525903): Potentially apply this template logic everywhere. + // TODO(a bug): Potentially apply this template logic everywhere. return mContext.getString(resId, wifiBrand); } @@ -1027,11 +1026,6 @@ public class StatusBarNotifier @Override public void onAudioStateChanged(CallAudioState audioState) { - if (CallList.getInstance().getActiveOrBackgroundCall() == null) { - // We only care about speaker mode when in call - return; - } - updateNotification(); } diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java index ab02c3b86..fd775e2f5 100644 --- a/java/com/android/incallui/VideoCallPresenter.java +++ b/java/com/android/incallui/VideoCallPresenter.java @@ -358,7 +358,7 @@ public class VideoCallPresenter // Ensure that the call's camera direction is updated (most likely to UNKNOWN). Normally this // happens after any call state changes but we're unregistering from InCallPresenter above so - // we won't get any more call state changes. See b/32957114. + // we won't get any more call state changes. See a bug. if (mPrimaryCall != null) { updateCameraSelection(mPrimaryCall); } @@ -851,7 +851,7 @@ public class VideoCallPresenter if (!hasCameraPermission) { videoCall.setCamera(null); mPreviewSurfaceState = PreviewSurfaceState.NONE; - // TODO(wangqi): Inform remote party that the video is off. This is similar to b/30256571. + // TODO(wangqi): Inform remote party that the video is off. This is similar to a bug. } else if (isCameraRequired) { InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager(); videoCall.setCamera(cameraManager.getActiveCameraId()); diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java index ea5956c8c..ebbaef8e5 100644 --- a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java +++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java @@ -421,9 +421,7 @@ public class FlingUpDownMethod extends AnswerMethod implements OnProgressChanged // spec timeline can be divided into 9 slots. Each slot is equivalent to 83ms in the spec. // Therefore, we use 9 slots of 83ms to map user gesture into the spec timeline. // - // See specs - - // Accept: https://direct.googleplex.com/#/spec/8510001 - // Decline: https://direct.googleplex.com/#/spec/3850001 + final float progressSlots = 9; // Fade out the "swipe up to answer". It only takes 1 slot to complete the fade. diff --git a/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml b/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml index 2dc274b05..d79da8843 100644 --- a/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml +++ b/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml @@ -87,7 +87,7 @@ android:layout_marginEnd="24dp"/> <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses. - b/31396406 --> + a bug --> <com.android.incallui.autoresizetext.AutoResizeTextView android:id="@id/contactgrid_contact_name" android:layout_width="wrap_content" diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java index 113144b7f..50f6ae695 100644 --- a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java +++ b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java @@ -90,8 +90,8 @@ public class AnswerProximitySensor } else { // TODO(twyen): choose a wake lock implementation base on framework/device. // These bugs requires the PseudoProximityWakeLock workaround: - // b/30439151 Proximity sensor not working on M - // b/31499931 fautly touch input when screen is off on marlin/sailfish + // a bug Proximity sensor not working on M + // a bug fautly touch input when screen is off on marlin/sailfish answerProximityWakeLock = new SystemProximityWakeLock(context); } answerProximityWakeLock.setScreenOnListener(this); diff --git a/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java b/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java index eda0ee720..676a1a3cc 100644 --- a/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java +++ b/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java @@ -27,7 +27,7 @@ import java.util.Set; * new DOWN event once the point started moving and then behave as a normal gesture. To prevent * accidental answer/rejects, touches that started when the screen is off should be ignored. * - * <p>b/31499931 on certain devices with N-DR1, if the screen is already touched when the screen is + * <p>a bug on certain devices with N-DR1, if the screen is already touched when the screen is * turned on, a "DOWN MOVE UP" will be sent for each movement before the touch is actually released. * These events is hard to discern from other normal events, and keeping the screen on reduces its' * probability. diff --git a/java/com/android/incallui/audiomode/AudioModeProvider.java b/java/com/android/incallui/audiomode/AudioModeProvider.java index eb59e95d4..6bdd23968 100644 --- a/java/com/android/incallui/audiomode/AudioModeProvider.java +++ b/java/com/android/incallui/audiomode/AudioModeProvider.java @@ -69,7 +69,7 @@ public class AudioModeProvider { * Sets a approximated audio state before {@link #onAudioStateChanged} is called. Classes such as * {@link com.android.incallui.ProximitySensor} fetches the audio state before it is updated by * telecom. This method attempts to guess the correct routing based on connected audio devices. - * The audio state may still be wrong on a second call due to b/64811128, telecom setting the + * The audio state may still be wrong on a second call due to a bug, telecom setting the * route back to earpiece when a call ends. */ public void initializeAudioState(Context context) { diff --git a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java index 9fd3aed5b..d481f4339 100644 --- a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java +++ b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java @@ -92,7 +92,7 @@ public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment CallAudioState.ROUTE_EARPIECE, audioState); - // TODO(b/67013452): set peak height correctly to fully expand it in landscape mode. + // TODO(a bug): set peak height correctly to fully expand it in landscape mode. return view; } diff --git a/java/com/android/incallui/autoresizetext/AutoResizeTextView.java b/java/com/android/incallui/autoresizetext/AutoResizeTextView.java index 5a22b93dc..2789ceac2 100644 --- a/java/com/android/incallui/autoresizetext/AutoResizeTextView.java +++ b/java/com/android/incallui/autoresizetext/AutoResizeTextView.java @@ -35,7 +35,7 @@ import javax.annotation.Nullable; * A TextView that automatically scales its text to completely fill its allotted width. * * <p>Note: In some edge cases, the binary search algorithm to find the best fit may slightly - * overshoot / undershoot its constraints. See b/26704434. No minimal repro case has been + * overshoot / undershoot its constraints. See a bug. No minimal repro case has been * found yet. A known workaround is the solution provided on StackOverflow: * http://stackoverflow.com/a/5535672 */ diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java index fcfb0a663..59f38349a 100644 --- a/java/com/android/incallui/call/CallList.java +++ b/java/com/android/incallui/call/CallList.java @@ -298,7 +298,7 @@ public class CallList implements DialerCallDelegate { manager.unregisterStateChangedListener(call); // Don't log an already logged call. logCall() might be called multiple times - // for the same call due to b/24109437. + // for the same call due to a bug. if (call.getLogState() != null && !call.getLogState().isLogged) { getLegacyBindings(context).logCall(call); call.getLogState().isLogged = true; @@ -344,7 +344,7 @@ public class CallList implements DialerCallDelegate { DialerCall call = mCallByTelecomCall.get(telecomCall); // Don't log an already logged call. logCall() might be called multiple times - // for the same call due to b/24109437. + // for the same call due to a bug. if (call.getLogState() != null && !call.getLogState().isLogged) { getLegacyBindings(context).logCall(call); call.getLogState().isLogged = true; diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java index d911a4c4f..e8523d650 100644 --- a/java/com/android/incallui/call/DialerCall.java +++ b/java/com/android/incallui/call/DialerCall.java @@ -94,7 +94,7 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2; // Hard coded property for {@code Call}. Upstreamed change from Motorola. - // TODO(b/35359461): Move it to Telecom in framework. + // TODO(a bug): Move it to Telecom in framework. public static final int PROPERTY_CODEC_KNOWN = 0x04000000; private static final String ID_PREFIX = "DialerCall_"; @@ -143,9 +143,12 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa @CallHistoryStatus private int mCallHistoryStatus = CALL_HISTORY_STATUS_UNKNOWN; private boolean mIsSpam; private boolean mIsBlocked; - private boolean isInUserSpamList; - private boolean isInUserWhiteList; - private boolean isInGlobalSpamList; + + @Nullable private Boolean isInUserSpamList; + + @Nullable private Boolean isInUserWhiteList; + + @Nullable private Boolean isInGlobalSpamList; private boolean didShowCameraPermission; private String callProviderLabel; private String callbackNumber; @@ -575,7 +578,7 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa */ protected boolean areCallExtrasCorrupted(Bundle callExtras) { /** - * There's currently a bug in Telephony service (b/25613098) that could corrupt the extras + * There's currently a bug in Telephony service (a bug) that could corrupt the extras * bundle, resulting in a IllegalArgumentException while validating data under {@link * Bundle#containsKey(String)}. */ @@ -1015,7 +1018,8 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa didShowCameraPermission = didShow; } - public boolean isInGlobalSpamList() { + @Nullable + public Boolean isInGlobalSpamList() { return isInGlobalSpamList; } @@ -1023,7 +1027,8 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa isInGlobalSpamList = inSpamList; } - public boolean isInUserSpamList() { + @Nullable + public Boolean isInUserSpamList() { return isInUserSpamList; } @@ -1031,7 +1036,8 @@ public class DialerCall implements VideoTechListener, StateChangedListener, Capa isInUserSpamList = inSpamList; } - public boolean isInUserWhiteList() { + @Nullable + public Boolean isInUserWhiteList() { return isInUserWhiteList; } diff --git a/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java index 305ab4377..0884c2fef 100644 --- a/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java +++ b/java/com/android/incallui/calllocation/impl/GoogleLocationSettingHelper.java @@ -30,7 +30,7 @@ import com.android.dialer.strictmode.StrictModeUtils; /** * 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 { diff --git a/java/com/android/incallui/calllocation/impl/HttpFetcher.java b/java/com/android/incallui/calllocation/impl/HttpFetcher.java index a63cbc6e1..10cc34d25 100644 --- a/java/com/android/incallui/calllocation/impl/HttpFetcher.java +++ b/java/com/android/incallui/calllocation/impl/HttpFetcher.java @@ -222,7 +222,7 @@ public class HttpFetcher { /** * Lookup up url re-write rules from gServices and apply to the given url. * - * <p>https://wiki.corp.google.com/twiki/bin/view/Main/AndroidGservices#URL_Rewriting_Rules + * * @return The new url. */ diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java index f0504bc56..e96060c06 100644 --- a/java/com/android/incallui/incall/impl/InCallFragment.java +++ b/java/com/android/incallui/incall/impl/InCallFragment.java @@ -45,6 +45,7 @@ import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; import com.android.dialer.multimedia.MultimediaData; import com.android.dialer.strictmode.StrictModeUtils; +import com.android.dialer.util.ViewUtil; import com.android.dialer.widget.LockableViewPager; import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment; import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter; @@ -174,6 +175,9 @@ public class InCallFragment extends Fragment : TelephonyManager.NETWORK_TYPE_UNKNOWN; } phoneType = getContext().getSystemService(TelephonyManager.class).getPhoneType(); + View space = view.findViewById(R.id.navigation_bar_background); + space.getLayoutParams().height = ViewUtil.getNavigationBarHeight(getContext()); + return view; } diff --git a/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml b/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml index 6d8556759..8e9120b09 100644 --- a/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml +++ b/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml @@ -1,5 +1,21 @@ <?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="#FF01579B" android:state_checked="true"/> + <item android:color="#FF1C3AA9" android:state_checked="true"/> <item android:color="#FFFFFFFF"/> </selector> diff --git a/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml b/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml index c06f7099c..73b9c267e 100644 --- a/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml +++ b/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml @@ -15,107 +15,114 @@ ~ limitations under the License --> <FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:clipChildren="false" - android:clipToPadding="false" - android:fitsSystemWindows="true"> + android:layout_height="match_parent"> - <LinearLayout - android:id="@id/incall_contact_grid" + <RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="12dp" - android:layout_marginStart="@dimen/incall_window_margin_horizontal" - android:layout_marginEnd="@dimen/incall_window_margin_horizontal" - android:gravity="center_horizontal" - android:orientation="vertical"> + android:layout_height="match_parent" + android:clipChildren="false" + android:clipToPadding="false" + android:fitsSystemWindows="true"> + + <LinearLayout + android:id="@id/incall_contact_grid" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:layout_marginStart="@dimen/incall_window_margin_horizontal" + android:layout_marginEnd="@dimen/incall_window_margin_horizontal" + android:gravity="center_horizontal" + android:orientation="vertical"> <ImageView - android:id="@id/contactgrid_avatar" - android:layout_width="@dimen/incall_avatar_size" - android:layout_height="@dimen/incall_avatar_size" - android:layout_marginBottom="8dp" - android:elevation="2dp"/> + android:id="@id/contactgrid_avatar" + android:layout_width="@dimen/incall_avatar_size" + android:layout_height="@dimen/incall_avatar_size" + android:layout_marginBottom="8dp" + android:elevation="2dp"/> <include - layout="@layout/incall_contactgrid_top_row" - android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + layout="@layout/incall_contactgrid_top_row" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses. - b/31396406 --> + a bug --> <com.android.incallui.autoresizetext.AutoResizeTextView - android:id="@id/contactgrid_contact_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="4dp" - android:singleLine="true" - android:textAppearance="@style/Dialer.Incall.TextAppearance.Large" - app:autoResizeText_minTextSize="28sp" - tools:text="Jake Peralta" - tools:ignore="Deprecated"/> + android:id="@id/contactgrid_contact_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:singleLine="true" + android:textAppearance="@style/Dialer.Incall.TextAppearance.Large" + app:autoResizeText_minTextSize="28sp" + tools:ignore="Deprecated" + tools:text="Jake Peralta"/> <include - layout="@layout/incall_contactgrid_bottom_row" - android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + layout="@layout/incall_contactgrid_bottom_row" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> <FrameLayout - android:id="@+id/incall_location_holder" - android:layout_width="match_parent" - android:layout_height="match_parent"/> + android:id="@+id/incall_location_holder" + android:layout_width="match_parent" + android:layout_height="match_parent"/> </LinearLayout> <com.android.dialer.widget.LockableViewPager - android:id="@+id/incall_pager" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_above="@+id/incall_paginator" - android:layout_below="@+id/incall_contact_grid" - android:layout_centerHorizontal="true"/> + android:id="@+id/incall_pager" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_above="@+id/incall_paginator" + android:layout_below="@+id/incall_contact_grid" + android:layout_centerHorizontal="true"/> <com.android.incallui.incall.impl.InCallPaginator android:id="@+id/incall_paginator" - android:layout_height="@dimen/paginator_height" android:layout_width="@dimen/paginator_width" + android:layout_height="@dimen/paginator_height" android:layout_above="@+id/incall_end_call" android:layout_centerHorizontal="true" android:visibility="gone"/> <FrameLayout - android:id="@+id/incall_dialpad_container" - style="@style/DialpadContainer" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_alignParentBottom="true" - android:clipChildren="false" - android:clipToPadding="false" - tools:background="@android:color/white" - tools:visibility="gone"/> + android:id="@+id/incall_dialpad_container" + style="@style/DialpadContainer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:clipChildren="false" + android:clipToPadding="false" + tools:background="@android:color/white" + tools:visibility="gone"/> <ImageButton - android:id="@+id/incall_end_call" - style="@style/Incall.Button.End" - android:layout_marginTop="16dp" - android:layout_marginBottom="36dp" - android:layout_alignParentBottom="true" - android:layout_centerHorizontal="true" - android:contentDescription="@string/incall_content_description_end_call"/> + android:id="@+id/incall_end_call" + style="@style/Incall.Button.End" + android:layout_marginTop="16dp" + android:layout_marginBottom="36dp" + android:layout_alignParentBottom="true" + android:layout_centerHorizontal="true" + android:contentDescription="@string/incall_content_description_end_call"/> </RelativeLayout> <FrameLayout - android:id="@id/incall_on_hold_banner" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="top"/> + android:id="@id/incall_on_hold_banner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="top"/> + <FrameLayout + android:id="@+id/navigation_bar_background" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_gravity="bottom" + android:visibility="gone" + android:background="@android:color/background_dark"/> </FrameLayout> diff --git a/java/com/android/incallui/incall/protocol/PrimaryCallState.java b/java/com/android/incallui/incall/protocol/PrimaryCallState.java index 4a50fbeaf..7a3abedcd 100644 --- a/java/com/android/incallui/incall/protocol/PrimaryCallState.java +++ b/java/com/android/incallui/incall/protocol/PrimaryCallState.java @@ -71,7 +71,7 @@ public class PrimaryCallState { @Nullable public final String customLabel; @Nullable public final TransformationInfo assistedDialingExtras; - // TODO: Convert to autovalue. b/34502119 + // TODO: Convert to autovalue. a bug public static PrimaryCallState createEmptyPrimaryCallState() { return createEmptyPrimaryCallStateWithState(DialerCall.State.IDLE, null); } diff --git a/java/com/android/incallui/incall/protocol/PrimaryInfo.java b/java/com/android/incallui/incall/protocol/PrimaryInfo.java index 69eee20ff..f7457c3ff 100644 --- a/java/com/android/incallui/incall/protocol/PrimaryInfo.java +++ b/java/com/android/incallui/incall/protocol/PrimaryInfo.java @@ -45,7 +45,7 @@ public class PrimaryInfo { public final boolean showInCallButtonGrid; public final int numberPresentation; - // TODO: Convert to autovalue. b/34502119 + // TODO: Convert to autovalue. a bug public static PrimaryInfo createEmptyPrimaryInfo() { return new PrimaryInfo( null, diff --git a/java/com/android/incallui/sessiondata/MultimediaFragment.java b/java/com/android/incallui/sessiondata/MultimediaFragment.java index 3e6cdbbe0..6c2490f92 100644 --- a/java/com/android/incallui/sessiondata/MultimediaFragment.java +++ b/java/com/android/incallui/sessiondata/MultimediaFragment.java @@ -182,7 +182,7 @@ public class MultimediaFragment extends Fragment implements AvatarPresenter { boolean isFirstResource) { view.findViewById(R.id.loading_spinner).setVisibility(View.GONE); LogUtil.e("MultimediaFragment.onLoadFailed", null, e); - // TODO(b/34720074) handle error cases nicely + // TODO(a bug) handle error cases nicely return false; // Let Glide handle the rest } diff --git a/java/com/android/incallui/spam/SpamNotificationActivity.java b/java/com/android/incallui/spam/SpamNotificationActivity.java index 86988ad23..f5ea73c73 100644 --- a/java/com/android/incallui/spam/SpamNotificationActivity.java +++ b/java/com/android/incallui/spam/SpamNotificationActivity.java @@ -274,7 +274,7 @@ public class SpamNotificationActivity extends FragmentActivity { logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER); filteredNumberAsyncQueryHandler.blockNumber(null, number, getCountryIso()); - // TODO: DialerCall finish() after block/reporting async tasks complete (b/28441936) + // TODO: DialerCall finish() after block/reporting async tasks complete (a bug) finish(); } @@ -289,7 +289,7 @@ public class SpamNotificationActivity extends FragmentActivity { CallLog.Calls.INCOMING_TYPE, ReportingLocation.Type.FEEDBACK_PROMPT, contactLookupResultType); - // TODO: DialerCall finish() after async task completes (b/28441936) + // TODO: DialerCall finish() after async task completes (a bug) finish(); } diff --git a/java/com/android/incallui/spam/SpamNotificationService.java b/java/com/android/incallui/spam/SpamNotificationService.java index 91377db83..3c1061af1 100644 --- a/java/com/android/incallui/spam/SpamNotificationService.java +++ b/java/com/android/incallui/spam/SpamNotificationService.java @@ -117,7 +117,7 @@ public class SpamNotificationService extends Service { break; default: // fall out } - // TODO: call stopSelf() after async tasks complete (b/28441936) + // TODO: call stopSelf() after async tasks complete (a bug) stopSelf(); return START_NOT_STICKY; } diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java index b0beb77c1..a3614c09c 100644 --- a/java/com/android/incallui/video/impl/VideoCallFragment.java +++ b/java/com/android/incallui/video/impl/VideoCallFragment.java @@ -85,7 +85,7 @@ import com.android.incallui.videosurface.protocol.VideoSurfaceTexture; import com.android.incallui.videotech.utils.VideoUtils; /** Contains UI elements for a video call. */ -// LINT.IfChange + public class VideoCallFragment extends Fragment implements InCallScreen, InCallButtonUi, @@ -1271,4 +1271,4 @@ public class VideoCallFragment extends Fragment } } } -// LINT.ThenChange(//depot/google3/third_party/java_src/android_app/dialer/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java) + diff --git a/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml b/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml index ad984f36e..3cf050467 100644 --- a/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml +++ b/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml @@ -15,7 +15,7 @@ android:layout_height="wrap_content"/> <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses. - b/31396406 --> + a bug --> <com.android.incallui.autoresizetext.AutoResizeTextView android:id="@id/contactgrid_contact_name" android:layout_width="wrap_content" diff --git a/java/com/android/newbubble/AndroidManifest.xml b/java/com/android/newbubble/AndroidManifest.xml new file mode 100644 index 000000000..048f8cf1b --- /dev/null +++ b/java/com/android/newbubble/AndroidManifest.xml @@ -0,0 +1,22 @@ +<!-- + ~ 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 + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.newbubble"> + + <uses-sdk android:minSdkVersion="23"/> + <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> +</manifest> diff --git a/java/com/android/newbubble/NewBubble.java b/java/com/android/newbubble/NewBubble.java new file mode 100644 index 000000000..d9b9ae2ad --- /dev/null +++ b/java/com/android/newbubble/NewBubble.java @@ -0,0 +1,837 @@ +/* + * 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.newbubble; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +import android.app.PendingIntent.CanceledException; +import android.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.support.annotation.ColorInt; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.graphics.ColorUtils; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v4.os.BuildCompat; +import android.support.v4.view.animation.FastOutLinearInInterpolator; +import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.transition.TransitionManager; +import android.transition.TransitionValues; +import android.view.ContextThemeWrapper; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewPropertyAnimator; +import android.view.ViewTreeObserver.OnPreDrawListener; +import android.view.WindowManager; +import android.view.WindowManager.LayoutParams; +import android.view.animation.AnticipateInterpolator; +import android.view.animation.OvershootInterpolator; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.ViewAnimator; +import com.android.newbubble.NewBubbleInfo.Action; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +/** + * Creates and manages a bubble window from information in a {@link NewBubbleInfo}. Before creating, + * be sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request + * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for + * convenience) + */ +public class NewBubble { + // This class has some odd behavior that is not immediately obvious in order to avoid jank when + // resizing. See http://go/bubble-resize for details. + + // How long text should show after showText(CharSequence) is called + private static final int SHOW_TEXT_DURATION_MILLIS = 3000; + // How long the new window should show before destroying the old one during resize operations. + // This ensures the new window has had time to draw first. + private static final int WINDOW_REDRAW_DELAY_MILLIS = 50; + + private static Boolean canShowBubblesForTesting = null; + + private final Context context; + private final WindowManager windowManager; + + private final Handler handler; + private LayoutParams windowParams; + + // Initialized in factory method + @SuppressWarnings("NullableProblems") + @NonNull + private NewBubbleInfo currentInfo; + + @Visibility private int visibility; + private boolean expanded; + private boolean textShowing; + private boolean hideAfterText; + private CharSequence textAfterShow; + private int collapseEndAction; + + @VisibleForTesting ViewHolder viewHolder; + private ViewPropertyAnimator collapseAnimation; + private Integer overrideGravity; + private ViewPropertyAnimator exitAnimator; + + private final Runnable collapseRunnable = + new Runnable() { + @Override + public void run() { + textShowing = false; + if (hideAfterText) { + // Always reset here since text shouldn't keep showing. + hideAndReset(); + } else { + doResize( + () -> viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_ICON)); + } + } + }; + + private BubbleExpansionStateListener bubbleExpansionStateListener; + + /** Type of action after bubble collapse */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE}) + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public @interface CollapseEnd { + int NOTHING = 0; + int HIDE = 1; + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({Visibility.ENTERING, Visibility.SHOWING, Visibility.EXITING, Visibility.HIDDEN}) + private @interface Visibility { + int HIDDEN = 0; + int ENTERING = 1; + int SHOWING = 2; + int EXITING = 3; + } + + /** Indicate bubble expansion state. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ExpansionState.START_EXPANDING, ExpansionState.START_COLLAPSING}) + public @interface ExpansionState { + // TODO(yueg): add more states when needed + int START_EXPANDING = 0; + int START_COLLAPSING = 1; + } + + /** + * Determines whether bubbles can be shown based on permissions obtained. This should be checked + * before attempting to create a Bubble. + * + * @return true iff bubbles are able to be shown. + * @see Settings#canDrawOverlays(Context) + */ + public static boolean canShowBubbles(@NonNull Context context) { + return canShowBubblesForTesting != null + ? canShowBubblesForTesting + : Settings.canDrawOverlays(context); + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public static void setCanShowBubblesForTesting(boolean canShowBubbles) { + canShowBubblesForTesting = canShowBubbles; + } + + /** Returns an Intent to request permission to show overlays */ + @NonNull + public static Intent getRequestPermissionIntent(@NonNull Context context) { + return new Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.fromParts("package", context.getPackageName(), null)); + } + + /** Creates instances of Bubble. The default implementation just calls the constructor. */ + @VisibleForTesting + public interface BubbleFactory { + NewBubble createBubble(@NonNull Context context, @NonNull Handler handler); + } + + private static BubbleFactory bubbleFactory = NewBubble::new; + + public static NewBubble createBubble(@NonNull Context context, @NonNull NewBubbleInfo info) { + NewBubble bubble = bubbleFactory.createBubble(context, new Handler()); + bubble.setBubbleInfo(info); + return bubble; + } + + @VisibleForTesting + public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) { + NewBubble.bubbleFactory = bubbleFactory; + } + + @VisibleForTesting + public static void resetBubbleFactory() { + NewBubble.bubbleFactory = NewBubble::new; + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + NewBubble(@NonNull Context context, @NonNull Handler handler) { + context = new ContextThemeWrapper(context, R.style.Theme_AppCompat); + this.context = context; + this.handler = handler; + windowManager = context.getSystemService(WindowManager.class); + + viewHolder = new ViewHolder(context); + } + + /** Expands the main bubble menu. */ + public void expand(boolean isUserAction) { + if (bubbleExpansionStateListener != null) { + bubbleExpansionStateListener.onBubbleExpansionStateChanged( + ExpansionState.START_EXPANDING, isUserAction); + } + doResize(() -> viewHolder.setDrawerVisibility(View.VISIBLE)); + View expandedView = viewHolder.getExpandedView(); + expandedView + .getViewTreeObserver() + .addOnPreDrawListener( + new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + // Animate expanded view to move from above primary button to its final position + expandedView.getViewTreeObserver().removeOnPreDrawListener(this); + expandedView.setTranslationY(-viewHolder.getRoot().getHeight()); + expandedView + .animate() + .setInterpolator(new LinearOutSlowInInterpolator()) + .translationY(0); + return false; + } + }); + setFocused(true); + expanded = true; + } + + /** + * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is + * already showing this method does nothing. + */ + public void show() { + if (collapseEndAction == CollapseEnd.HIDE) { + // If show() was called while collapsing, make sure we don't hide after. + collapseEndAction = CollapseEnd.NOTHING; + } + if (visibility == Visibility.SHOWING || visibility == Visibility.ENTERING) { + return; + } + + hideAfterText = false; + + if (windowParams == null) { + // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O. + @SuppressWarnings("deprecation") + @SuppressLint("InlinedApi") + int type = + BuildCompat.isAtLeastO() + ? LayoutParams.TYPE_APPLICATION_OVERLAY + : LayoutParams.TYPE_PHONE; + + windowParams = + new LayoutParams( + type, + LayoutParams.FLAG_NOT_TOUCH_MODAL + | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | LayoutParams.FLAG_NOT_FOCUSABLE + | LayoutParams.FLAG_LAYOUT_NO_LIMITS, + PixelFormat.TRANSLUCENT); + windowParams.gravity = Gravity.TOP | Gravity.LEFT; + windowParams.x = + context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_horizontal); + windowParams.y = currentInfo.getStartingYPosition(); + windowParams.height = LayoutParams.WRAP_CONTENT; + windowParams.width = LayoutParams.WRAP_CONTENT; + } + + if (exitAnimator != null) { + exitAnimator.cancel(); + exitAnimator = null; + } else { + windowManager.addView(viewHolder.getRoot(), windowParams); + viewHolder.getPrimaryButton().setScaleX(0); + viewHolder.getPrimaryButton().setScaleY(0); + } + + viewHolder.setChildClickable(true); + visibility = Visibility.ENTERING; + viewHolder + .getPrimaryButton() + .animate() + .setInterpolator(new OvershootInterpolator()) + .scaleX(1) + .scaleY(1) + .withEndAction( + () -> { + visibility = Visibility.SHOWING; + // Show the queued up text, if available. + if (textAfterShow != null) { + showText(textAfterShow); + textAfterShow = null; + } + }) + .start(); + + updatePrimaryIconAnimation(); + } + + /** Hide the bubble. */ + public void hide() { + if (hideAfterText) { + // hideAndReset() will be called after showing text, do nothing here. + return; + } + hideHelper(this::defaultAfterHidingAnimation); + } + + /** Hide the bubble and reset {@viewHolder} to initial state */ + public void hideAndReset() { + hideHelper( + () -> { + defaultAfterHidingAnimation(); + reset(); + }); + } + + /** Returns whether the bubble is currently visible */ + public boolean isVisible() { + return visibility == Visibility.SHOWING + || visibility == Visibility.ENTERING + || visibility == Visibility.EXITING; + } + + /** + * Set the info for this Bubble to display + * + * @param bubbleInfo the BubbleInfo to display in this Bubble. + */ + public void setBubbleInfo(@NonNull NewBubbleInfo bubbleInfo) { + currentInfo = bubbleInfo; + update(); + } + + /** + * Update the state and behavior of actions. + * + * @param actions the new state of the bubble's actions + */ + public void updateActions(@NonNull List<Action> actions) { + currentInfo = NewBubbleInfo.from(currentInfo).setActions(actions).build(); + updateButtonStates(); + } + + /** Returns the currently displayed NewBubbleInfo */ + public NewBubbleInfo getBubbleInfo() { + return currentInfo; + } + + /** + * Display text in the main bubble. The bubble's drawer is not expandable while text is showing, + * and the drawer will be closed if already open. + * + * @param text the text to display to the user + */ + public void showText(@NonNull CharSequence text) { + textShowing = true; + if (expanded) { + startCollapse(CollapseEnd.NOTHING, false); + doShowText(text); + } else { + // Need to transition from old bounds to new bounds manually + NewChangeOnScreenBounds transition = new NewChangeOnScreenBounds(); + // Prepare and capture start values + TransitionValues startValues = new TransitionValues(); + startValues.view = viewHolder.getPrimaryButton(); + transition.addTarget(startValues.view); + transition.captureStartValues(startValues); + + // If our view is not laid out yet, postpone showing the text. + if (startValues.values.isEmpty()) { + textAfterShow = text; + return; + } + + doResize( + () -> { + doShowText(text); + // Hide the text so we can animate it in + viewHolder.getPrimaryText().setAlpha(0); + + ViewAnimator primaryButton = viewHolder.getPrimaryButton(); + // Cancel the automatic transition scheduled in doShowText + TransitionManager.endTransitions((ViewGroup) primaryButton.getParent()); + primaryButton + .getViewTreeObserver() + .addOnPreDrawListener( + new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + primaryButton.getViewTreeObserver().removeOnPreDrawListener(this); + + // Prepare and capture end values, always use the size of primaryText since + // its invisibility makes primaryButton smaller than expected + TransitionValues endValues = new TransitionValues(); + endValues.values.put( + NewChangeOnScreenBounds.PROPNAME_WIDTH, + viewHolder.getPrimaryText().getWidth()); + endValues.values.put( + NewChangeOnScreenBounds.PROPNAME_HEIGHT, + viewHolder.getPrimaryText().getHeight()); + endValues.view = primaryButton; + transition.addTarget(endValues.view); + transition.captureEndValues(endValues); + + // animate the primary button bounds change + Animator bounds = + transition.createAnimator(primaryButton, startValues, endValues); + + // Animate the text in + Animator alpha = + ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f); + + AnimatorSet set = new AnimatorSet(); + set.play(bounds).before(alpha); + set.start(); + return false; + } + }); + }); + } + handler.removeCallbacks(collapseRunnable); + handler.postDelayed(collapseRunnable, SHOW_TEXT_DURATION_MILLIS); + } + + public void setBubbleExpansionStateListener( + BubbleExpansionStateListener bubbleExpansionStateListener) { + this.bubbleExpansionStateListener = bubbleExpansionStateListener; + } + + @Nullable + Integer getGravityOverride() { + return overrideGravity; + } + + void onMoveStart() { + startCollapse(CollapseEnd.NOTHING, true); + viewHolder + .getPrimaryButton() + .animate() + .translationZ( + context.getResources().getDimensionPixelOffset(R.dimen.bubble_move_elevation_change)); + } + + void onMoveFinish() { + viewHolder.getPrimaryButton().animate().translationZ(0); + // If it's GONE, no resize is necessary. If it's VISIBLE, it will get cleaned up when the + // collapse animation finishes + if (viewHolder.getExpandedView().getVisibility() == View.INVISIBLE) { + doResize(null); + } + } + + void primaryButtonClick() { + if (textShowing || currentInfo.getActions().isEmpty()) { + return; + } + if (expanded) { + startCollapse(CollapseEnd.NOTHING, true); + } else { + expand(true); + } + } + + LayoutParams getWindowParams() { + return windowParams; + } + + View getRootView() { + return viewHolder.getRoot(); + } + + /** + * Hide the bubble if visible. Will run a short exit animation and before hiding, and {@code + * afterHiding} after hiding. If the bubble is currently showing text, will hide after the text is + * done displaying. If the bubble is not visible this method does nothing. + */ + private void hideHelper(Runnable afterHiding) { + if (visibility == Visibility.HIDDEN || visibility == Visibility.EXITING) { + return; + } + + // Make bubble non clickable to prevent further buggy actions + viewHolder.setChildClickable(false); + + if (textShowing) { + hideAfterText = true; + return; + } + + if (collapseAnimation != null) { + collapseEndAction = CollapseEnd.HIDE; + return; + } + + if (expanded) { + startCollapse(CollapseEnd.HIDE, false); + return; + } + + visibility = Visibility.EXITING; + exitAnimator = + viewHolder + .getPrimaryButton() + .animate() + .setInterpolator(new AnticipateInterpolator()) + .scaleX(0) + .scaleY(0) + .withEndAction(afterHiding); + exitAnimator.start(); + } + + private void reset() { + viewHolder = new ViewHolder(viewHolder.getRoot().getContext()); + update(); + } + + private void update() { + RippleDrawable backgroundRipple = + (RippleDrawable) + context.getResources().getDrawable(R.drawable.bubble_ripple_circle, context.getTheme()); + int primaryTint = + ColorUtils.compositeColors( + context.getColor(R.color.bubble_primary_background_darken), + currentInfo.getPrimaryColor()); + backgroundRipple.getDrawable(0).setTint(primaryTint); + viewHolder.getPrimaryButton().setBackground(backgroundRipple); + + viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon()); + updatePrimaryIconAnimation(); + + updateButtonStates(); + } + + private void updatePrimaryIconAnimation() { + Drawable drawable = viewHolder.getPrimaryIcon().getDrawable(); + if (drawable instanceof Animatable) { + if (isVisible()) { + ((Animatable) drawable).start(); + } else { + ((Animatable) drawable).stop(); + } + } + } + + private void updateButtonStates() { + int colorBlue = context.getColor(R.color.bubble_button_text_color_blue); + int colorWhite = context.getColor(R.color.bubble_button_text_color_white); + + configureButton(currentInfo.getActions().get(0), viewHolder.getFullScreenButton(), colorBlue); + configureButton(currentInfo.getActions().get(1), viewHolder.getMuteButton(), colorBlue); + configureButton(currentInfo.getActions().get(2), viewHolder.getAudioRouteButton(), colorBlue); + configureButton(currentInfo.getActions().get(3), viewHolder.getEndCallButton(), colorWhite); + } + + private void doShowText(@NonNull CharSequence text) { + TransitionManager.beginDelayedTransition((ViewGroup) viewHolder.getPrimaryButton().getParent()); + viewHolder.getPrimaryText().setText(text); + viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_TEXT); + } + + private void configureButton(Action action, NewCheckableButton button, @ColorInt int iconColor) { + Drawable iconDrawable = DrawableCompat.wrap(action.getIconDrawable()); + DrawableCompat.setTint(iconDrawable.mutate(), iconColor); + + button.setCompoundDrawablesWithIntrinsicBounds(iconDrawable, null, null, null); + button.setChecked(action.isChecked()); + button.setEnabled(action.isEnabled()); + if (action.getName() != null) { + button.setText(action.getName()); + } + button.setOnClickListener(v -> doAction(action)); + } + + private void doAction(Action action) { + try { + action.getIntent().send(); + } catch (CanceledException e) { + throw new RuntimeException(e); + } + } + + private void doResize(@Nullable Runnable operation) { + // If we're resizing on the right side of the screen, there is an implicit move operation + // necessary. The WindowManager does not sync the move and resize operations, so serious jank + // would occur. To fix this, instead of resizing the window, we create a new one and destroy + // the old one. There is a short delay before destroying the old view to ensure the new one has + // had time to draw. + ViewHolder oldViewHolder = viewHolder; + if (isDrawingFromRight()) { + viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext()); + update(); + viewHolder + .getPrimaryButton() + .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild()); + viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText()); + } + + if (operation != null) { + operation.run(); + } + + if (isDrawingFromRight()) { + swapViewHolders(oldViewHolder); + } + } + + private void swapViewHolders(ViewHolder oldViewHolder) { + ViewGroup root = viewHolder.getRoot(); + windowManager.addView(root, windowParams); + root.getViewTreeObserver() + .addOnPreDrawListener( + new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + root.getViewTreeObserver().removeOnPreDrawListener(this); + // Wait a bit before removing the old view; make sure the new one has drawn over it. + handler.postDelayed( + () -> windowManager.removeView(oldViewHolder.getRoot()), + WINDOW_REDRAW_DELAY_MILLIS); + return true; + } + }); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public void startCollapse(@CollapseEnd int endAction, boolean isUserAction) { + View expandedView = viewHolder.getExpandedView(); + if (expandedView.getVisibility() != View.VISIBLE || collapseAnimation != null) { + // Drawer is already collapsed or animation is running. + return; + } + + overrideGravity = isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT; + setFocused(false); + + if (collapseEndAction == CollapseEnd.NOTHING) { + collapseEndAction = endAction; + } + if (bubbleExpansionStateListener != null && collapseEndAction == CollapseEnd.NOTHING) { + bubbleExpansionStateListener.onBubbleExpansionStateChanged( + ExpansionState.START_COLLAPSING, isUserAction); + } + // Animate expanded view to move from its position to above primary button and hide + collapseAnimation = + expandedView + .animate() + .translationY(-viewHolder.getRoot().getHeight()) + .setInterpolator(new FastOutLinearInInterpolator()) + .withEndAction( + () -> { + collapseAnimation = null; + expanded = false; + + if (textShowing) { + // Will do resize once the text is done. + return; + } + + // Hide the drawer and resize if possible. + viewHolder.setDrawerVisibility(View.INVISIBLE); + if (!viewHolder.isMoving() || !isDrawingFromRight()) { + doResize(() -> viewHolder.setDrawerVisibility(View.GONE)); + } + + // If this collapse was to come before a hide, do it now. + if (collapseEndAction == CollapseEnd.HIDE) { + hide(); + } + collapseEndAction = CollapseEnd.NOTHING; + + // Resume normal gravity after any resizing is done. + handler.postDelayed( + () -> { + overrideGravity = null; + if (!viewHolder.isMoving()) { + viewHolder.undoGravityOverride(); + } + }, + // Need to wait twice as long for resize and layout + WINDOW_REDRAW_DELAY_MILLIS * 2); + }); + } + + private boolean isDrawingFromRight() { + return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT; + } + + private void setFocused(boolean focused) { + if (focused) { + windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE; + } + windowManager.updateViewLayout(getRootView(), windowParams); + } + + private void defaultAfterHidingAnimation() { + exitAnimator = null; + windowManager.removeView(viewHolder.getRoot()); + visibility = Visibility.HIDDEN; + + updatePrimaryIconAnimation(); + } + + @VisibleForTesting + class ViewHolder { + + public static final int CHILD_INDEX_ICON = 0; + public static final int CHILD_INDEX_TEXT = 1; + + private final NewMoveHandler moveHandler; + private final NewWindowRoot root; + private final ViewAnimator primaryButton; + private final ImageView primaryIcon; + private final TextView primaryText; + + private final NewCheckableButton fullScreenButton; + private final NewCheckableButton muteButton; + private final NewCheckableButton audioRouteButton; + private final NewCheckableButton endCallButton; + private final View expandedView; + + public ViewHolder(Context context) { + // Window root is not in the layout file so that the inflater has a view to inflate into + this.root = new NewWindowRoot(context); + LayoutInflater inflater = LayoutInflater.from(root.getContext()); + View contentView = inflater.inflate(R.layout.new_bubble_base, root, true); + expandedView = contentView.findViewById(R.id.bubble_expanded_layout); + primaryButton = contentView.findViewById(R.id.bubble_button_primary); + primaryIcon = contentView.findViewById(R.id.bubble_icon_primary); + primaryText = contentView.findViewById(R.id.bubble_text); + + fullScreenButton = contentView.findViewById(R.id.bubble_button_full_screen); + muteButton = contentView.findViewById(R.id.bubble_button_mute); + audioRouteButton = contentView.findViewById(R.id.bubble_button_audio_route); + endCallButton = contentView.findViewById(R.id.bubble_button_end_call); + + root.setOnBackPressedListener( + () -> { + if (visibility == Visibility.SHOWING && expanded) { + startCollapse(CollapseEnd.NOTHING, true); + return true; + } + return false; + }); + root.setOnConfigurationChangedListener( + (configuration) -> { + // The values in the current MoveHandler may be stale, so replace it. Then ensure the + // Window is in bounds + moveHandler = new NewMoveHandler(primaryButton, NewBubble.this); + moveHandler.snapToBounds(); + }); + root.setOnTouchListener( + (v, event) -> { + if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) { + startCollapse(CollapseEnd.NOTHING, true); + return true; + } + return false; + }); + moveHandler = new NewMoveHandler(primaryButton, NewBubble.this); + } + + private void setChildClickable(boolean clickable) { + fullScreenButton.setClickable(clickable); + muteButton.setClickable(clickable); + audioRouteButton.setClickable(clickable); + endCallButton.setClickable(clickable); + + // For primaryButton + moveHandler.setClickable(clickable); + } + + public ViewGroup getRoot() { + return root; + } + + public ViewAnimator getPrimaryButton() { + return primaryButton; + } + + public ImageView getPrimaryIcon() { + return primaryIcon; + } + + public TextView getPrimaryText() { + return primaryText; + } + + public View getExpandedView() { + return expandedView; + } + + public NewCheckableButton getFullScreenButton() { + return fullScreenButton; + } + + public NewCheckableButton getMuteButton() { + return muteButton; + } + + public NewCheckableButton getAudioRouteButton() { + return audioRouteButton; + } + + public NewCheckableButton getEndCallButton() { + return endCallButton; + } + + public void setDrawerVisibility(int visibility) { + expandedView.setVisibility(visibility); + } + + public boolean isMoving() { + return moveHandler.isMoving(); + } + + public void undoGravityOverride() { + moveHandler.undoGravityOverride(); + } + } + + /** Listener for bubble expansion state change. */ + public interface BubbleExpansionStateListener { + void onBubbleExpansionStateChanged(@ExpansionState int expansionState, boolean isUserAction); + } +} diff --git a/java/com/android/newbubble/NewBubbleInfo.java b/java/com/android/newbubble/NewBubbleInfo.java new file mode 100644 index 000000000..f615929e3 --- /dev/null +++ b/java/com/android/newbubble/NewBubbleInfo.java @@ -0,0 +1,117 @@ +/* + * 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.newbubble; + +import android.app.PendingIntent; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.Px; +import com.google.auto.value.AutoValue; +import java.util.Collections; +import java.util.List; + +/** Info for displaying a {@link NewBubble} */ +@AutoValue +public abstract class NewBubbleInfo { + @ColorInt + public abstract int getPrimaryColor(); + + public abstract Icon getPrimaryIcon(); + + @Px + public abstract int getStartingYPosition(); + + @NonNull + public abstract List<Action> getActions(); + + public static Builder builder() { + return new AutoValue_NewBubbleInfo.Builder().setActions(Collections.emptyList()); + } + + public static Builder from(@NonNull NewBubbleInfo bubbleInfo) { + return builder() + .setPrimaryColor(bubbleInfo.getPrimaryColor()) + .setPrimaryIcon(bubbleInfo.getPrimaryIcon()) + .setStartingYPosition(bubbleInfo.getStartingYPosition()) + .setActions(bubbleInfo.getActions()); + } + + /** Builder for {@link NewBubbleInfo} */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setPrimaryColor(@ColorInt int primaryColor); + + public abstract Builder setPrimaryIcon(@NonNull Icon primaryIcon); + + public abstract Builder setStartingYPosition(@Px int startingYPosition); + + public abstract Builder setActions(List<Action> actions); + + public abstract NewBubbleInfo build(); + } + + /** Represents actions to be shown in the bubble when expanded */ + @AutoValue + public abstract static class Action { + + public abstract Drawable getIconDrawable(); + + @Nullable + public abstract CharSequence getName(); + + @NonNull + public abstract PendingIntent getIntent(); + + public abstract boolean isEnabled(); + + public abstract boolean isChecked(); + + public static Builder builder() { + return new AutoValue_NewBubbleInfo_Action.Builder().setEnabled(true).setChecked(false); + } + + public static Builder from(@NonNull Action action) { + return builder() + .setIntent(action.getIntent()) + .setChecked(action.isChecked()) + .setEnabled(action.isEnabled()) + .setName(action.getName()) + .setIconDrawable(action.getIconDrawable()); + } + + /** Builder for {@link Action} */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setIconDrawable(Drawable iconDrawable); + + public abstract Builder setName(@Nullable CharSequence name); + + public abstract Builder setIntent(@NonNull PendingIntent intent); + + public abstract Builder setEnabled(boolean enabled); + + public abstract Builder setChecked(boolean checked); + + public abstract Action build(); + } + } +} diff --git a/java/com/android/newbubble/NewChangeOnScreenBounds.java b/java/com/android/newbubble/NewChangeOnScreenBounds.java new file mode 100644 index 000000000..0653d3a4b --- /dev/null +++ b/java/com/android/newbubble/NewChangeOnScreenBounds.java @@ -0,0 +1,191 @@ +/* + * 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.newbubble; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.Rect; +import android.support.annotation.VisibleForTesting; +import android.transition.Transition; +import android.transition.TransitionValues; +import android.util.Property; +import android.view.View; +import android.view.ViewGroup; + +/** Similar to {@link android.transition.ChangeBounds ChangeBounds} but works across windows */ +public class NewChangeOnScreenBounds extends Transition { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String PROPNAME_BOUNDS = "bubble:changeScreenBounds:bounds"; + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String PROPNAME_SCREEN_X = "bubble:changeScreenBounds:screenX"; + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String PROPNAME_SCREEN_Y = "bubble:changeScreenBounds:screenY"; + + static final String PROPNAME_WIDTH = "bubble:changeScreenBounds:width"; + static final String PROPNAME_HEIGHT = "bubble:changeScreenBounds:height"; + + private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY = + new Property<ViewBounds, PointF>(PointF.class, "topLeft") { + @Override + public void set(ViewBounds viewBounds, PointF topLeft) { + viewBounds.setTopLeft(topLeft); + } + + @Override + public PointF get(ViewBounds viewBounds) { + return null; + } + }; + + private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY = + new Property<ViewBounds, PointF>(PointF.class, "bottomRight") { + @Override + public void set(ViewBounds viewBounds, PointF bottomRight) { + viewBounds.setBottomRight(bottomRight); + } + + @Override + public PointF get(ViewBounds viewBounds) { + return null; + } + }; + private final int[] tempLocation = new int[2]; + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValuesWithSize(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValuesWithSize(transitionValues); + } + + /** + * Capture location (left and top) from {@code values.view} and size (width and height) from + * {@code values.values}. If size is not set, use the size of {@code values.view}. + */ + private void captureValuesWithSize(TransitionValues values) { + View view = values.view; + + if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) { + Integer width = (Integer) values.values.get(PROPNAME_WIDTH); + Integer height = (Integer) values.values.get(PROPNAME_HEIGHT); + + values.values.put( + PROPNAME_BOUNDS, + new Rect( + view.getLeft(), + view.getTop(), + width == null ? view.getRight() : view.getLeft() + width, + height == null ? view.getBottom() : view.getTop() + height)); + values.view.getLocationOnScreen(tempLocation); + values.values.put(PROPNAME_SCREEN_X, tempLocation[0]); + values.values.put(PROPNAME_SCREEN_Y, tempLocation[1]); + } + } + + @Override + public Animator createAnimator( + ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) { + Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS); + Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); + + if (startBounds == null || endBounds == null) { + // start or end values were not captured, so don't animate. + return null; + } + + // Offset the startBounds by the difference in screen position + int startScreenX = (Integer) startValues.values.get(PROPNAME_SCREEN_X); + int startScreenY = (Integer) startValues.values.get(PROPNAME_SCREEN_Y); + int endScreenX = (Integer) endValues.values.get(PROPNAME_SCREEN_X); + int endScreenY = (Integer) endValues.values.get(PROPNAME_SCREEN_Y); + startBounds.offset(startScreenX - endScreenX, startScreenY - endScreenY); + + final int startLeft = startBounds.left; + final int endLeft = endBounds.left; + final int startTop = startBounds.top; + final int endTop = endBounds.top; + final int startRight = startBounds.right; + final int endRight = endBounds.right; + final int startBottom = startBounds.bottom; + final int endBottom = endBounds.bottom; + ViewBounds viewBounds = new ViewBounds(endValues.view); + viewBounds.setTopLeft(new PointF(startLeft, startTop)); + viewBounds.setBottomRight(new PointF(startRight, startBottom)); + + // Animate the top left and bottom right corners along a path + Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft, endTop); + ObjectAnimator topLeftAnimator = + ObjectAnimator.ofObject(viewBounds, TOP_LEFT_PROPERTY, null, topLeftPath); + + Path bottomRightPath = getPathMotion().getPath(startRight, startBottom, endRight, endBottom); + ObjectAnimator bottomRightAnimator = + ObjectAnimator.ofObject(viewBounds, BOTTOM_RIGHT_PROPERTY, null, bottomRightPath); + AnimatorSet set = new AnimatorSet(); + set.playTogether(topLeftAnimator, bottomRightAnimator); + return set; + } + + private static class ViewBounds { + private int left; + private int top; + private int right; + private int bottom; + private final View view; + private int topLeftCalls; + private int bottomRightCalls; + + public ViewBounds(View view) { + this.view = view; + } + + public void setTopLeft(PointF topLeft) { + left = Math.round(topLeft.x); + top = Math.round(topLeft.y); + topLeftCalls++; + if (topLeftCalls == bottomRightCalls) { + updateLeftTopRightBottom(); + } + } + + public void setBottomRight(PointF bottomRight) { + right = Math.round(bottomRight.x); + bottom = Math.round(bottomRight.y); + bottomRightCalls++; + if (topLeftCalls == bottomRightCalls) { + updateLeftTopRightBottom(); + } + } + + private void updateLeftTopRightBottom() { + view.setLeft(left); + view.setTop(top); + view.setRight(right); + view.setBottom(bottom); + topLeftCalls = 0; + bottomRightCalls = 0; + } + } +} diff --git a/java/com/android/newbubble/NewCheckableButton.java b/java/com/android/newbubble/NewCheckableButton.java new file mode 100644 index 000000000..63525a4a1 --- /dev/null +++ b/java/com/android/newbubble/NewCheckableButton.java @@ -0,0 +1,87 @@ +/* + * 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.newbubble; + +import android.content.Context; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v7.widget.AppCompatButton; +import android.util.AttributeSet; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.widget.Checkable; + +/** + * A {@link android.widget.Button Button} that implements {@link Checkable} and propagates the + * checkable state + */ +public class NewCheckableButton extends AppCompatButton implements Checkable { + + private boolean mChecked; + + public NewCheckableButton(Context context) { + this(context, null); + } + + public NewCheckableButton(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.imageButtonStyle); + } + + public NewCheckableButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + ViewCompat.setAccessibilityDelegate( + this, + new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setChecked(isChecked()); + } + + @Override + public void onInitializeAccessibilityNodeInfo( + View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setCheckable(true); + info.setChecked(isChecked()); + } + }); + } + + @Override + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + setTextColor( + checked + ? getContext().getColor(R.color.bubble_button_text_color_blue) + : getContext().getColor(R.color.bubble_button_text_color_black)); + } + } + + @Override + public boolean isChecked() { + return mChecked; + } + + @Override + public void toggle() { + setChecked(!mChecked); + } +} diff --git a/java/com/android/newbubble/NewMoveHandler.java b/java/com/android/newbubble/NewMoveHandler.java new file mode 100644 index 000000000..189ad8472 --- /dev/null +++ b/java/com/android/newbubble/NewMoveHandler.java @@ -0,0 +1,279 @@ +/* + * 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.newbubble; + +import android.content.Context; +import android.graphics.Point; +import android.support.animation.FloatPropertyCompat; +import android.support.animation.SpringAnimation; +import android.support.animation.SpringForce; +import android.support.annotation.NonNull; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.ViewConfiguration; +import android.view.WindowManager; +import android.view.WindowManager.LayoutParams; +import android.widget.Scroller; + +/** Handles touches and manages moving the bubble in response */ +class NewMoveHandler implements OnTouchListener { + + // Amount the ViewConfiguration's minFlingVelocity will be scaled by for our own minVelocity + private static final int MIN_FLING_VELOCITY_FACTOR = 8; + // The friction multiplier to control how slippery the bubble is when flung + private static final float SCROLL_FRICTION_MULTIPLIER = 4f; + + private final Context context; + private final WindowManager windowManager; + private final NewBubble bubble; + private final int minX; + private final int minY; + private final int maxX; + private final int maxY; + private final int bubbleSize; + private final float touchSlopSquared; + + private boolean clickable = true; + private boolean isMoving; + private float firstX; + private float firstY; + + private SpringAnimation moveXAnimation; + private SpringAnimation moveYAnimation; + private VelocityTracker velocityTracker; + private Scroller scroller; + + private static float clamp(float value, float min, float max) { + return Math.min(max, Math.max(min, value)); + } + + // Handles the left/right gravity conversion and centering + private final FloatPropertyCompat<WindowManager.LayoutParams> xProperty = + new FloatPropertyCompat<LayoutParams>("xProperty") { + @Override + public float getValue(LayoutParams windowParams) { + int realX = windowParams.x; + realX = realX + bubbleSize / 2; + if (relativeToRight(windowParams)) { + int displayWidth = context.getResources().getDisplayMetrics().widthPixels; + realX = displayWidth - realX; + } + return clamp(realX, minX, maxX); + } + + @Override + public void setValue(LayoutParams windowParams, float value) { + int displayWidth = context.getResources().getDisplayMetrics().widthPixels; + boolean onRight; + Integer gravityOverride = bubble.getGravityOverride(); + if (gravityOverride == null) { + onRight = value > displayWidth / 2; + } else { + onRight = (gravityOverride & Gravity.RIGHT) == Gravity.RIGHT; + } + int centeringOffset = bubbleSize / 2; + windowParams.x = + (int) (onRight ? (displayWidth - value - centeringOffset) : value - centeringOffset); + windowParams.gravity = Gravity.TOP | (onRight ? Gravity.RIGHT : Gravity.LEFT); + if (bubble.isVisible()) { + windowManager.updateViewLayout(bubble.getRootView(), windowParams); + } + } + }; + + private final FloatPropertyCompat<WindowManager.LayoutParams> yProperty = + new FloatPropertyCompat<LayoutParams>("yProperty") { + @Override + public float getValue(LayoutParams object) { + return clamp(object.y + bubbleSize, minY, maxY); + } + + @Override + public void setValue(LayoutParams object, float value) { + object.y = (int) value - bubbleSize; + if (bubble.isVisible()) { + windowManager.updateViewLayout(bubble.getRootView(), object); + } + } + }; + + public NewMoveHandler(@NonNull View targetView, @NonNull NewBubble bubble) { + this.bubble = bubble; + context = targetView.getContext(); + windowManager = context.getSystemService(WindowManager.class); + + bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size); + minX = + context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_horizontal) + + bubbleSize / 2; + minY = + context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_vertical) + + bubbleSize / 2; + maxX = context.getResources().getDisplayMetrics().widthPixels - minX; + maxY = context.getResources().getDisplayMetrics().heightPixels - minY; + + // Squared because it will be compared against the square of the touch delta. This is more + // efficient than needing to take a square root. + touchSlopSquared = (float) Math.pow(ViewConfiguration.get(context).getScaledTouchSlop(), 2); + + targetView.setOnTouchListener(this); + } + + public void setClickable(boolean clickable) { + this.clickable = clickable; + } + + public boolean isMoving() { + return isMoving; + } + + public void undoGravityOverride() { + LayoutParams windowParams = bubble.getWindowParams(); + xProperty.setValue(windowParams, xProperty.getValue(windowParams)); + } + + public void snapToBounds() { + ensureSprings(); + + moveXAnimation.animateToFinalPosition(relativeToRight(bubble.getWindowParams()) ? maxX : minX); + moveYAnimation.animateToFinalPosition(yProperty.getValue(bubble.getWindowParams())); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + float eventX = event.getRawX(); + float eventY = event.getRawY(); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + firstX = eventX; + firstY = eventY; + velocityTracker = VelocityTracker.obtain(); + break; + case MotionEvent.ACTION_MOVE: + if (isMoving || hasExceededTouchSlop(event)) { + if (!isMoving) { + isMoving = true; + bubble.onMoveStart(); + } + + ensureSprings(); + + moveXAnimation.animateToFinalPosition(clamp(eventX, minX, maxX)); + moveYAnimation.animateToFinalPosition(clamp(eventY, minY, maxY)); + } + + velocityTracker.addMovement(event); + break; + case MotionEvent.ACTION_UP: + if (isMoving) { + ViewConfiguration viewConfiguration = ViewConfiguration.get(context); + velocityTracker.computeCurrentVelocity( + 1000, viewConfiguration.getScaledMaximumFlingVelocity()); + float xVelocity = velocityTracker.getXVelocity(); + float yVelocity = velocityTracker.getYVelocity(); + boolean isFling = isFling(xVelocity, yVelocity); + + if (isFling) { + Point target = + findTarget( + xVelocity, + yVelocity, + (int) xProperty.getValue(bubble.getWindowParams()), + (int) yProperty.getValue(bubble.getWindowParams())); + + moveXAnimation.animateToFinalPosition(target.x); + moveYAnimation.animateToFinalPosition(target.y); + } else { + snapX(); + } + isMoving = false; + bubble.onMoveFinish(); + } else { + v.performClick(); + if (clickable) { + bubble.primaryButtonClick(); + } + } + break; + default: // fall out + } + return true; + } + + private void ensureSprings() { + if (moveXAnimation == null) { + moveXAnimation = new SpringAnimation(bubble.getWindowParams(), xProperty); + moveXAnimation.setSpring(new SpringForce()); + moveXAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY); + } + + if (moveYAnimation == null) { + moveYAnimation = new SpringAnimation(bubble.getWindowParams(), yProperty); + moveYAnimation.setSpring(new SpringForce()); + moveYAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY); + } + } + + private Point findTarget(float xVelocity, float yVelocity, int startX, int startY) { + if (scroller == null) { + scroller = new Scroller(context); + scroller.setFriction(ViewConfiguration.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER); + } + + // Find where a fling would end vertically + scroller.fling(startX, startY, (int) xVelocity, (int) yVelocity, minX, maxX, minY, maxY); + int targetY = scroller.getFinalY(); + scroller.abortAnimation(); + + // If the x component of the velocity is above the minimum fling velocity, use velocity to + // determine edge. Otherwise use its starting position + boolean pullRight = isFling(xVelocity, 0) ? xVelocity > 0 : isOnRightHalf(startX); + return new Point(pullRight ? maxX : minX, targetY); + } + + private boolean isFling(float xVelocity, float yVelocity) { + int minFlingVelocity = + ViewConfiguration.get(context).getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_FACTOR; + return getMagnitudeSquared(xVelocity, yVelocity) > minFlingVelocity * minFlingVelocity; + } + + private boolean isOnRightHalf(float currentX) { + return currentX > (minX + maxX) / 2; + } + + private void snapX() { + // Check if x value is closer to min or max + boolean pullRight = isOnRightHalf(xProperty.getValue(bubble.getWindowParams())); + moveXAnimation.animateToFinalPosition(pullRight ? maxX : minX); + } + + private boolean relativeToRight(LayoutParams windowParams) { + return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT; + } + + private boolean hasExceededTouchSlop(MotionEvent event) { + return getMagnitudeSquared(event.getRawX() - firstX, event.getRawY() - firstY) + > touchSlopSquared; + } + + private float getMagnitudeSquared(float deltaX, float deltaY) { + return deltaX * deltaX + deltaY * deltaY; + } +} diff --git a/java/com/android/newbubble/NewWindowRoot.java b/java/com/android/newbubble/NewWindowRoot.java new file mode 100644 index 000000000..da24b7143 --- /dev/null +++ b/java/com/android/newbubble/NewWindowRoot.java @@ -0,0 +1,74 @@ +/* + * 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.newbubble; + +import android.content.Context; +import android.content.res.Configuration; +import android.support.annotation.NonNull; +import android.view.KeyEvent; +import android.widget.FrameLayout; + +/** + * ViewGroup that handles some overlay window concerns. Allows back button and configuration change + * events to be listened for via interfaces. + */ +public class NewWindowRoot extends FrameLayout { + + /** Callback for when the back button is pressed while this window is in focus */ + public interface OnBackPressedListener { + boolean onBackPressed(); + } + + /** Callback for when the Configuration changes for this window */ + public interface OnConfigurationChangedListener { + void onConfigurationChanged(Configuration newConfiguration); + } + + private OnBackPressedListener backPressedListener; + private OnConfigurationChangedListener configurationChangedListener; + + public NewWindowRoot(@NonNull Context context) { + super(context); + } + + public void setOnBackPressedListener(OnBackPressedListener listener) { + backPressedListener = listener; + } + + public void setOnConfigurationChangedListener(OnConfigurationChangedListener listener) { + configurationChangedListener = listener; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && backPressedListener != null) { + if (event.getAction() == KeyEvent.ACTION_UP) { + return backPressedListener.onBackPressed(); + } + return true; + } + return super.dispatchKeyEvent(event); + } + + @Override + public void dispatchConfigurationChanged(Configuration newConfig) { + super.dispatchConfigurationChanged(newConfig); + if (configurationChangedListener != null) { + configurationChangedListener.onConfigurationChanged(newConfig); + } + } +} diff --git a/java/com/android/newbubble/res/drawable/bubble_background_with_radius.xml b/java/com/android/newbubble/res/drawable/bubble_background_with_radius.xml new file mode 100644 index 000000000..4fd871c47 --- /dev/null +++ b/java/com/android/newbubble/res/drawable/bubble_background_with_radius.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners + android:bottomRightRadius="@dimen/bubble_radius" + android:topRightRadius="@dimen/bubble_radius" + android:bottomLeftRadius="@dimen/bubble_radius" + android:topLeftRadius="@dimen/bubble_radius"/> + <solid android:color="@android:color/white"/> +</shape> diff --git a/java/com/android/newbubble/res/drawable/bubble_ripple_circle.xml b/java/com/android/newbubble/res/drawable/bubble_ripple_circle.xml new file mode 100644 index 000000000..8d5cf0bb5 --- /dev/null +++ b/java/com/android/newbubble/res/drawable/bubble_ripple_circle.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:colorControlHighlight"> + <item> + <shape> + <corners android:radius="@dimen/bubble_size"/> + <solid android:color="@android:color/white"/> + </shape> + </item> +</ripple> diff --git a/java/com/android/newbubble/res/layout/new_bubble_base.xml b/java/com/android/newbubble/res/layout/new_bubble_base.xml new file mode 100644 index 000000000..ef35d7426 --- /dev/null +++ b/java/com/android/newbubble/res/layout/new_bubble_base.xml @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:theme="@style/Theme.AppCompat"> + <RelativeLayout + android:id="@+id/bubble_primary_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:animateLayoutChanges="true" + android:clipChildren="false" + android:clipToPadding="false" + android:elevation="12dp"> + <ViewAnimator + android:id="@+id/bubble_button_primary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/bubble_ripple_circle" + android:measureAllChildren="false" + tools:backgroundTint="#FF0000AA"> + <ImageView + android:id="@+id/bubble_icon_primary" + android:layout_width="@dimen/bubble_size" + android:layout_height="@dimen/bubble_size" + android:padding="@dimen/bubble_icon_padding" + android:tint="@android:color/white" + android:tintMode="src_in" + tools:src="@android:drawable/ic_btn_speak_now"/> + <TextView + android:id="@+id/bubble_text" + android:layout_width="wrap_content" + android:layout_height="@dimen/bubble_size" + android:paddingStart="@dimen/bubble_icon_padding" + android:paddingEnd="@dimen/bubble_icon_padding" + android:gravity="center" + android:minWidth="@dimen/bubble_size" + android:textAppearance="@style/TextAppearance.AppCompat" + tools:text="Call ended"/> + </ViewAnimator> + </RelativeLayout> + <RelativeLayout + android:id="@+id/bubble_expanded_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/bubble_primary_container" + android:paddingTop="@dimen/bubble_shadow_padding_size_vertical" + android:paddingBottom="@dimen/bubble_shadow_padding_size_vertical" + android:paddingStart="@dimen/bubble_shadow_padding_size_horizontal" + android:paddingEnd="@dimen/bubble_shadow_padding_size_horizontal" + android:clipToPadding="false" + android:visibility="gone" + tools:visibility="visible"> + <RelativeLayout + android:id="@+id/bubble_expanded_layout_part_one" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/bubble_background_with_radius" + android:elevation="@dimen/bubble_elevation" + android:layoutDirection="inherit"> + <com.android.newbubble.NewCheckableButton + android:id="@+id/bubble_button_full_screen" + android:layout_width="@dimen/bubble_expanded_width" + android:layout_height="@dimen/bubble_size" + android:padding="@dimen/bubble_icon_padding" + android:tint="@color/bubble_button_text_color_blue" + android:tintMode="src_in" + android:text="Full screen" + android:textColor="@color/bubble_button_text_color_black" + android:background="@android:color/transparent" + android:drawablePadding="@dimen/bubble_icon_padding"/> + <com.android.newbubble.NewCheckableButton + android:id="@+id/bubble_button_mute" + android:layout_width="@dimen/bubble_expanded_width" + android:layout_height="@dimen/bubble_size" + android:layout_below="@id/bubble_button_full_screen" + android:padding="@dimen/bubble_icon_padding" + android:tint="@color/bubble_button_text_color_blue" + android:tintMode="src_in" + android:text="Mute" + android:textColor="@color/bubble_button_text_color_black" + android:background="@android:color/transparent" + android:drawablePadding="@dimen/bubble_icon_padding"/> + <com.android.newbubble.NewCheckableButton + android:id="@+id/bubble_button_audio_route" + android:layout_width="@dimen/bubble_expanded_width" + android:layout_height="@dimen/bubble_size" + android:layout_below="@id/bubble_button_mute" + android:padding="@dimen/bubble_icon_padding" + android:tint="@color/bubble_button_text_color_blue" + android:tintMode="src_in" + android:text="Speakerphone" + android:textColor="@color/bubble_button_text_color_black" + android:background="@android:color/transparent" + android:drawablePadding="@dimen/bubble_icon_padding"/> + </RelativeLayout> + <RelativeLayout + android:id="@+id/bubble_expanded_layout_part_two" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/bubble_expanded_separator_height" + android:layout_below="@id/bubble_expanded_layout_part_one" + android:background="@drawable/bubble_ripple_circle" + android:backgroundTint="@color/bubble_end_call_button_background" + android:elevation="@dimen/bubble_elevation" + android:layoutDirection="inherit"> + <com.android.newbubble.NewCheckableButton + android:id="@+id/bubble_button_end_call" + android:layout_width="@dimen/bubble_expanded_width" + android:layout_height="@dimen/bubble_size" + android:padding="@dimen/bubble_icon_padding" + android:tint="@color/bubble_button_text_color_white" + android:tintMode="src_in" + android:text="End Call" + android:textColor="@color/bubble_button_text_color_white" + android:background="@android:color/transparent" + android:drawablePadding="@dimen/bubble_icon_padding"/> + </RelativeLayout> + </RelativeLayout> +</RelativeLayout> diff --git a/java/com/android/newbubble/res/values/colors.xml b/java/com/android/newbubble/res/values/colors.xml new file mode 100644 index 000000000..556d8bd95 --- /dev/null +++ b/java/com/android/newbubble/res/values/colors.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<resources> + <color name="bubble_primary_background_darken">#33000000</color> + + <color name="bubble_button_text_color_black">@color/dialer_primary_text_color</color> + <color name="bubble_button_text_color_white">@color/dialer_primary_text_color_white</color> + <color name="bubble_button_text_color_blue">@color/dialer_theme_color</color> + <color name="bubble_end_call_button_background">@color/dialer_end_call_button_color</color> +</resources> diff --git a/java/com/android/newbubble/res/values/values.xml b/java/com/android/newbubble/res/values/values.xml new file mode 100644 index 000000000..4bb90aff0 --- /dev/null +++ b/java/com/android/newbubble/res/values/values.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<resources> + <dimen name="bubble_size">56dp</dimen> + <dimen name="bubble_icon_padding">16dp</dimen> + <dimen name="bubble_move_elevation_change">4dp</dimen> + + <dimen name="bubble_safe_margin_horizontal">-4dp</dimen> + <dimen name="bubble_safe_margin_vertical">64dp</dimen> + <dimen name="bubble_shadow_padding_size_vertical">16dp</dimen> + <dimen name="bubble_shadow_padding_size_horizontal">12dp</dimen> + + <dimen name="bubble_elevation">10dp</dimen> + <dimen name="bubble_expanded_width">160dp</dimen> + <dimen name="bubble_radius">20dp</dimen> + <dimen name="bubble_expanded_separator_height">4dp</dimen> +</resources> diff --git a/java/com/android/voicemail/impl/OmtpService.java b/java/com/android/voicemail/impl/OmtpService.java index 4db1aeb7e..4e8860c02 100644 --- a/java/com/android/voicemail/impl/OmtpService.java +++ b/java/com/android/voicemail/impl/OmtpService.java @@ -25,6 +25,7 @@ import android.preference.PreferenceManager; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.telecom.PhoneAccountHandle; +import android.telephony.TelephonyManager; import android.telephony.VisualVoicemailService; import android.telephony.VisualVoicemailSms; import com.android.dialer.logging.DialerImpression; @@ -63,6 +64,7 @@ public class OmtpService extends VisualVoicemailService { } if (!isServiceEnabled(phoneAccountHandle)) { + disableFilter(phoneAccountHandle); task.finish(); return; } @@ -87,6 +89,8 @@ public class OmtpService extends VisualVoicemailService { } if (!isServiceEnabled(sms.getPhoneAccountHandle())) { + VvmLog.e(TAG, "onSmsReceived received when service is disabled"); + disableFilter(sms.getPhoneAccountHandle()); task.finish(); return; } @@ -178,6 +182,15 @@ public class OmtpService extends VisualVoicemailService { return true; } + private void disableFilter(PhoneAccountHandle phoneAccountHandle) { + TelephonyManager telephonyManager = + getSystemService(TelephonyManager.class).createForPhoneAccountHandle(phoneAccountHandle); + if (telephonyManager != null) { + VvmLog.i(TAG, "disabling SMS filter"); + telephonyManager.setVisualVoicemailSmsFilterSettings(null); + } + } + private static boolean isUserUnlocked(@NonNull Context context) { UserManager userManager = context.getSystemService(UserManager.class); return userManager.isUserUnlocked(); diff --git a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java index 90303f59f..f8dc4bd79 100644 --- a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java +++ b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java @@ -87,6 +87,8 @@ public class OmtpVvmCarrierConfigHelper { public static final String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string"; + @Nullable private static PersistableBundle sOverrideConfigForTest; + private final Context mContext; private final PersistableBundle mCarrierConfig; private final String mVvmType; @@ -114,16 +116,22 @@ public class OmtpVvmCarrierConfigHelper { return; } - if (ConfigOverrideFragment.isOverridden(context)) { - mOverrideConfig = ConfigOverrideFragment.getConfig(context); - VvmLog.w(TAG, "Config override is activated: " + mOverrideConfig); + if (sOverrideConfigForTest != null) { + mOverrideConfig = sOverrideConfigForTest; + mCarrierConfig = new PersistableBundle(); + mTelephonyConfig = new PersistableBundle(); } else { - mOverrideConfig = null; - } + if (ConfigOverrideFragment.isOverridden(context)) { + mOverrideConfig = ConfigOverrideFragment.getConfig(context); + VvmLog.w(TAG, "Config override is activated: " + mOverrideConfig); + } else { + mOverrideConfig = null; + } - mCarrierConfig = getCarrierConfig(telephonyManager); - mTelephonyConfig = - new TelephonyVvmConfigManager(context).getConfig(telephonyManager.getSimOperator()); + mCarrierConfig = getCarrierConfig(telephonyManager); + mTelephonyConfig = + new TelephonyVvmConfigManager(context).getConfig(telephonyManager.getSimOperator()); + } mVvmType = getVvmType(); mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType); @@ -275,7 +283,7 @@ public class OmtpVvmCarrierConfigHelper { * Hidden Config. * * <p>Sometimes the server states it supports a certain feature but we found they have bug on the - * server side. For example, in b/28717550 the server reported AUTH=DIGEST-MD5 capability but + * server side. For example, in a bug the server reported AUTH=DIGEST-MD5 capability but * using it to login will cause subsequent response to be erroneous. * * @return A set of capabilities that is reported by the IMAP CAPABILITY command, but determined @@ -485,4 +493,9 @@ public class OmtpVvmCarrierConfigHelper { } return defaultValue; } + + @VisibleForTesting + public static void setOverrideConfigForTest(PersistableBundle config) { + sOverrideConfigForTest = config; + } } diff --git a/java/com/android/voicemail/impl/VvmPhoneStateListener.java b/java/com/android/voicemail/impl/VvmPhoneStateListener.java index 00c1358ab..914120b69 100644 --- a/java/com/android/voicemail/impl/VvmPhoneStateListener.java +++ b/java/com/android/voicemail/impl/VvmPhoneStateListener.java @@ -24,7 +24,7 @@ import com.android.voicemail.impl.sync.VoicemailStatusQueryHelper; import com.android.voicemail.impl.sync.VvmAccountManager; /** - * Check if service is lost and indicate this in the voicemail status. TODO(b/35125657): Not used + * Check if service is lost and indicate this in the voicemail status. TODO(a bug): Not used * for now, restore it. */ public class VvmPhoneStateListener extends PhoneStateListener { @@ -36,7 +36,7 @@ public class VvmPhoneStateListener extends PhoneStateListener { private int mPreviousState = -1; public VvmPhoneStateListener(Context context, PhoneAccountHandle accountHandle) { - // TODO(twyen): b/32637799 too much trouble to call super constructor through reflection, + // TODO(twyen): a bug too much trouble to call super constructor through reflection, // just use non-phoneAccountHandle version for now. super(); mContext = context; diff --git a/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java b/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java index 8bc3cc21c..24f530f6e 100644 --- a/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java +++ b/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java @@ -34,7 +34,7 @@ import java.lang.annotation.RetentionPolicy; * Handles {@link OmtpEvents} when {@link Vvm3Protocol} is being used. This handler writes custom * error codes into the voicemail status table so support on the dialer side is required. * - * <p>TODO(b/29577838) disable VVM3 by default so support on system dialer can be ensured. + * <p>TODO(a bug) disable VVM3 by default so support on system dialer can be ensured. */ public class Vvm3EventHandler { diff --git a/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java b/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java index fc7fdf3d4..5454bac54 100644 --- a/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java +++ b/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java @@ -216,7 +216,7 @@ public class Vvm3Protocol extends VisualVoicemailProtocol { new ImapHelper(config.getContext(), phoneAccountHandle, network, status)) { // VVM3 has inconsistent error language code to OMTP. Just issue a raw command // here. - // TODO(b/29082671): use LocaleList + // TODO(a bug): use LocaleList if (Locale.getDefault().getLanguage().equals(new Locale(ISO639_Spanish).getLanguage())) { // Spanish helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS); diff --git a/java/com/android/voicemail/impl/res/xml/vvm_config.xml b/java/com/android/voicemail/impl/res/xml/vvm_config.xml index d95f315bb..1e5190a7c 100644 --- a/java/com/android/voicemail/impl/res/xml/vvm_config.xml +++ b/java/com/android/voicemail/impl/res/xml/vvm_config.xml @@ -38,7 +38,7 @@ value="true"/> <!-- Carrier VVM app com.orange.vvm only supports Orange FR--> <string-array name="vvm_disabled_capabilities_string_array"> - <!-- b/32365569 --> + <!-- a bug --> <item value="STARTTLS"/> </string-array> </pbundle_as_map> @@ -62,7 +62,7 @@ name="vvm_cellular_data_required_bool" value="true"/> <string-array name="vvm_disabled_capabilities_string_array"> - <!-- b/32365569 --> + <!-- a bug --> <item value="STARTTLS"/> </string-array> </pbundle_as_map> @@ -101,7 +101,7 @@ </string-array> <string name="vvm_type_string">vvm_type_cvvm</string>> <string-array name="vvm_disabled_capabilities_string_array"> - <!-- b/28717550 --> + <!-- a bug --> <item value="AUTH=DIGEST-MD5"/> </string-array> </pbundle_as_map> diff --git a/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java index 5b5d6b054..f7c8f29c7 100644 --- a/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java +++ b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java @@ -122,7 +122,7 @@ public class OmtpVvmSyncService { success = downloadOneVoicemail(imapHelper, voicemail, phoneAccount); } if (success) { - // TODO: b/30569269 failure should interrupt all subsequent task via exceptions + // TODO: a bug failure should interrupt all subsequent task via exceptions imapHelper.updateQuota(); autoDeleteAndArchiveVM(imapHelper, phoneAccount); imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED); diff --git a/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java index 316e1ca61..1af5e688e 100644 --- a/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java +++ b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java @@ -83,7 +83,7 @@ public class VoicemailsQueryHelper { /** * Utility method to make queries to the voicemail database. * - * <p>TODO(b/36588206) add PhoneAccountHandle filtering back + * <p>TODO(a bug) add PhoneAccountHandle filtering back * * @param selection A filter declaring which rows to return. {@code null} returns all rows. * @return A list of voicemails according to the selection statement. diff --git a/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java index 068b19b70..17f5c1608 100644 --- a/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java +++ b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java @@ -128,7 +128,7 @@ public abstract class VvmNetworkRequestCallback extends ConnectivityManager.Netw @CallSuper public void onUnavailable() { - // TODO(twyen): b/32637799 this is hidden, do we really need this? + // TODO(twyen): a bug this is hidden, do we really need this? mResultReceived = true; onFailed(NETWORK_REQUEST_FAILED_TIMEOUT); } diff --git a/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto b/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto index 697e9e337..b06017075 100644 --- a/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto +++ b/java/com/android/voicemail/impl/transcribe/grpc/voicemail_transcription.proto @@ -1,4 +1,4 @@ -// LINT.IfChange + syntax = "proto2"; @@ -151,5 +151,4 @@ service VoicemailTranscriptionService { } } -// LINT.ThenChange(//depot/google3/google/internal/communications/voicemailtranscription/v1/\ -// voicemail_transcription.proto) + |