diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2017-12-07 20:25:58 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2017-12-07 20:25:58 +0000 |
commit | 5412de42cc632bb63c72ddf73867ea3a2c35cc6e (patch) | |
tree | e4758bf76e5f2516fdbdfc62e5966eb9cb82b6e7 /java/com | |
parent | f39d23184df8a5d7a18efc0c8a8ddb512b791629 (diff) | |
parent | 952fc68f330177b05c29275155b3f473b7ac4dbe (diff) |
Merge changes I6806c5c8,I60afe7c6,I6d103569,I4cd6f76b,I6ddbebc9, ...
* changes:
Hide bubble in (New)ReturnToCallController.tearDown().
Grouping each listener's logic in an inner class in CallDetailsActivity.
Allow VVM activation to be rerun if carrier sent STATUS SMS
Check for null subscription info list in TelecomUtil#getSubscriptionInfo.
Ignore "new" voicemails that are too old for notifications
Display the automatically detected home country in assisted dialing settings.
Add DATA_USAGE column to the annotated call log.
Temporarily disable PhoneHistoryRecorder.
Bubble v2 animation changes.
Refactor Assisted Dialing Settings into standalone activity.
Added PhoneLookupHistoryRecorder.
Added PhoneLookupSelector.
Diffstat (limited to 'java/com')
26 files changed, 969 insertions, 428 deletions
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java index f962e17ac..47457360d 100644 --- a/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java @@ -28,6 +28,7 @@ import android.os.Build; import android.provider.CallLog.Calls; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.support.v4.os.UserManagerCompat; import android.telephony.PhoneNumberUtils; @@ -35,7 +36,9 @@ import android.text.TextUtils; import com.android.dialer.app.R; import com.android.dialer.calllogutils.PhoneNumberDisplayUtil; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.database.Selection; import com.android.dialer.compat.android.provider.VoicemailCompat; +import com.android.dialer.configprovider.ConfigProviderBindings; import com.android.dialer.location.GeoUtil; import com.android.dialer.phonenumbercache.ContactInfo; import com.android.dialer.phonenumbercache.ContactInfoHelper; @@ -43,11 +46,16 @@ import com.android.dialer.util.PermissionsUtil; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; /** Helper class operating on call log notifications. */ @TargetApi(Build.VERSION_CODES.M) public class CallLogNotificationsQueryHelper { + @VisibleForTesting + static final String CONFIG_NEW_VOICEMAIL_NOTIFICATION_THRESHOLD_OFFSET = + "new_voicemail_notification_threshold"; + private final Context mContext; private final NewCallsQuery mNewCallsQuery; private final ContactInfoHelper mContactInfoHelper; @@ -147,7 +155,12 @@ public class CallLogNotificationsQueryHelper { */ @Nullable public List<NewCall> getNewVoicemails() { - return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE); + return mNewCallsQuery.query( + Calls.VOICEMAIL_TYPE, + System.currentTimeMillis() + - ConfigProviderBindings.get(mContext) + .getLong( + CONFIG_NEW_VOICEMAIL_NOTIFICATION_THRESHOLD_OFFSET, TimeUnit.DAYS.toMillis(7))); } /** @@ -220,10 +233,21 @@ public class CallLogNotificationsQueryHelper { /** Allows determining the new calls for which a notification should be generated. */ public interface NewCallsQuery { + long NO_THRESHOLD = Long.MAX_VALUE; + /** Returns the new calls of a certain type for which a notification should be generated. */ @Nullable List<NewCall> query(int type); + /** + * Returns the new calls of a certain type for which a notification should be generated. + * + * @param thresholdMillis New calls added before this timestamp will be considered old, or + * {@link #NO_THRESHOLD} if threshold is not checked. + */ + @Nullable + List<NewCall> query(int type, long thresholdMillis); + /** Returns a {@link NewCall} pointed by the {@code callsUri} */ @Nullable NewCall query(Uri callsUri); @@ -317,6 +341,14 @@ public class CallLogNotificationsQueryHelper { @Nullable @TargetApi(Build.VERSION_CODES.M) public List<NewCall> query(int type) { + return query(type, NO_THRESHOLD); + } + + @Override + @Nullable + @TargetApi(Build.VERSION_CODES.M) + @SuppressWarnings("MissingPermission") + public List<NewCall> query(int type, long thresholdMillis) { if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) { LogUtil.w( "CallLogNotificationsQueryHelper.DefaultNewCallsQuery.query", @@ -328,15 +360,27 @@ public class CallLogNotificationsQueryHelper { // TYPE matches the query type. // IS_READ is not 1. A call might be backed up and restored, so it will be "new" to the // call log, but the user has already read it on another device. - final String selection = - String.format("%s = 1 AND %s = ? AND %s IS NOT 1", Calls.NEW, Calls.TYPE, Calls.IS_READ); - final String[] selectionArgs = new String[] {Integer.toString(type)}; + Selection.Builder selectionBuilder = + Selection.builder() + .and(Selection.column(Calls.NEW).is("= 1")) + .and(Selection.column(Calls.TYPE).is("=", type)) + .and(Selection.column(Calls.IS_READ).is("IS NOT 1")); + if (thresholdMillis != NO_THRESHOLD) { + selectionBuilder = + selectionBuilder.and( + Selection.column(Calls.DATE) + .is("IS NULL") + .buildUpon() + .or(Selection.column(Calls.DATE).is(">=", thresholdMillis)) + .build()); + } + Selection selection = selectionBuilder.build(); try (Cursor cursor = mContentResolver.query( Calls.CONTENT_URI_WITH_VOICEMAIL, (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ? PROJECTION_O : PROJECTION, - selection, - selectionArgs, + selection.getSelection(), + selection.getSelectionArgs(), Calls.DEFAULT_SORT_ORDER)) { if (cursor == null) { return null; diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java index fbd6f4808..641095512 100644 --- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java +++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java @@ -35,7 +35,6 @@ import android.widget.Toast; import com.android.dialer.about.AboutPhoneFragment; import com.android.dialer.app.R; import com.android.dialer.assisteddialing.ConcreteCreator; -import com.android.dialer.assisteddialing.ui.AssistedDialingSettingFragment; import com.android.dialer.blocking.FilteredNumberCompat; import com.android.dialer.common.LogUtil; import com.android.dialer.compat.telephony.TelephonyManagerCompat; @@ -170,7 +169,8 @@ public class DialerSettingsActivity extends AppCompatPreferenceActivity { Header assistedDialingSettingsHeader = new Header(); assistedDialingSettingsHeader.titleRes = com.android.dialer.assisteddialing.ui.R.string.assisted_dialing_setting_title; - assistedDialingSettingsHeader.fragment = AssistedDialingSettingFragment.class.getName(); + assistedDialingSettingsHeader.intent = + new Intent("com.android.dialer.app.settings.SHOW_ASSISTED_DIALING_SETTINGS"); target.add(assistedDialingSettingsHeader); } diff --git a/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml b/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml index 724874750..5266b1376 100644 --- a/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml +++ b/java/com/android/dialer/assisteddialing/ui/AndroidManifest.xml @@ -19,4 +19,17 @@ android:minSdkVersion="23" android:targetSdkVersion="24"/> + <application> + <activity + android:label="@string/assisted_dialing_setting_title" + android:name=".AssistedDialingSettingActivity" + android:parentActivityName="com.android.dialer.app.settings.DialerSettingsActivity" + android:theme="@style/SettingsStyle"> + <intent-filter> + <action android:name="com.android.dialer.app.settings.SHOW_ASSISTED_DIALING_SETTINGS"/> + <category android:name="android.intent.category.DEFAULT"/> + </intent-filter> + </activity> + </application> + </manifest>
\ No newline at end of file diff --git a/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingActivity.java b/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingActivity.java new file mode 100644 index 000000000..ca36745b3 --- /dev/null +++ b/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingActivity.java @@ -0,0 +1,44 @@ +/* + * 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.assisteddialing.ui; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.view.MenuItem; + +/** The Settings Activity for Assisted Dialing. */ +public class AssistedDialingSettingActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + + getFragmentManager() + .beginTransaction() + .replace(android.R.id.content, new AssistedDialingSettingFragment()) + .commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingFragment.java b/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingFragment.java index 03418940a..d4fb3f64b 100644 --- a/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingFragment.java +++ b/java/com/android/dialer/assisteddialing/ui/AssistedDialingSettingFragment.java @@ -22,15 +22,18 @@ import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.SwitchPreference; -import android.text.TextUtils; +import android.telephony.TelephonyManager; +import com.android.dialer.assisteddialing.AssistedDialingMediator; import com.android.dialer.assisteddialing.ConcreteCreator; import com.android.dialer.assisteddialing.CountryCodeProvider; +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.google.auto.value.AutoValue; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** The setting for Assisted Dialing */ @TargetApi(VERSION_CODES.N) @@ -38,6 +41,7 @@ import java.util.List; public class AssistedDialingSettingFragment extends PreferenceFragment { private CountryCodeProvider countryCodeProvider; + private AssistedDialingMediator assistedDialingMediator; @AutoValue abstract static class DisplayNameAndCountryCodeTuple { @@ -59,6 +63,10 @@ public class AssistedDialingSettingFragment extends PreferenceFragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + assistedDialingMediator = + ConcreteCreator.createNewAssistedDialingMediator( + getContext().getSystemService(TelephonyManager.class), getContext()); + countryCodeProvider = ConcreteCreator.getCountryCodeProvider(ConfigProviderBindings.get(getContext())); @@ -73,14 +81,39 @@ public class AssistedDialingSettingFragment extends PreferenceFragment { findPreference(getContext().getString(R.string.assisted_dialing_setting_cc_key)); updateCountryChoices(countryChooserPref); + updateCountryChooserSummary(countryChooserPref); - if (!TextUtils.isEmpty(countryChooserPref.getEntry())) { - countryChooserPref.setSummary(countryChooserPref.getEntry()); - } countryChooserPref.setOnPreferenceChangeListener(this::updateListSummary); switchPref.setOnPreferenceChangeListener(this::logIfUserDisabledFeature); } + private void updateCountryChooserSummary(ListPreference countryChooserPref) { + String defaultSummaryText = countryChooserPref.getEntries()[0].toString(); + + if (countryChooserPref.getEntry().equals(defaultSummaryText)) { + Optional<String> userHomeCountryCode = assistedDialingMediator.userHomeCountryCode(); + if (userHomeCountryCode.isPresent()) { + CharSequence[] entries = countryChooserPref.getEntries(); + try { + CharSequence regionalDisplayName = + entries[countryChooserPref.findIndexOfValue(userHomeCountryCode.get())]; + countryChooserPref.setSummary( + getContext() + .getString( + R.string.assisted_dialing_setting_cc_default_summary, regionalDisplayName)); + } catch (ArrayIndexOutOfBoundsException e) { + // This might happen if there is a mismatch between the automatically + // detected home country, and the countries currently eligible to select in the settings. + LogUtil.i( + "AssistedDialingSettingFragment.onCreate", + "Failed to find human readable mapping for country code, using default."); + } + } + } else { + countryChooserPref.setSummary(countryChooserPref.getEntry()); + } + } + /** * Filters the default entries in the country chooser by only showing those countries in which the * feature in enabled. diff --git a/java/com/android/dialer/assisteddialing/ui/res/values/strings.xml b/java/com/android/dialer/assisteddialing/ui/res/values/strings.xml index 3a81780a9..35aa2f147 100644 --- a/java/com/android/dialer/assisteddialing/ui/res/values/strings.xml +++ b/java/com/android/dialer/assisteddialing/ui/res/values/strings.xml @@ -23,7 +23,10 @@ <string name="assisted_dialing_setting_summary">Predict and add a country code when you call while traveling abroad</string> <!-- Indicates the default state for the home country selector--> - <string name="assisted_dialing_setting_cc_default_summary">Automatically detected</string> + <string name="assisted_dialing_setting_cc_default_summary">Automatically detected • <xliff:g example="United Kingdom (+44)" id="ad_country_code_info">%1$s</xliff:g></string> + + <!-- Indicates the default failure state for the home country selector--> + <string name="assisted_dialing_setting_cc_default_summary_fallback">Automatically detected</string> <!-- Category title for the country code picker in assisted dialing [CHAR LIMIT=40]--> <string name="assisted_dialing_setting_cc_category_title">Home country</string> @@ -36,7 +39,7 @@ <!-- Excluding ['Antarctica', 'Bouvet Island', 'French Southern Territories (the)', 'Heard Island and McDonald Islands', 'Pitcairn', 'South Georgia and the South Sandwich Islands', 'United States Minor Outlying Islands (the)'] --> <!-- Options for the country codes used in assisted dialing. DO NOT TRANSLATE NUMBERS. [CHAR LIMIT=40] --> <string-array name="assisted_dialing_cc_entries"> - <item>@string/assisted_dialing_setting_cc_default_summary</item> + <item>@string/assisted_dialing_setting_cc_default_summary_fallback</item> <item>Afghanistan <xliff:g>(+93)</xliff:g></item> <item>Åland Islands <xliff:g>(+358)</xliff:g></item> <item>Albania <xliff:g>(+355)</xliff:g></item> 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 c1706b2ed..9fb61a159 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 @@ -31,7 +31,7 @@ android:entries="@array/assisted_dialing_cc_entries" android:entryValues="@array/assisted_dialing_cc_values" android:key="@string/assisted_dialing_setting_cc_key" - android:summary="@string/assisted_dialing_setting_cc_default_summary" + android:summary="@string/assisted_dialing_setting_cc_default_summary_fallback" android:title="@string/assisted_dialing_setting_cc_title"/> </PreferenceCategory> diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java index 06b6a1040..b51d833dc 100644 --- a/java/com/android/dialer/calldetails/CallDetailsActivity.java +++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java @@ -35,7 +35,6 @@ import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.widget.Toast; import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; -import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.Assert; @@ -46,7 +45,7 @@ import com.android.dialer.dialercontact.DialerContact; import com.android.dialer.duo.Duo; import com.android.dialer.duo.DuoComponent; import com.android.dialer.enrichedcall.EnrichedCallComponent; -import com.android.dialer.enrichedcall.EnrichedCallManager.HistoricalDataChangedListener; +import com.android.dialer.enrichedcall.EnrichedCallManager; import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; @@ -55,17 +54,14 @@ import com.android.dialer.performancereport.PerformanceReport; import com.android.dialer.postcall.PostCall; import com.android.dialer.precall.PreCall; import com.android.dialer.protos.ProtoParsers; +import com.google.common.base.Preconditions; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.List; import java.util.Map; /** Displays the details of a specific call log entry. */ -public class CallDetailsActivity extends AppCompatActivity - implements CallDetailsHeaderViewHolder.CallbackActionListener, - CallDetailsFooterViewHolder.ReportCallIdListener, - DeleteCallDetailsListener, - HistoricalDataChangedListener { +public class CallDetailsActivity extends AppCompatActivity { public static final String EXTRA_PHONE_NUMBER = "phone_number"; public static final String EXTRA_HAS_ENRICHED_CALL_DATA = "has_enriched_call_data"; @@ -73,7 +69,16 @@ public class CallDetailsActivity extends AppCompatActivity public static final String EXTRA_CONTACT = "contact"; public static final String EXTRA_CAN_REPORT_CALLER_ID = "can_report_caller_id"; private static final String EXTRA_CAN_SUPPORT_ASSISTED_DIALING = "can_support_assisted_dialing"; - private static final String TASK_DELETE = "task_delete"; + + private final CallDetailsHeaderViewHolder.CallbackActionListener callbackActionListener = + new CallbackActionListener(this); + private final CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener = + new DeleteCallDetailsListener(this); + private final CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener = + new ReportCallIdListener(this); + private final EnrichedCallManager.HistoricalDataChangedListener + enrichedCallHistoricalDataChangedListener = + new EnrichedCallHistoricalDataChangedListener(this); private CallDetailsEntries entries; private DialerContact contact; @@ -130,7 +135,7 @@ public class CallDetailsActivity extends AppCompatActivity EnrichedCallComponent.get(this) .getEnrichedCallManager() - .registerHistoricalDataChangedListener(this); + .registerHistoricalDataChangedListener(enrichedCallHistoricalDataChangedListener); EnrichedCallComponent.get(this) .getEnrichedCallManager() .requestAllHistoricalData(contact.getNumber(), entries); @@ -142,7 +147,7 @@ public class CallDetailsActivity extends AppCompatActivity EnrichedCallComponent.get(this) .getEnrichedCallManager() - .unregisterHistoricalDataChangedListener(this); + .unregisterHistoricalDataChangedListener(enrichedCallHistoricalDataChangedListener); } @Override @@ -161,9 +166,9 @@ public class CallDetailsActivity extends AppCompatActivity this /* context */, contact, entries.getEntriesList(), - this /* callbackListener */, - this /* reportCallIdListener */, - this /* callDetailDeletionListener */); + callbackActionListener, + reportCallIdListener, + deleteCallDetailsListener); RecyclerView recyclerView = findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(this)); @@ -177,113 +182,6 @@ public class CallDetailsActivity extends AppCompatActivity super.onBackPressed(); } - @Override - public void reportCallId(String number) { - ReportDialogFragment.newInstance(number).show(getFragmentManager(), null); - } - - @Override - public boolean canReportCallerId(String number) { - return getIntent().getExtras().getBoolean(EXTRA_CAN_REPORT_CALLER_ID, false); - } - - @Override - public void onHistoricalDataChanged() { - Map<CallDetailsEntry, List<HistoryResult>> mappedResults = - getAllHistoricalData(contact.getNumber(), entries); - - adapter.updateCallDetailsEntries( - generateAndMapNewCallDetailsEntriesHistoryResults( - contact.getNumber(), entries, mappedResults) - .getEntriesList()); - } - - @Override - public void placeImsVideoCall(String phoneNumber) { - Logger.get(this).logImpression(DialerImpression.Type.CALL_DETAILS_IMS_VIDEO_CALL_BACK); - PreCall.start( - this, - new CallIntentBuilder(phoneNumber, CallInitiationType.Type.CALL_DETAILS) - .setIsVideoCall(true)); - } - - @Override - public void placeDuoVideoCall(String phoneNumber) { - Logger.get(this).logImpression(DialerImpression.Type.CALL_DETAILS_LIGHTBRINGER_CALL_BACK); - Duo duo = DuoComponent.get(this).getDuo(); - if (!duo.isReachable(this, phoneNumber)) { - placeImsVideoCall(phoneNumber); - return; - } - - try { - startActivityForResult(duo.getIntent(this, phoneNumber), ActivityRequestCodes.DIALTACTS_DUO); - } catch (ActivityNotFoundException e) { - Toast.makeText(this, R.string.activity_not_available, Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void placeVoiceCall(String phoneNumber, String postDialDigits) { - Logger.get(this).logImpression(DialerImpression.Type.CALL_DETAILS_VOICE_CALL_BACK); - - boolean canSupportedAssistedDialing = - getIntent().getExtras().getBoolean(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, false); - CallIntentBuilder callIntentBuilder = - new CallIntentBuilder(phoneNumber + postDialDigits, CallInitiationType.Type.CALL_DETAILS); - if (canSupportedAssistedDialing) { - callIntentBuilder.setAllowAssistedDial(true); - } - - PreCall.start(this, callIntentBuilder); - } - - @Override - public void delete() { - AsyncTaskExecutors.createAsyncTaskExecutor() - .submit(TASK_DELETE, new DeleteCallsTask(this, contact, entries)); - } - - @NonNull - private Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData( - @Nullable String number, @NonNull CallDetailsEntries entries) { - if (number == null) { - return Collections.emptyMap(); - } - - Map<CallDetailsEntry, List<HistoryResult>> historicalData = - EnrichedCallComponent.get(this) - .getEnrichedCallManager() - .getAllHistoricalData(number, entries); - if (historicalData == null) { - return Collections.emptyMap(); - } - return historicalData; - } - - private static CallDetailsEntries generateAndMapNewCallDetailsEntriesHistoryResults( - @Nullable String number, - @NonNull CallDetailsEntries callDetailsEntries, - @NonNull Map<CallDetailsEntry, List<HistoryResult>> mappedResults) { - if (number == null) { - return callDetailsEntries; - } - CallDetailsEntries.Builder mutableCallDetailsEntries = CallDetailsEntries.newBuilder(); - for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { - CallDetailsEntry.Builder newEntry = CallDetailsEntry.newBuilder().mergeFrom(entry); - List<HistoryResult> results = mappedResults.get(entry); - if (results != null) { - newEntry.addAllHistoryResults(mappedResults.get(entry)); - LogUtil.v( - "CallLogAdapter.generateAndMapNewCallDetailsEntriesHistoryResults", - "mapped %d results", - newEntry.getHistoryResultsList().size()); - } - mutableCallDetailsEntries.addEntries(newEntry.build()); - } - return mutableCallDetailsEntries.build(); - } - /** Delete specified calls from the call log. */ private static class DeleteCallsTask extends AsyncTask<Void, Void, Void> { // Use a weak reference to hold the Activity so that there is no memory leak. @@ -349,4 +247,176 @@ public class CallDetailsActivity extends AppCompatActivity activity.finish(); } } + + private static final class CallbackActionListener + implements CallDetailsHeaderViewHolder.CallbackActionListener { + private final WeakReference<Activity> activityWeakReference; + + CallbackActionListener(Activity activity) { + this.activityWeakReference = new WeakReference<>(activity); + } + + @Override + public void placeImsVideoCall(String phoneNumber) { + Logger.get(getActivity()) + .logImpression(DialerImpression.Type.CALL_DETAILS_IMS_VIDEO_CALL_BACK); + PreCall.start( + getActivity(), + new CallIntentBuilder(phoneNumber, CallInitiationType.Type.CALL_DETAILS) + .setIsVideoCall(true)); + } + + @Override + public void placeDuoVideoCall(String phoneNumber) { + Logger.get(getActivity()) + .logImpression(DialerImpression.Type.CALL_DETAILS_LIGHTBRINGER_CALL_BACK); + Duo duo = DuoComponent.get(getActivity()).getDuo(); + if (!duo.isReachable(getActivity(), phoneNumber)) { + placeImsVideoCall(phoneNumber); + return; + } + + try { + getActivity() + .startActivityForResult( + duo.getIntent(getActivity(), phoneNumber), ActivityRequestCodes.DIALTACTS_DUO); + } catch (ActivityNotFoundException e) { + Toast.makeText(getActivity(), R.string.activity_not_available, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void placeVoiceCall(String phoneNumber, String postDialDigits) { + Logger.get(getActivity()).logImpression(DialerImpression.Type.CALL_DETAILS_VOICE_CALL_BACK); + + boolean canSupportedAssistedDialing = + getActivity() + .getIntent() + .getExtras() + .getBoolean(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, false); + CallIntentBuilder callIntentBuilder = + new CallIntentBuilder(phoneNumber + postDialDigits, CallInitiationType.Type.CALL_DETAILS); + if (canSupportedAssistedDialing) { + callIntentBuilder.setAllowAssistedDial(true); + } + + PreCall.start(getActivity(), callIntentBuilder); + } + + private Activity getActivity() { + return Preconditions.checkNotNull(activityWeakReference.get()); + } + } + + private static final class DeleteCallDetailsListener + implements CallDetailsFooterViewHolder.DeleteCallDetailsListener { + private static final String ASYNC_TASK_ID = "task_delete"; + + private final WeakReference<CallDetailsActivity> activityWeakReference; + + DeleteCallDetailsListener(CallDetailsActivity activity) { + this.activityWeakReference = new WeakReference<>(activity); + } + + @Override + public void delete() { + AsyncTaskExecutors.createAsyncTaskExecutor() + .submit( + ASYNC_TASK_ID, + new DeleteCallsTask(getActivity(), getActivity().contact, getActivity().entries)); + } + + private CallDetailsActivity getActivity() { + return Preconditions.checkNotNull(activityWeakReference.get()); + } + } + + private static final class ReportCallIdListener + implements CallDetailsFooterViewHolder.ReportCallIdListener { + private final WeakReference<Activity> activityWeakReference; + + ReportCallIdListener(Activity activity) { + this.activityWeakReference = new WeakReference<>(activity); + } + + @Override + public void reportCallId(String number) { + ReportDialogFragment.newInstance(number) + .show(getActivity().getFragmentManager(), null /* tag */); + } + + @Override + public boolean canReportCallerId(String number) { + return getActivity().getIntent().getExtras().getBoolean(EXTRA_CAN_REPORT_CALLER_ID, false); + } + + private Activity getActivity() { + return Preconditions.checkNotNull(activityWeakReference.get()); + } + } + + private static final class EnrichedCallHistoricalDataChangedListener + implements EnrichedCallManager.HistoricalDataChangedListener { + private final WeakReference<CallDetailsActivity> activityWeakReference; + + EnrichedCallHistoricalDataChangedListener(CallDetailsActivity activity) { + this.activityWeakReference = new WeakReference<>(activity); + } + + @Override + public void onHistoricalDataChanged() { + CallDetailsActivity activity = getActivity(); + Map<CallDetailsEntry, List<HistoryResult>> mappedResults = + getAllHistoricalData(activity.contact.getNumber(), activity.entries); + + activity.adapter.updateCallDetailsEntries( + generateAndMapNewCallDetailsEntriesHistoryResults( + activity.contact.getNumber(), activity.entries, mappedResults) + .getEntriesList()); + } + + private CallDetailsActivity getActivity() { + return Preconditions.checkNotNull(activityWeakReference.get()); + } + + @NonNull + private Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData( + @Nullable String number, @NonNull CallDetailsEntries entries) { + if (number == null) { + return Collections.emptyMap(); + } + + Map<CallDetailsEntry, List<HistoryResult>> historicalData = + EnrichedCallComponent.get(getActivity()) + .getEnrichedCallManager() + .getAllHistoricalData(number, entries); + if (historicalData == null) { + return Collections.emptyMap(); + } + return historicalData; + } + + private static CallDetailsEntries generateAndMapNewCallDetailsEntriesHistoryResults( + @Nullable String number, + @NonNull CallDetailsEntries callDetailsEntries, + @NonNull Map<CallDetailsEntry, List<HistoryResult>> mappedResults) { + if (number == null) { + return callDetailsEntries; + } + CallDetailsEntries.Builder mutableCallDetailsEntries = CallDetailsEntries.newBuilder(); + for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { + CallDetailsEntry.Builder newEntry = CallDetailsEntry.newBuilder().mergeFrom(entry); + List<HistoryResult> results = mappedResults.get(entry); + if (results != null) { + newEntry.addAllHistoryResults(mappedResults.get(entry)); + LogUtil.v( + "CallDetailsActivity.generateAndMapNewCallDetailsEntriesHistoryResults", + "mapped %d results", + newEntry.getHistoryResultsList().size()); + } + mutableCallDetailsEntries.addEntries(newEntry.build()); + } + return mutableCallDetailsEntries.build(); + } + } } diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java index 0d8e8ceeb..8c6d58634 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java @@ -45,6 +45,7 @@ class AnnotatedCallLogDatabaseHelper extends SQLiteOpenHelper { + (AnnotatedCallLog.PHOTO_ID + " integer, ") + (AnnotatedCallLog.LOOKUP_URI + " text, ") + (AnnotatedCallLog.DURATION + " integer, ") + + (AnnotatedCallLog.DATA_USAGE + " integer, ") + (AnnotatedCallLog.NUMBER_TYPE_LABEL + " text, ") + (AnnotatedCallLog.IS_READ + " integer, ") + (AnnotatedCallLog.NEW + " integer, ") diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java index c9b463e74..9efe21487 100644 --- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java +++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java @@ -225,6 +225,13 @@ public class AnnotatedCallLogContract { public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/annotated_call_log"; /** + * See {@link android.provider.CallLog.Calls#DATA_USAGE}. + * + * <p>Type: INTEGER (long) + */ + public static final String DATA_USAGE = "data_usage"; + + /** * See {@link android.provider.CallLog.Calls#DURATION}. * * <p>TYPE: INTEGER (long) diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index 7a7f2070b..010cb8541 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -31,6 +31,7 @@ import com.android.dialer.calllog.datasources.CallLogMutations; import com.android.dialer.common.LogUtil; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; +import com.android.dialer.phonelookup.PhoneLookupSelector; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.google.common.collect.ImmutableMap; @@ -376,11 +377,7 @@ public final class PhoneLookupDataSource implements CallLogDataSource { } } - // TODO(zachh): Extract this logic into a proper selection class or package. private static String selectName(PhoneLookupInfo phoneLookupInfo) { - if (phoneLookupInfo.getCp2Info().getCp2ContactInfoCount() > 0) { - return phoneLookupInfo.getCp2Info().getCp2ContactInfo(0).getName(); - } - return ""; + return PhoneLookupSelector.selectName(phoneLookupInfo); } } diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index 5ca160778..ef40c308e 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -220,6 +220,7 @@ public class SystemCallLogDataSource implements CallLogDataSource { Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_LABEL, Calls.DURATION, + Calls.DATA_USAGE, Calls.TRANSCRIPTION, Calls.VOICEMAIL_URI, Calls.IS_READ, @@ -259,6 +260,7 @@ public class SystemCallLogDataSource implements CallLogDataSource { int cachedNumberTypeColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_TYPE); int cachedNumberLabelColumn = cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_LABEL); int durationsColumn = cursor.getColumnIndexOrThrow(Calls.DURATION); + int dataUsageColumn = cursor.getColumnIndexOrThrow(Calls.DATA_USAGE); int transcriptionColumn = cursor.getColumnIndexOrThrow(Calls.TRANSCRIPTION); int voicemailUriColumn = cursor.getColumnIndexOrThrow(Calls.VOICEMAIL_URI); int isReadColumn = cursor.getColumnIndexOrThrow(Calls.IS_READ); @@ -286,6 +288,7 @@ public class SystemCallLogDataSource implements CallLogDataSource { int cachedNumberType = cursor.getInt(cachedNumberTypeColumn); String cachedNumberLabel = cursor.getString(cachedNumberLabelColumn); int duration = cursor.getInt(durationsColumn); + int dataUsage = cursor.getInt(dataUsageColumn); String transcription = cursor.getString(transcriptionColumn); String voicemailUri = cursor.getString(voicemailUriColumn); int isRead = cursor.getInt(isReadColumn); @@ -334,6 +337,7 @@ public class SystemCallLogDataSource implements CallLogDataSource { appContext, contentValues, phoneAccountComponentName, phoneAccountId); contentValues.put(AnnotatedCallLog.FEATURES, features); contentValues.put(AnnotatedCallLog.DURATION, duration); + contentValues.put(AnnotatedCallLog.DATA_USAGE, dataUsage); contentValues.put(AnnotatedCallLog.TRANSCRIPTION, transcription); contentValues.put(AnnotatedCallLog.VOICEMAIL_URI, voicemailUri); diff --git a/java/com/android/dialer/phonelookup/PhoneLookupSelector.java b/java/com/android/dialer/phonelookup/PhoneLookupSelector.java new file mode 100644 index 000000000..a746ea44f --- /dev/null +++ b/java/com/android/dialer/phonelookup/PhoneLookupSelector.java @@ -0,0 +1,47 @@ +/* + * 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.phonelookup; + +import android.support.annotation.NonNull; + +/** + * Prioritizes information from a {@link PhoneLookupInfo}. + * + * <p>For example, a single {@link PhoneLookupInfo} may contain different name information from many + * different {@link PhoneLookup} sources. This class defines the rules for deciding which name + * should be selected for display to the user, by prioritizing the data from some {@link PhoneLookup + * PhoneLookups} over others. + * + * <p>Note that the logic in this class may be highly coupled with the logic in {@code + * CompositePhoneLookup}, because {@code CompositePhoneLookup} may also include prioritization logic + * for short-circuiting low-priority {@link PhoneLookup PhoneLookups}. + */ +public final class PhoneLookupSelector { + + /** + * Select the name associated with this number. Examples of this are a local contact's name or a + * business name received from caller ID. + */ + @NonNull + public static String selectName(PhoneLookupInfo phoneLookupInfo) { + if (phoneLookupInfo.getCp2Info().getCp2ContactInfoCount() > 0) { + // Arbitrarily select the first contact's name. In the future, it may make sense to join the + // names such as "Mom, Dad" in the case that multiple contacts share the same number. + return phoneLookupInfo.getCp2Info().getCp2ContactInfo(0).getName(); + } + return ""; + } +} diff --git a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java index 520c46f9e..f432e27ae 100644 --- a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java +++ b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java @@ -53,6 +53,8 @@ public final class CompositePhoneLookup implements PhoneLookup { */ @Override public ListenableFuture<PhoneLookupInfo> lookup(@NonNull Call call) { + // TODO(zachh): Add short-circuiting logic so that this call is not blocked on low-priority + // lookups finishing when a higher-priority one has already finished. List<ListenableFuture<PhoneLookupInfo>> futures = new ArrayList<>(); for (PhoneLookup phoneLookup : phoneLookups) { futures.add(phoneLookup.lookup(call)); diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java index 307f998ea..cfb8fb7b8 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java @@ -82,7 +82,8 @@ public final class Cp2PhoneLookup implements PhoneLookup { @Override public ListenableFuture<PhoneLookupInfo> lookup(@NonNull Call call) { - throw new UnsupportedOperationException(); + // TODO(zachh): Implementation. + return MoreExecutors.newDirectExecutorService().submit(PhoneLookupInfo::getDefaultInstance); } @Override diff --git a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java index 27041e746..e85654e99 100644 --- a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java +++ b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java @@ -201,6 +201,12 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { return rows; } + /** + * Note: If the normalized number is included as part of the URI (for example, + * "content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+123") then the update + * operation will actually be a "replace" operation, inserting a new row if one does not already + * exist. + */ @Override public int update( @NonNull Uri uri, @@ -214,7 +220,13 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { int match = uriMatcher.match(uri); switch (match) { case PHONE_LOOKUP_HISTORY_TABLE_CODE: - break; + int rows = database.update(PhoneLookupHistory.TABLE, values, selection, selectionArgs); + if (rows == 0) { + LogUtil.w("PhoneLookupHistoryContentProvider.update", "no rows updated"); + return rows; + } + notifyChange(uri); + return rows; case PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: Assert.checkArgument( !values.containsKey(PhoneLookupHistory.NORMALIZED_NUMBER), @@ -222,19 +234,15 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { Assert.checkArgument(selection == null, "Do not specify selection when updating by ID"); Assert.checkArgument( selectionArgs == null, "Do not specify selection args when updating by ID"); - selection = PhoneLookupHistory.NORMALIZED_NUMBER + "= ?"; - selectionArgs = new String[] {uri.getLastPathSegment()}; - break; + + String normalizedNumber = uri.getLastPathSegment(); + values.put(PhoneLookupHistory.NORMALIZED_NUMBER, normalizedNumber); + database.replace(PhoneLookupHistory.TABLE, null, values); + notifyChange(uri); + return 1; default: throw new IllegalArgumentException("Unknown uri: " + uri); } - int rows = database.update(PhoneLookupHistory.TABLE, values, selection, selectionArgs); - if (rows == 0) { - LogUtil.w("PhoneLookupHistoryContentProvider.update", "no rows updated"); - return rows; - } - notifyChange(uri); - return rows; } private void notifyChange(Uri uri) { diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java index c79d9013d..6f424de4a 100644 --- a/java/com/android/dialer/telecom/TelecomUtil.java +++ b/java/com/android/dialer/telecom/TelecomUtil.java @@ -158,7 +158,11 @@ public abstract class TelecomUtil { return Optional.absent(); } SubscriptionManager subscriptionManager = context.getSystemService(SubscriptionManager.class); - for (SubscriptionInfo info : subscriptionManager.getActiveSubscriptionInfoList()) { + List<SubscriptionInfo> subscriptionInfos = subscriptionManager.getActiveSubscriptionInfoList(); + if (subscriptionInfos == null) { + return Optional.absent(); + } + for (SubscriptionInfo info : subscriptionInfos) { if (phoneAccountHandle.getId().startsWith(info.getIccId())) { return Optional.of(info); } diff --git a/java/com/android/incallui/NewReturnToCallController.java b/java/com/android/incallui/NewReturnToCallController.java index 399b18568..abff000fd 100644 --- a/java/com/android/incallui/NewReturnToCallController.java +++ b/java/com/android/incallui/NewReturnToCallController.java @@ -29,8 +29,6 @@ import com.android.contacts.common.util.ContactDisplayUtils; import com.android.dialer.common.LogUtil; import com.android.dialer.configprovider.ConfigProviderBindings; import com.android.dialer.lettertile.LetterTileDrawable; -import com.android.dialer.logging.DialerImpression; -import com.android.dialer.logging.Logger; import com.android.dialer.telecom.TelecomUtil; import com.android.incallui.ContactInfoCache.ContactCacheEntry; import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; @@ -43,8 +41,6 @@ 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.lang.ref.WeakReference; @@ -102,6 +98,7 @@ public class NewReturnToCallController implements InCallUiListener, Listener, Au } public void tearDown() { + hide(); InCallPresenter.getInstance().removeInCallUiListener(this); CallList.getInstance().removeListener(this); AudioModeProvider.getInstance().removeListener(this); @@ -150,45 +147,6 @@ public class NewReturnToCallController implements InCallUiListener, Listener, Au 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; } diff --git a/java/com/android/incallui/PhoneLookupHistoryRecorder.java b/java/com/android/incallui/PhoneLookupHistoryRecorder.java new file mode 100644 index 000000000..2632e6515 --- /dev/null +++ b/java/com/android/incallui/PhoneLookupHistoryRecorder.java @@ -0,0 +1,116 @@ +/* + * 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.content.ContentValues; +import android.content.Context; +import android.support.annotation.Nullable; +import android.telecom.Call; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.telephony.SubscriptionInfo; +import android.text.TextUtils; +import com.android.dialer.buildtype.BuildType; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutors; +import com.android.dialer.location.CountryDetector; +import com.android.dialer.phonelookup.PhoneLookupComponent; +import com.android.dialer.phonelookup.PhoneLookupInfo; +import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; +import com.android.dialer.telecom.TelecomUtil; +import com.android.incallui.util.TelecomCallUtil; +import com.google.common.base.Optional; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.Locale; + +/** + * Fetches the current {@link PhoneLookupInfo} for the provided call and writes it to the + * PhoneLookupHistory. + */ +final class PhoneLookupHistoryRecorder { + + /** + * If the new UI is enabled, fetches the current {@link PhoneLookupInfo} for the provided call and + * writes it to the PhoneLookupHistory. Otherwise does nothing. + */ + static void recordPhoneLookupInfo(Context appContext, Call call) { + if (!(BuildType.get() == BuildType.BUGFOOD || LogUtil.isDebugEnabled())) { + return; + } + ListenableFuture<PhoneLookupInfo> future = + PhoneLookupComponent.get(appContext).phoneLookup().lookup(call); + Futures.addCallback( + future, + new FutureCallback<PhoneLookupInfo>() { + @Override + public void onSuccess(@Nullable PhoneLookupInfo result) { + Assert.checkArgument(result != null); + Optional<String> normalizedNumber = getNormalizedNumber(appContext, call); + if (!normalizedNumber.isPresent()) { + LogUtil.w("PhoneLookupHistoryRecorder.onSuccess", "couldn't get a number"); + return; + } + ContentValues contentValues = new ContentValues(); + contentValues.put(PhoneLookupHistory.PHONE_LOOKUP_INFO, result.toByteArray()); + contentValues.put(PhoneLookupHistory.LAST_MODIFIED, System.currentTimeMillis()); + appContext + .getContentResolver() + .update( + PhoneLookupHistory.CONTENT_URI + .buildUpon() + .appendEncodedPath(normalizedNumber.get()) + .build(), + contentValues, + null, + null); + } + + @Override + public void onFailure(Throwable t) { + // TODO(zachh): Consider how to best handle this; take measures to repair call log? + LogUtil.w( + "PhoneLookupHistoryRecorder.onFailure", "could not write PhoneLookupHistory", t); + } + }, + DialerExecutors.getLowPriorityThreadPool(appContext)); + } + + private static Optional<String> getNormalizedNumber(Context appContext, Call call) { + PhoneAccountHandle phoneAccountHandle = call.getDetails().getAccountHandle(); + Optional<SubscriptionInfo> subscriptionInfo = + TelecomUtil.getSubscriptionInfo(appContext, phoneAccountHandle); + String countryCode = + subscriptionInfo.isPresent() + ? subscriptionInfo.get().getCountryIso() + : CountryDetector.getInstance(appContext).getCurrentCountryIso(); + if (countryCode == null) { + LogUtil.w( + "PhoneLookupHistoryRecorder.getNormalizedNumber", + "couldn't find a country code for call"); + countryCode = "US"; + } + String rawNumber = TelecomCallUtil.getNumber(call); + if (TextUtils.isEmpty(rawNumber)) { + return Optional.absent(); + } + String normalizedNumber = + PhoneNumberUtils.formatNumberToE164(rawNumber, countryCode.toUpperCase(Locale.US)); + return normalizedNumber == null ? Optional.of(rawNumber) : Optional.of(normalizedNumber); + } +} diff --git a/java/com/android/incallui/ReturnToCallController.java b/java/com/android/incallui/ReturnToCallController.java index 5f4cc5f84..58d868818 100644 --- a/java/com/android/incallui/ReturnToCallController.java +++ b/java/com/android/incallui/ReturnToCallController.java @@ -84,6 +84,7 @@ public class ReturnToCallController implements InCallUiListener, Listener, Audio } public void tearDown() { + hide(); InCallPresenter.getInstance().removeInCallUiListener(this); CallList.getInstance().removeListener(this); AudioModeProvider.getInstance().removeListener(this); diff --git a/java/com/android/newbubble/NewBubble.java b/java/com/android/newbubble/NewBubble.java index e690f4be4..ef3a971dd 100644 --- a/java/com/android/newbubble/NewBubble.java +++ b/java/com/android/newbubble/NewBubble.java @@ -17,12 +17,15 @@ package com.android.newbubble; import android.animation.Animator; +import android.animation.Animator.AnimatorListener; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.PendingIntent.CanceledException; import android.content.Context; import android.content.Intent; +import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; @@ -51,10 +54,16 @@ import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.animation.AnticipateInterpolator; import android.view.animation.OvershootInterpolator; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import android.widget.ViewAnimator; +import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; import com.android.dialer.util.DrawableConverter; +import com.android.incallui.call.CallList; +import com.android.incallui.call.DialerCall; import com.android.newbubble.NewBubbleInfo.Action; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -96,11 +105,14 @@ public class NewBubble { private CharSequence textAfterShow; private int collapseEndAction; - @VisibleForTesting ViewHolder viewHolder; + ViewHolder viewHolder; private ViewPropertyAnimator collapseAnimation; private Integer overrideGravity; private ViewPropertyAnimator exitAnimator; + private int leftBoundary; + private int savedYPosition = -1; + private final Runnable collapseRunnable = new Runnable() { @Override @@ -110,17 +122,11 @@ public class NewBubble { // Always reset here since text shouldn't keep showing. hideAndReset(); } else { - doResize( - () -> - viewHolder - .getPrimaryButton() - .setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON)); + viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_AVATAR_AND_ICON); } } }; - private BubbleExpansionStateListener bubbleExpansionStateListener; - /** Type of action after bubble collapse */ @Retention(RetentionPolicy.SOURCE) @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE}) @@ -206,15 +212,20 @@ public class NewBubble { windowManager = context.getSystemService(WindowManager.class); viewHolder = new ViewHolder(context); + + leftBoundary = + context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal) + - context + .getResources() + .getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal); } /** Expands the main bubble menu. */ public void expand(boolean isUserAction) { - if (bubbleExpansionStateListener != null) { - bubbleExpansionStateListener.onBubbleExpansionStateChanged( - ExpansionState.START_EXPANDING, isUserAction); + if (isUserAction) { + logBasicOrCallImpression(DialerImpression.Type.BUBBLE_PRIMARY_BUTTON_EXPAND); } - doResize(() -> viewHolder.setDrawerVisibility(View.VISIBLE)); + viewHolder.setDrawerVisibility(View.INVISIBLE); View expandedView = viewHolder.getExpandedView(); expandedView .getViewTreeObserver() @@ -222,13 +233,62 @@ public class NewBubble { new OnPreDrawListener() { @Override public boolean onPreDraw() { - // Animate expanded view to move from above primary button to its final position + // Move the whole bubble up so that expanded view is still in screen + int moveUpDistance = viewHolder.getMoveUpDistance(); + if (moveUpDistance != 0) { + savedYPosition = windowParams.y; + } + + // Calculate the move-to-middle distance + int deltaX = + (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX(); + float k = (float) moveUpDistance / deltaX; + if (isDrawingFromRight()) { + deltaX = -deltaX; + } + + // Do X-move and Y-move together + + final int startX = windowParams.x - deltaX; + final int startY = windowParams.y; + ValueAnimator animator = ValueAnimator.ofFloat(startX, windowParams.x); + animator.setInterpolator(new LinearOutSlowInInterpolator()); + animator.addUpdateListener( + (valueAnimator) -> { + // Update windowParams and the root layout. + // We can't do ViewPropertyAnimation since it clips children. + float newX = (float) valueAnimator.getAnimatedValue(); + if (moveUpDistance != 0) { + windowParams.y = startY - (int) (Math.abs(newX - (float) startX) * k); + } + windowParams.x = (int) newX; + windowManager.updateViewLayout(viewHolder.getRoot(), windowParams); + }); + animator.addListener( + new AnimatorListener() { + @Override + public void onAnimationEnd(Animator animation) { + // Show expanded view + expandedView.setVisibility(View.VISIBLE); + expandedView.setTranslationY(-expandedView.getHeight()); + expandedView + .animate() + .setInterpolator(new LinearOutSlowInInterpolator()) + .translationY(0); + } + + @Override + public void onAnimationStart(Animator animation) {} + + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationRepeat(Animator animation) {} + }); + animator.start(); + expandedView.getViewTreeObserver().removeOnPreDrawListener(this); - expandedView.setTranslationY(-viewHolder.getRoot().getHeight()); - expandedView - .animate() - .setInterpolator(new LinearOutSlowInInterpolator()) - .translationY(0); return false; } }); @@ -236,6 +296,115 @@ public class NewBubble { expanded = true; } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public void startCollapse( + @CollapseEnd int endAction, boolean isUserAction, boolean shouldRecoverYPosition) { + 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 (isUserAction && collapseEndAction == CollapseEnd.NOTHING) { + logBasicOrCallImpression(DialerImpression.Type.BUBBLE_COLLAPSE_BY_USER); + } + // Animate expanded view to move from its position to above primary button and hide + collapseAnimation = + expandedView + .animate() + .translationY(-expandedView.getHeight()) + .setInterpolator(new FastOutLinearInInterpolator()) + .withEndAction( + () -> { + collapseAnimation = null; + expanded = false; + + if (textShowing) { + // Will do resize once the text is done. + return; + } + + // Set drawer visibility to INVISIBLE instead of GONE to keep primary button fixed + viewHolder.setDrawerVisibility(View.INVISIBLE); + + // Do X-move and Y-move together + int deltaX = + (int) viewHolder.getRoot().findViewById(R.id.bubble_primary_container).getX(); + int startX = windowParams.x; + int startY = windowParams.y; + float k = + (savedYPosition != -1 && shouldRecoverYPosition) + ? (savedYPosition - startY) / (float) deltaX + : 0; + Path path = new Path(); + path.moveTo(windowParams.x, windowParams.y); + path.lineTo( + windowParams.x - deltaX, + (savedYPosition != -1 && shouldRecoverYPosition) + ? savedYPosition + : windowParams.y); + // The position is not useful after collapse + savedYPosition = -1; + + ValueAnimator animator = ValueAnimator.ofFloat(startX, startX - deltaX); + animator.setInterpolator(new LinearOutSlowInInterpolator()); + animator.addUpdateListener( + (valueAnimator) -> { + // Update windowParams and the root layout. + // We can't do ViewPropertyAnimation since it clips children. + float newX = (float) valueAnimator.getAnimatedValue(); + if (k != 0) { + windowParams.y = startY + (int) (Math.abs(newX - (float) startX) * k); + } + windowParams.x = (int) newX; + windowManager.updateViewLayout(viewHolder.getRoot(), windowParams); + }); + animator.addListener( + new AnimatorListener() { + @Override + public void onAnimationEnd(Animator animation) { + // If collapse on the right side, the primary button move left a bit after + // drawer + // visibility becoming GONE. To avoid it, we create a new ViewHolder. + replaceViewHolder(); + } + + @Override + public void onAnimationStart(Animator animation) {} + + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationRepeat(Animator animation) {} + }); + animator.start(); + + // 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); + }); + } + /** * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is * already showing this method does nothing. @@ -269,8 +438,7 @@ public class NewBubble { | 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.x = leftBoundary; windowParams.y = currentInfo.getStartingYPosition(); windowParams.height = LayoutParams.WRAP_CONTENT; windowParams.width = LayoutParams.WRAP_CONTENT; @@ -392,7 +560,8 @@ public class NewBubble { public void showText(@NonNull CharSequence text) { textShowing = true; if (expanded) { - startCollapse(CollapseEnd.NOTHING, false); + startCollapse( + CollapseEnd.NOTHING, false /* isUserAction */, false /* shouldRecoverYPosition */); doShowText(text); } else { // Need to transition from old bounds to new bounds manually @@ -409,68 +578,65 @@ public class NewBubble { 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; - } - }); - }); + 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); + if (viewHolder.getExpandedView().getVisibility() == View.VISIBLE) { + viewHolder.setDrawerVisibility(View.INVISIBLE); + } + expanded = false; + savedYPosition = -1; + viewHolder .getPrimaryButton() .animate() @@ -482,11 +648,6 @@ public class NewBubble { 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() { @@ -494,12 +655,22 @@ public class NewBubble { return; } if (expanded) { - startCollapse(CollapseEnd.NOTHING, true); + startCollapse( + CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */); } else { expand(true); } } + void onLeftRightSwitch(boolean onRight) { + // Set layout direction so the small icon is not partially hidden. + View primaryIcon = viewHolder.getPrimaryIcon(); + int newGravity = (onRight ? Gravity.LEFT : Gravity.RIGHT) | Gravity.BOTTOM; + FrameLayout.LayoutParams layoutParams = + new FrameLayout.LayoutParams(primaryIcon.getWidth(), primaryIcon.getHeight(), newGravity); + primaryIcon.setLayoutParams(layoutParams); + } + LayoutParams getWindowParams() { return windowParams; } @@ -532,7 +703,7 @@ public class NewBubble { } if (expanded) { - startCollapse(CollapseEnd.HIDE, false); + startCollapse(CollapseEnd.HIDE, false /* isUserAction */, false /* shouldRecoverYPosition */); return; } @@ -618,34 +789,39 @@ public class NewBubble { } } - 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. + /** + * Create a new ViewHolder object to replace the old one.It only happens when not moving and + * collapsed. + */ + void replaceViewHolder() { + LogUtil.enterBlock("NewBubble.replaceViewHolder"); 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(); - } + // Create a new ViewHolder and copy needed info. + viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext()); + viewHolder + .getPrimaryButton() + .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild()); + viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText()); - if (isDrawingFromRight()) { - swapViewHolders(oldViewHolder); - } - } + int size = context.getResources().getDimensionPixelSize(R.dimen.bubble_small_icon_size); + viewHolder + .getPrimaryIcon() + .setLayoutParams( + new FrameLayout.LayoutParams( + size, + size, + Gravity.BOTTOM | (isDrawingFromRight() ? Gravity.LEFT : Gravity.RIGHT))); - private void swapViewHolders(ViewHolder oldViewHolder) { + update(); + + // Add new view at its horizontal boundary ViewGroup root = viewHolder.getRoot(); + windowParams.x = leftBoundary; + windowParams.gravity = Gravity.TOP | (isDrawingFromRight() ? Gravity.RIGHT : Gravity.LEFT); windowManager.addView(root, windowParams); + + // Remove the old view after delay root.getViewTreeObserver() .addOnPreDrawListener( new OnPreDrawListener() { @@ -661,63 +837,8 @@ public class NewBubble { }); } - @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); - }); + int getDrawerVisibility() { + return viewHolder.getExpandedView().getVisibility(); } private boolean isDrawingFromRight() { @@ -741,6 +862,16 @@ public class NewBubble { updatePrimaryIconAnimation(); } + private void logBasicOrCallImpression(DialerImpression.Type impressionType) { + DialerCall call = CallList.getInstance().getActiveOrBackgroundCall(); + if (call != null) { + Logger.get(context) + .logCallImpression(impressionType, call.getUniqueCallId(), call.getTimeAddedMs()); + } else { + Logger.get(context).logImpression(impressionType); + } + } + @VisibleForTesting class ViewHolder { @@ -779,7 +910,8 @@ public class NewBubble { root.setOnBackPressedListener( () -> { if (visibility == Visibility.SHOWING && expanded) { - startCollapse(CollapseEnd.NOTHING, true); + startCollapse( + CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */); return true; } return false; @@ -794,7 +926,8 @@ public class NewBubble { root.setOnTouchListener( (v, event) -> { if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) { - startCollapse(CollapseEnd.NOTHING, true); + startCollapse( + CollapseEnd.NOTHING, true /* isUserAction */, true /* shouldRecoverYPosition */); return true; } return false; @@ -812,6 +945,16 @@ public class NewBubble { moveHandler.setClickable(clickable); } + public int getMoveUpDistance() { + int deltaAllowed = + expandedView.getHeight() + - context + .getResources() + .getDimensionPixelOffset(R.dimen.bubble_button_padding_vertical) + * 2; + return moveHandler.getMoveUpDistance(deltaAllowed); + } + public ViewGroup getRoot() { return root; } @@ -864,9 +1007,4 @@ public class NewBubble { 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/NewMoveHandler.java b/java/com/android/newbubble/NewMoveHandler.java index 189ad8472..9cb1f1eca 100644 --- a/java/com/android/newbubble/NewMoveHandler.java +++ b/java/com/android/newbubble/NewMoveHandler.java @@ -48,6 +48,8 @@ class NewMoveHandler implements OnTouchListener { private final int maxX; private final int maxY; private final int bubbleSize; + private final int bubbleShadowPaddingHorizontal; + private final int bubbleExpandedViewWidth; private final float touchSlopSquared; private boolean clickable = true; @@ -70,8 +72,14 @@ class NewMoveHandler implements OnTouchListener { @Override public float getValue(LayoutParams windowParams) { int realX = windowParams.x; - realX = realX + bubbleSize / 2; + // Get bubble center position from real position + if (bubble.getDrawerVisibility() == View.INVISIBLE) { + realX += bubbleExpandedViewWidth / 2 + bubbleShadowPaddingHorizontal * 2; + } else { + realX += bubbleSize / 2 + bubbleShadowPaddingHorizontal; + } if (relativeToRight(windowParams)) { + // If gravity is right, get distant from bubble center position to screen right edge int displayWidth = context.getResources().getDisplayMetrics().widthPixels; realX = displayWidth - realX; } @@ -88,12 +96,19 @@ class NewMoveHandler implements OnTouchListener { } else { onRight = (gravityOverride & Gravity.RIGHT) == Gravity.RIGHT; } - int centeringOffset = bubbleSize / 2; + // Get real position from bubble center position + int centeringOffset; + if (bubble.getDrawerVisibility() == View.INVISIBLE) { + centeringOffset = bubbleExpandedViewWidth / 2 + bubbleShadowPaddingHorizontal * 2; + } else { + centeringOffset = bubbleSize / 2 + bubbleShadowPaddingHorizontal; + } 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); + bubble.onLeftRightSwitch(onRight); } } }; @@ -120,8 +135,13 @@ class NewMoveHandler implements OnTouchListener { windowManager = context.getSystemService(WindowManager.class); bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size); + bubbleShadowPaddingHorizontal = + context.getResources().getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal); + bubbleExpandedViewWidth = + context.getResources().getDimensionPixelSize(R.dimen.bubble_expanded_width); + // The following value is based on bubble center minX = - context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_horizontal) + context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal) + bubbleSize / 2; minY = context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_vertical) @@ -156,6 +176,12 @@ class NewMoveHandler implements OnTouchListener { moveYAnimation.animateToFinalPosition(yProperty.getValue(bubble.getWindowParams())); } + public int getMoveUpDistance(int deltaAllowed) { + int currentY = (int) yProperty.getValue(bubble.getWindowParams()); + int currentDelta = maxY - currentY; + return currentDelta >= deltaAllowed ? 0 : deltaAllowed - currentDelta; + } + @Override public boolean onTouch(View v, MotionEvent event) { float eventX = event.getRawX(); @@ -222,6 +248,14 @@ class NewMoveHandler implements OnTouchListener { moveXAnimation = new SpringAnimation(bubble.getWindowParams(), xProperty); moveXAnimation.setSpring(new SpringForce()); moveXAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY); + // Moving when expanded makes expanded view INVISIBLE, and the whole view is not at the + // boundary. It's time to create a viewHolder. + moveXAnimation.addEndListener( + (animation, canceled, value, velocity) -> { + if (!isMoving && bubble.getDrawerVisibility() == View.INVISIBLE) { + bubble.replaceViewHolder(); + } + }); } if (moveYAnimation == null) { diff --git a/java/com/android/newbubble/res/layout/new_bubble_base.xml b/java/com/android/newbubble/res/layout/new_bubble_base.xml index 8cac982f4..8d4771631 100644 --- a/java/com/android/newbubble/res/layout/new_bubble_base.xml +++ b/java/com/android/newbubble/res/layout/new_bubble_base.xml @@ -19,7 +19,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:clipChildren="false" + android:clipChildren="true" + android:clipToPadding="false" tools:theme="@style/Theme.AppCompat"> <RelativeLayout android:id="@+id/bubble_primary_container" @@ -41,7 +42,8 @@ android:measureAllChildren="false" android:elevation="@dimen/bubble_elevation" tools:backgroundTint="#FF0000AA"> - <RelativeLayout + <FrameLayout + android:id="@+id/bubble_icon_container" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView @@ -53,8 +55,7 @@ android:id="@+id/bubble_icon_primary" android:layout_width="@dimen/bubble_small_icon_size" android:layout_height="@dimen/bubble_small_icon_size" - android:layout_alignBottom="@id/bubble_icon_avatar" - android:layout_alignEnd="@id/bubble_icon_avatar" + android:layout_gravity="bottom|right" android:padding="@dimen/bubble_small_icon_padding" android:tint="@android:color/white" android:tintMode="src_in" @@ -62,7 +63,7 @@ android:measureAllChildren="false" tools:backgroundTint="#FF0000AA" tools:src="@android:drawable/ic_btn_speak_now"/> - </RelativeLayout> + </FrameLayout> <TextView android:id="@+id/bubble_text" android:layout_width="wrap_content" @@ -75,67 +76,77 @@ tools:text="Call ended"/> </ViewAnimator> </RelativeLayout> + <!-- The RelativeLayout below serves as boundary for @id/bubble_expanded_layout during animation --> <RelativeLayout - android:id="@+id/bubble_expanded_layout" - android:layout_width="@dimen/bubble_expanded_width" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_below="@id/bubble_primary_container" - android:layout_marginStart="@dimen/bubble_shadow_padding_size_horizontal_double" - android:layout_marginEnd="@dimen/bubble_shadow_padding_size_horizontal_double" android:layout_marginTop="@dimen/bubble_shadow_padding_size_vertical_minus" - android:layout_marginBottom="@dimen/bubble_shadow_padding_size_vertical" - android:visibility="gone" - tools:visibility="visible"> + android:clipChildren="true" + android:clipToPadding="false" + android:layout_below="@id/bubble_primary_container"> <RelativeLayout - android:id="@+id/bubble_triangle" - android:layout_width="12dp" - android:layout_height="12dp" - android:layout_marginTop="7dp" - android:layout_marginBottom="-6dp" - android:layout_centerHorizontal="true" - android:background="@color/background_dialer_white" - android:elevation="@dimen/bubble_expanded_elevation" - android:rotation="45"> - </RelativeLayout> - <RelativeLayout - android:layout_width="match_parent" + android:id="@+id/bubble_expanded_layout" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_below="@id/bubble_triangle" + android:paddingStart="@dimen/bubble_shadow_padding_size_horizontal_double" + android:paddingEnd="@dimen/bubble_shadow_padding_size_horizontal_double" + android:paddingBottom="@dimen/bubble_shadow_padding_size_vertical" + android:clipChildren="false" android:clipToPadding="false" - android:background="@drawable/bubble_background_with_radius" - android:elevation="@dimen/bubble_expanded_elevation" - android:layoutDirection="inherit"> - <com.android.newbubble.NewCheckableButton - android:id="@+id/bubble_button_full_screen" - android:layout_marginTop="8dp" - android:textColor="@color/bubble_button_color_grey" - android:background="@drawable/bubble_ripple_pill_up" - android:drawableTint="@color/bubble_button_color_grey" - style="@style/CheckableButton"/> - <com.android.newbubble.NewCheckableButton - android:id="@+id/bubble_button_mute" - android:layout_below="@id/bubble_button_full_screen" - android:textColor="@color/bubble_button_color_grey" - android:background="@color/background_dialer_white" - android:drawableTint="@color/bubble_button_color_grey" - style="@style/CheckableButtonWithSelectableItemBackground"/> - <com.android.newbubble.NewCheckableButton - android:id="@+id/bubble_button_audio_route" - android:layout_below="@id/bubble_button_mute" - android:textColor="@color/bubble_button_color_grey" + android:visibility="gone" + tools:visibility="visible"> + <RelativeLayout + android:id="@+id/bubble_triangle" + android:layout_width="12dp" + android:layout_height="12dp" + android:layout_marginTop="7dp" + android:layout_marginBottom="-6dp" + android:layout_centerHorizontal="true" android:background="@color/background_dialer_white" - android:drawableTint="@color/bubble_button_color_grey" - style="@style/CheckableButtonWithSelectableItemBackground"/> - <com.android.newbubble.NewCheckableButton - android:id="@+id/bubble_button_end_call" - android:layout_below="@id/bubble_button_audio_route" - android:layout_marginTop="@dimen/bubble_expanded_separator_height" - android:textColor="@color/bubble_button_color_white" - android:background="@drawable/bubble_pill_down" - android:backgroundTint="@color/dialer_end_call_button_color" - android:foreground="?attr/selectableItemBackground" - android:drawableTint="@color/bubble_button_color_white" - style="@style/CheckableButton"/> + android:elevation="@dimen/bubble_expanded_elevation" + android:rotation="45"> + </RelativeLayout> + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/bubble_triangle" + android:background="@drawable/bubble_background_with_radius" + android:elevation="@dimen/bubble_expanded_elevation" + android:clipChildren="false" + android:clipToPadding="false" + android:layoutDirection="inherit"> + <com.android.newbubble.NewCheckableButton + android:id="@+id/bubble_button_full_screen" + android:layout_marginTop="8dp" + android:textColor="@color/bubble_button_color_grey" + android:background="@drawable/bubble_ripple_pill_up" + android:drawableTint="@color/bubble_button_color_grey" + style="@style/CheckableButton"/> + <com.android.newbubble.NewCheckableButton + android:id="@+id/bubble_button_mute" + android:layout_below="@id/bubble_button_full_screen" + android:textColor="@color/bubble_button_color_grey" + android:background="@color/background_dialer_white" + android:drawableTint="@color/bubble_button_color_grey" + style="@style/CheckableButtonWithSelectableItemBackground"/> + <com.android.newbubble.NewCheckableButton + android:id="@+id/bubble_button_audio_route" + android:layout_below="@id/bubble_button_mute" + android:textColor="@color/bubble_button_color_grey" + android:background="@color/background_dialer_white" + android:drawableTint="@color/bubble_button_color_grey" + style="@style/CheckableButtonWithSelectableItemBackground"/> + <com.android.newbubble.NewCheckableButton + android:id="@+id/bubble_button_end_call" + android:layout_below="@id/bubble_button_audio_route" + android:layout_marginTop="@dimen/bubble_expanded_separator_height" + android:textColor="@color/bubble_button_color_white" + android:background="@drawable/bubble_pill_down" + android:backgroundTint="@color/dialer_end_call_button_color" + android:foreground="?attr/selectableItemBackground" + android:drawableTint="@color/bubble_button_color_white" + style="@style/CheckableButton"/> + </RelativeLayout> </RelativeLayout> </RelativeLayout> </RelativeLayout> diff --git a/java/com/android/newbubble/res/values/values.xml b/java/com/android/newbubble/res/values/values.xml index 6dda61d6c..71f813ac6 100644 --- a/java/com/android/newbubble/res/values/values.xml +++ b/java/com/android/newbubble/res/values/values.xml @@ -24,8 +24,11 @@ <dimen name="bubble_button_icon_padding">16dp</dimen> <dimen name="bubble_button_padding_vertical">12dp</dimen> <dimen name="bubble_button_padding_horizontal">16dp</dimen> - <dimen name="bubble_safe_margin_horizontal">-16dp</dimen> - <dimen name="bubble_safe_margin_vertical">64dp</dimen> + + <dimen name="bubble_off_screen_size_horizontal">-4dp</dimen> + <!-- 64dp - 16dp(bubble_shadow_padding_size_vertical) --> + <dimen name="bubble_safe_margin_vertical">48dp</dimen> + <dimen name="bubble_shadow_padding_size_vertical">16dp</dimen> <dimen name="bubble_shadow_padding_size_vertical_minus">-16dp</dimen> <dimen name="bubble_shadow_padding_size_horizontal">12dp</dimen> diff --git a/java/com/android/voicemail/impl/ActivationTask.java b/java/com/android/voicemail/impl/ActivationTask.java index 29c91e01e..83f2fd836 100644 --- a/java/com/android/voicemail/impl/ActivationTask.java +++ b/java/com/android/voicemail/impl/ActivationTask.java @@ -61,7 +61,7 @@ public class ActivationTask extends BaseTask { private static final int RETRY_TIMES = 4; private static final int RETRY_INTERVAL_MILLIS = 5_000; - private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle"; + @VisibleForTesting static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle"; private final RetryPolicy mRetryPolicy; @@ -168,7 +168,8 @@ public class ActivationTask extends BaseTask { } VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType()); - if (VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle)) { + if (mMessageData == null + && VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle)) { VvmLog.i(TAG, "Account is already activated"); // The activated state might come from restored data, the filter still needs to be set up. helper.activateSmsFilter(); diff --git a/java/com/android/voicemail/impl/PreOMigrationHandler.java b/java/com/android/voicemail/impl/PreOMigrationHandler.java index 3ec5e0826..2c454715c 100644 --- a/java/com/android/voicemail/impl/PreOMigrationHandler.java +++ b/java/com/android/voicemail/impl/PreOMigrationHandler.java @@ -18,6 +18,7 @@ package com.android.voicemail.impl; import android.content.Context; import android.os.Bundle; +import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.telecom.PhoneAccountHandle; import android.telephony.TelephonyManager; @@ -49,7 +50,7 @@ public final class PreOMigrationHandler { private static final String EXTRA_VOICEMAIL_SCRAMBLED_PIN_STRING = "android.telephony.extra.VOICEMAIL_SCRAMBLED_PIN_STRING"; - private static final String PRE_O_MIGRATION_FINISHED = "pre_o_migration_finished"; + @VisibleForTesting static final String PRE_O_MIGRATION_FINISHED = "pre_o_migration_finished"; @WorkerThread public static void migrate(Context context, PhoneAccountHandle phoneAccountHandle) { |