diff options
author | Eric Erfanian <erfanian@google.com> | 2017-03-15 14:41:07 -0700 |
---|---|---|
committer | Eric Erfanian <erfanian@google.com> | 2017-03-15 16:24:23 -0700 |
commit | d5e47f6da5b08b13ecdfa7f1edc7e12aeb83fab9 (patch) | |
tree | b54abbb51fb7d66e7755a1fbb5db023ff601090b /java/com/android/dialer | |
parent | 30436e7e6d3f2c8755a91b2b6222b74d465a9e87 (diff) | |
download | android_packages_apps_Dialer-d5e47f6da5b08b13ecdfa7f1edc7e12aeb83fab9.tar.gz android_packages_apps_Dialer-d5e47f6da5b08b13ecdfa7f1edc7e12aeb83fab9.tar.bz2 android_packages_apps_Dialer-d5e47f6da5b08b13ecdfa7f1edc7e12aeb83fab9.zip |
Update Dialer source from latest green build.
* Refactor voicemail component
* Add new enriched calling components
Test: treehugger, manual aosp testing
Change-Id: I521a0f86327d4b42e14d93927c7d613044ed5942
Diffstat (limited to 'java/com/android/dialer')
212 files changed, 6937 insertions, 3809 deletions
diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml index 80f294acc..5ce13dbd7 100644 --- a/java/com/android/dialer/app/AndroidManifest.xml +++ b/java/com/android/dialer/app/AndroidManifest.xml @@ -57,11 +57,7 @@ android:minSdkVersion="23" android:targetSdkVersion="25"/> - <application - android:backupAgent='com.android.dialer.backup.DialerBackupAgent' - android:fullBackupOnly="true" - android:restoreAnyVersion="true" - android:name="com.android.dialer.app.DialerApplication"> + <application android:theme="@style/Theme.AppCompat"> <activity android:exported="false" @@ -75,6 +71,12 @@ </intent-filter> </activity> + <activity + android:label="@string/call_log_activity_title" + android:name="com.android.dialer.app.calllog.CallLogActivity" + android:theme="@style/DialtactsThemeWithoutActionBarOverlay"> + </activity> + <receiver android:name="com.android.dialer.app.calllog.CallLogReceiver"> <intent-filter> <action android:name="android.intent.action.NEW_VOICEMAIL"/> diff --git a/java/com/android/dialer/app/CallDetailActivity.java b/java/com/android/dialer/app/CallDetailActivity.java deleted file mode 100644 index cda2b2e2c..000000000 --- a/java/com/android/dialer/app/CallDetailActivity.java +++ /dev/null @@ -1,480 +0,0 @@ -/* - * Copyright (C) 2009 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; - -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.support.v7.app.AppCompatActivity; -import android.text.BidiFormatter; -import android.text.TextDirectionHeuristics; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.widget.ListView; -import android.widget.QuickContactBadge; -import android.widget.TextView; -import android.widget.Toast; -import com.android.contacts.common.ClipboardUtils; -import com.android.contacts.common.ContactPhotoManager; -import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; -import com.android.contacts.common.GeoUtil; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.util.UriUtils; -import com.android.dialer.app.calllog.CallDetailHistoryAdapter; -import com.android.dialer.app.calllog.CallLogAsyncTaskUtil; -import com.android.dialer.app.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener; -import com.android.dialer.app.calllog.CallTypeHelper; -import com.android.dialer.app.calllog.PhoneAccountUtils; -import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; -import com.android.dialer.callintent.CallIntentBuilder; -import com.android.dialer.callintent.nano.CallInitiationType; -import com.android.dialer.common.Assert; -import com.android.dialer.common.AsyncTaskExecutor; -import com.android.dialer.common.AsyncTaskExecutors; -import com.android.dialer.compat.CompatUtils; -import com.android.dialer.logging.Logger; -import com.android.dialer.logging.nano.DialerImpression; -import com.android.dialer.logging.nano.ScreenEvent; -import com.android.dialer.phonenumbercache.ContactInfoHelper; -import com.android.dialer.phonenumberutil.PhoneNumberHelper; -import com.android.dialer.proguard.UsedByReflection; -import com.android.dialer.spam.Spam; -import com.android.dialer.telecom.TelecomUtil; -import com.android.dialer.util.CallUtil; -import com.android.dialer.util.DialerUtils; -import com.android.dialer.util.TouchPointManager; - -/** - * Displays the details of a specific call log entry. - * - * <p>This activity can be either started with the URI of a single call log entry, or with the - * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries. - */ -@UsedByReflection(value = "AndroidManifest-app.xml") -public class CallDetailActivity extends AppCompatActivity - implements MenuItem.OnMenuItemClickListener, View.OnClickListener { - - /** A long array extra containing ids of call log entries to display. */ - public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS"; - /** If we are started with a voicemail, we'll find the uri to play with this extra. */ - public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI"; - /** If the activity was triggered from a notification. */ - public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION"; - - public static final String BLOCKED_OR_SPAM_QUERY_IDENTIFIER = "blockedOrSpamIdentifier"; - - private final AsyncTaskExecutor executor = AsyncTaskExecutors.createAsyncTaskExecutor(); - protected String mNumber; - private Context mContext; - private ContactInfoHelper mContactInfoHelper; - private ContactsPreferences mContactsPreferences; - private CallTypeHelper mCallTypeHelper; - private ContactPhotoManager mContactPhotoManager; - private BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); - private LayoutInflater mInflater; - private Resources mResources; - private PhoneCallDetails mDetails; - private Uri mVoicemailUri; - private String mPostDialDigits = ""; - private ListView mHistoryList; - private QuickContactBadge mQuickContactBadge; - private TextView mCallerName; - private TextView mCallerNumber; - private TextView mAccountLabel; - private View mCallButton; - private View mEditBeforeCallActionItem; - private View mReportActionItem; - private View mCopyNumberActionItem; - private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; - private CallLogAsyncTaskListener mCallLogAsyncTaskListener = - new CallLogAsyncTaskListener() { - @Override - public void onDeleteCall() { - finish(); - } - - @Override - public void onDeleteVoicemail() { - finish(); - } - - @Override - public void onGetCallDetails(final PhoneCallDetails[] details) { - if (details == null) { - // Somewhere went wrong: we're going to bail out and show error to users. - Toast.makeText(mContext, R.string.toast_call_detail_error, Toast.LENGTH_SHORT).show(); - finish(); - return; - } - - // All calls are from the same number and same contact, so pick the first detail. - mDetails = details[0]; - mNumber = TextUtils.isEmpty(mDetails.number) ? null : mDetails.number.toString(); - - if (mNumber == null) { - updateDataAndRender(details); - return; - } - - executor.submit( - BLOCKED_OR_SPAM_QUERY_IDENTIFIER, - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - mDetails.isBlocked = - mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly( - mNumber, mDetails.countryIso) - != null; - if (Spam.get(mContext).isSpamEnabled()) { - mDetails.isSpam = - hasIncomingCalls(details) - && Spam.get(mContext) - .checkSpamStatusSynchronous(mNumber, mDetails.countryIso); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - updateDataAndRender(details); - } - }); - } - - private void updateDataAndRender(PhoneCallDetails[] details) { - mPostDialDigits = - TextUtils.isEmpty(mDetails.postDialDigits) ? "" : mDetails.postDialDigits; - - final CharSequence callLocationOrType = getNumberTypeOrLocation(mDetails); - - final CharSequence displayNumber; - if (!TextUtils.isEmpty(mDetails.postDialDigits)) { - displayNumber = mDetails.number + mDetails.postDialDigits; - } else { - displayNumber = mDetails.displayNumber; - } - - final String displayNumberStr = - mBidiFormatter.unicodeWrap(displayNumber.toString(), TextDirectionHeuristics.LTR); - - mDetails.nameDisplayOrder = mContactsPreferences.getDisplayOrder(); - - if (!TextUtils.isEmpty(mDetails.getPreferredName())) { - mCallerName.setText(mDetails.getPreferredName()); - mCallerNumber.setText(callLocationOrType + " " + displayNumberStr); - } else { - mCallerName.setText(displayNumberStr); - if (!TextUtils.isEmpty(callLocationOrType)) { - mCallerNumber.setText(callLocationOrType); - mCallerNumber.setVisibility(View.VISIBLE); - } else { - mCallerNumber.setVisibility(View.GONE); - } - } - - CharSequence accountLabel = - PhoneAccountUtils.getAccountLabel(mContext, mDetails.accountHandle); - CharSequence accountContentDescription = - PhoneCallDetails.createAccountLabelDescription( - mResources, mDetails.viaNumber, accountLabel); - if (!TextUtils.isEmpty(mDetails.viaNumber)) { - if (!TextUtils.isEmpty(accountLabel)) { - accountLabel = - mResources.getString( - R.string.call_log_via_number_phone_account, accountLabel, mDetails.viaNumber); - } else { - accountLabel = mResources.getString(R.string.call_log_via_number, mDetails.viaNumber); - } - } - if (!TextUtils.isEmpty(accountLabel)) { - mAccountLabel.setText(accountLabel); - mAccountLabel.setContentDescription(accountContentDescription); - mAccountLabel.setVisibility(View.VISIBLE); - } else { - mAccountLabel.setVisibility(View.GONE); - } - - final boolean canPlaceCallsTo = - PhoneNumberHelper.canPlaceCallsTo(mNumber, mDetails.numberPresentation); - mCallButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE); - mCopyNumberActionItem.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE); - - final boolean isSipNumber = PhoneNumberHelper.isSipNumber(mNumber); - final boolean isVoicemailNumber = - PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber); - final boolean showEditNumberBeforeCallAction = - canPlaceCallsTo && !isSipNumber && !isVoicemailNumber; - mEditBeforeCallActionItem.setVisibility( - showEditNumberBeforeCallAction ? View.VISIBLE : View.GONE); - - final boolean showReportAction = - mContactInfoHelper.canReportAsInvalid(mDetails.sourceType, mDetails.objectId); - mReportActionItem.setVisibility(showReportAction ? View.VISIBLE : View.GONE); - - invalidateOptionsMenu(); - - mHistoryList.setAdapter( - new CallDetailHistoryAdapter(mContext, mInflater, mCallTypeHelper, details)); - - updateContactPhoto(mDetails.isSpam); - - findViewById(R.id.call_detail).setVisibility(View.VISIBLE); - } - - /** - * Determines the location geocode text for a call, or the phone number type (if available). - * - * @param details The call details. - * @return The phone number type or location. - */ - private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) { - if (details.isSpam) { - return mResources.getString(R.string.spam_number_call_log_label); - } else if (details.isBlocked) { - return mResources.getString(R.string.blocked_number_call_log_label); - } else if (!TextUtils.isEmpty(details.namePrimary)) { - return Phone.getTypeLabel(mResources, details.numberType, details.numberLabel); - } else { - return details.geocode; - } - } - }; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - mContext = this; - mResources = getResources(); - mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this)); - mContactsPreferences = new ContactsPreferences(mContext); - mCallTypeHelper = new CallTypeHelper(getResources()); - mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mContext); - - mVoicemailUri = getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - setContentView(R.layout.call_detail); - mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); - - mHistoryList = (ListView) findViewById(R.id.history); - mHistoryList.addHeaderView(mInflater.inflate(R.layout.call_detail_header, null)); - mHistoryList.addFooterView(mInflater.inflate(R.layout.call_detail_footer, null), null, false); - - mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo); - mQuickContactBadge.setOverlay(null); - if (CompatUtils.hasPrioritizedMimeType()) { - mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); - } - mCallerName = (TextView) findViewById(R.id.caller_name); - mCallerNumber = (TextView) findViewById(R.id.caller_number); - mAccountLabel = (TextView) findViewById(R.id.phone_account_label); - mContactPhotoManager = ContactPhotoManager.getInstance(this); - - mCallButton = findViewById(R.id.call_back_button); - mCallButton.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View view) { - if (TextUtils.isEmpty(mNumber)) { - return; - } - DialerUtils.startActivityWithErrorToast( - CallDetailActivity.this, - new CallIntentBuilder(getDialableNumber(), CallInitiationType.Type.CALL_DETAILS) - .build()); - } - }); - - mEditBeforeCallActionItem = findViewById(R.id.call_detail_action_edit_before_call); - mEditBeforeCallActionItem.setOnClickListener(this); - mReportActionItem = findViewById(R.id.call_detail_action_report); - mReportActionItem.setOnClickListener(this); - - mCopyNumberActionItem = findViewById(R.id.call_detail_action_copy); - mCopyNumberActionItem.setOnClickListener(this); - - if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) { - closeSystemDialogs(); - } - - Logger.get(this).logScreenView(ScreenEvent.Type.CALL_DETAILS, this); - } - - @Override - public void onResume() { - super.onResume(); - mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); - getCallDetails(); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); - } - return super.dispatchTouchEvent(ev); - } - - public void getCallDetails() { - CallLogAsyncTaskUtil.getCallDetails(this, mCallLogAsyncTaskListener, getCallLogEntryUris()); - } - - /** - * Returns the list of URIs to show. - * - * <p>There are two ways the URIs can be provided to the activity: as the data on the intent, or - * as a list of ids in the call log added as an extra on the URI. - * - * <p>If both are available, the data on the intent takes precedence. - */ - private Uri[] getCallLogEntryUris() { - final Uri uri = getIntent().getData(); - if (uri != null) { - // If there is a data on the intent, it takes precedence over the extra. - return new Uri[] {uri}; - } - final long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS); - final int numIds = ids == null ? 0 : ids.length; - final Uri[] uris = new Uri[numIds]; - for (int index = 0; index < numIds; ++index) { - uris[index] = - ContentUris.withAppendedId( - TelecomUtil.getCallLogUri(CallDetailActivity.this), ids[index]); - } - return uris; - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - final MenuItem deleteMenuItem = - menu.add( - Menu.NONE, R.id.call_detail_delete_menu_item, Menu.NONE, R.string.call_details_delete); - deleteMenuItem.setIcon(R.drawable.ic_delete_24dp); - deleteMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - deleteMenuItem.setOnMenuItemClickListener(this); - - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - if (item.getItemId() == R.id.call_detail_delete_menu_item) { - Logger.get(mContext).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM); - if (hasVoicemail()) { - CallLogAsyncTaskUtil.deleteVoicemail(this, mVoicemailUri, mCallLogAsyncTaskListener); - } else { - final StringBuilder callIds = new StringBuilder(); - for (Uri callUri : getCallLogEntryUris()) { - if (callIds.length() != 0) { - callIds.append(","); - } - callIds.append(ContentUris.parseId(callUri)); - } - CallLogAsyncTaskUtil.deleteCalls(this, callIds.toString(), mCallLogAsyncTaskListener); - } - } - return true; - } - - @Override - public void onClick(View view) { - int resId = view.getId(); - if (resId == R.id.call_detail_action_copy) { - ClipboardUtils.copyText(mContext, null, mNumber, true); - } else if (resId == R.id.call_detail_action_edit_before_call) { - Intent dialIntent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(getDialableNumber())); - DialerUtils.startActivityWithErrorToast(mContext, dialIntent); - } else { - Assert.fail("Unexpected onClick event from " + view); - } - } - - // Loads and displays the contact photo. - private void updateContactPhoto(boolean isSpam) { - if (mDetails == null) { - return; - } - - mQuickContactBadge.assignContactUri(mDetails.contactUri); - final String displayName = - TextUtils.isEmpty(mDetails.namePrimary) - ? mDetails.displayNumber - : mDetails.namePrimary.toString(); - mQuickContactBadge.setContentDescription( - mResources.getString(R.string.description_contact_details, displayName)); - - final boolean isVoicemailNumber = - PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber); - if (isSpam) { - mQuickContactBadge.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact)); - return; - } - - final boolean isBusiness = mContactInfoHelper.isBusiness(mDetails.sourceType); - int contactType = ContactPhotoManager.TYPE_DEFAULT; - if (isVoicemailNumber) { - contactType = ContactPhotoManager.TYPE_VOICEMAIL; - } else if (isBusiness) { - contactType = ContactPhotoManager.TYPE_BUSINESS; - } - - final String lookupKey = - mDetails.contactUri == null ? null : UriUtils.getLookupKeyFromUri(mDetails.contactUri); - - final DefaultImageRequest request = - new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */); - - mContactPhotoManager.loadDirectoryPhoto( - mQuickContactBadge, - mDetails.photoUri, - false /* darkTheme */, - true /* isCircular */, - request); - } - - private void closeSystemDialogs() { - sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); - } - - private String getDialableNumber() { - return mNumber + mPostDialDigits; - } - - public boolean hasVoicemail() { - return mVoicemailUri != null; - } - - private static boolean hasIncomingCalls(PhoneCallDetails[] details) { - for (int i = 0; i < details.length; i++) { - if (details[i].hasIncomingCalls()) { - return true; - } - } - return false; - } -} diff --git a/java/com/android/dialer/app/DialerApplication.java b/java/com/android/dialer/app/DialerApplication.java deleted file mode 100644 index 3b979212b..000000000 --- a/java/com/android/dialer/app/DialerApplication.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2013 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; - -import android.app.Application; -import android.os.Trace; -import android.preference.PreferenceManager; -import com.android.dialer.blocking.BlockedNumbersAutoMigrator; -import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; -import com.android.dialer.enrichedcall.EnrichedCallManager; -import com.android.dialer.inject.ApplicationModule; -import com.android.dialer.inject.DaggerDialerAppComponent; -import com.android.dialer.inject.DialerAppComponent; - -public class DialerApplication extends Application implements EnrichedCallManager.Factory { - - private static final String TAG = "DialerApplication"; - - private volatile DialerAppComponent component; - - @Override - public void onCreate() { - Trace.beginSection(TAG + " onCreate"); - super.onCreate(); - new BlockedNumbersAutoMigrator( - this, - PreferenceManager.getDefaultSharedPreferences(this), - new FilteredNumberAsyncQueryHandler(this)) - .autoMigrate(); - Trace.endSection(); - } - - @Override - public EnrichedCallManager getEnrichedCallManager() { - return component().enrichedCallManager(); - } - - protected DialerAppComponent buildApplicationComponent() { - return DaggerDialerAppComponent.builder() - .applicationModule(new ApplicationModule(this)) - .build(); - } - - /** - * Returns the application component. - * - * <p>A single Component is created per application instance. Note that it won't be instantiated - * until it's first requested, but guarantees that only one will ever be created. - */ - private final DialerAppComponent component() { - // Double-check idiom for lazy initialization - DialerAppComponent result = component; - if (result == null) { - synchronized (this) { - result = component; - if (result == null) { - component = result = buildApplicationComponent(); - } - } - } - return result; - } -} diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java index 4c57cda70..b2837769f 100644 --- a/java/com/android/dialer/app/DialtactsActivity.java +++ b/java/com/android/dialer/app/DialtactsActivity.java @@ -63,15 +63,14 @@ import android.widget.Toast; import com.android.contacts.common.dialog.ClearFrequentsDialog; import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; import com.android.contacts.common.list.PhoneNumberListAdapter; -import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; import com.android.contacts.common.list.PhoneNumberPickerFragment.CursorReranker; import com.android.contacts.common.list.PhoneNumberPickerFragment.OnLoadFinishedListener; import com.android.contacts.common.widget.FloatingActionButtonController; import com.android.dialer.animation.AnimUtils; import com.android.dialer.animation.AnimationListenerAdapter; +import com.android.dialer.app.calllog.CallLogActivity; import com.android.dialer.app.calllog.CallLogFragment; import com.android.dialer.app.calllog.CallLogNotificationsService; -import com.android.dialer.app.calllog.ClearCallLogDialog; import com.android.dialer.app.dialpad.DialpadFragment; import com.android.dialer.app.list.DragDropController; import com.android.dialer.app.list.ListsFragment; @@ -85,6 +84,7 @@ import com.android.dialer.app.list.SpeedDialFragment; import com.android.dialer.app.settings.DialerSettingsActivity; import com.android.dialer.app.widget.ActionBarController; import com.android.dialer.app.widget.SearchEditTextLayout; +import com.android.dialer.callcomposer.CallComposerActivity; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.callintent.nano.CallSpecificAppData; import com.android.dialer.common.Assert; @@ -101,7 +101,10 @@ import com.android.dialer.p13n.inference.protocol.P13nRanker; import com.android.dialer.p13n.inference.protocol.P13nRanker.P13nRefreshCompleteListener; import com.android.dialer.p13n.logging.P13nLogger; import com.android.dialer.p13n.logging.P13nLogging; +import com.android.dialer.postcall.PostCall; import com.android.dialer.proguard.UsedByReflection; +import com.android.dialer.simulator.Simulator; +import com.android.dialer.simulator.SimulatorComponent; import com.android.dialer.smartdial.SmartDialNameMatcher; import com.android.dialer.smartdial.SmartDialPrefix; import com.android.dialer.telecom.TelecomUtil; @@ -124,7 +127,6 @@ public class DialtactsActivity extends TransactionSafeActivity OnListFragmentScrolledListener, CallLogFragment.HostInterface, DialpadFragment.HostInterface, - ListsFragment.HostInterface, SpeedDialFragment.HostInterface, SearchFragment.HostInterface, OnDragDropListener, @@ -478,6 +480,7 @@ public class DialtactsActivity extends TransactionSafeActivity @Override protected void onResume() { + LogUtil.d("DialtactsActivity.onResume", ""); Trace.beginSection(TAG + " onResume"); super.onResume(); @@ -490,6 +493,8 @@ public class DialtactsActivity extends TransactionSafeActivity } else if (mShowDialpadOnResume) { showDialpadFragment(false); mShowDialpadOnResume = false; + } else { + PostCall.promptUserForMessageIfNecessary(this, mParentLayout); } // If there was a voice query result returned in the {@link #onActivityResult} callback, it @@ -539,7 +544,7 @@ public class DialtactsActivity extends TransactionSafeActivity } if (getIntent().getBooleanExtra(EXTRA_CLEAR_NEW_VOICEMAILS, false)) { - CallLogNotificationsService.markNewVoicemailsAsOld(this); + CallLogNotificationsService.markNewVoicemailsAsOld(this, null); } setSearchBoxHint(); @@ -588,6 +593,7 @@ public class DialtactsActivity extends TransactionSafeActivity @Override public void onAttachFragment(final Fragment fragment) { + LogUtil.d("DialtactsActivity.onAttachFragment", "fragment: %s", fragment); if (fragment instanceof DialpadFragment) { mDialpadFragment = (DialpadFragment) fragment; if (!mIsDialpadShown && !mShowDialpadOnResume) { @@ -616,7 +622,8 @@ public class DialtactsActivity extends TransactionSafeActivity @MainThread public Cursor rerankCursor(Cursor data) { Assert.isMainThread(); - return mP13nRanker.rankCursor(data, PhoneQuery.PHONE_NUMBER); + String queryString = searchFragment.getQueryString(); + return mP13nRanker.rankCursor(data, queryString == null ? 0 : queryString.length()); } }); searchFragment.addOnLoadFinishedListener( @@ -674,9 +681,9 @@ public class DialtactsActivity extends TransactionSafeActivity } int resId = item.getItemId(); - if (item.getItemId() == R.id.menu_delete_all) { - ClearCallLogDialog.show(getFragmentManager()); - return true; + if (resId == R.id.menu_history) { + final Intent intent = new Intent(this, CallLogActivity.class); + startActivity(intent); } else if (resId == R.id.menu_clear_frequents) { ClearFrequentsDialog.show(getFragmentManager()); Logger.get(this).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, this); @@ -691,6 +698,11 @@ public class DialtactsActivity extends TransactionSafeActivity @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { + LogUtil.i( + "DialtactsActivity.onActivityResult", + "requestCode:%d, resultCode:%d", + requestCode, + resultCode); if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) { if (resultCode == RESULT_OK) { final ArrayList<String> matches = @@ -701,15 +713,16 @@ public class DialtactsActivity extends TransactionSafeActivity LogUtil.i("DialtactsActivity.onActivityResult", "voice search - nothing heard"); } } else { - LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed: " + resultCode); + LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed"); } } else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_COMPOSE) { - if (resultCode != RESULT_OK) { + if (resultCode == RESULT_FIRST_USER) { LogUtil.i( - "DialtactsActivity.onActivityResult", - "returned from call composer, error occurred (resultCode=" + resultCode + ")"); + "DialtactsActivity.onActivityResult", "returned from call composer, error occurred"); String message = - getString(R.string.call_composer_connection_failed, getString(R.string.share_and_call)); + getString( + R.string.call_composer_connection_failed, + data.getStringExtra(CallComposerActivity.KEY_CONTACT_NAME)); Snackbar.make(mParentLayout, message, Snackbar.LENGTH_LONG).show(); } else { LogUtil.i("DialtactsActivity.onActivityResult", "returned from call composer, no error"); @@ -732,6 +745,7 @@ public class DialtactsActivity extends TransactionSafeActivity * @see #onDialpadShown */ private void showDialpadFragment(boolean animate) { + LogUtil.d("DialtactActivity.showDialpadFragment", "animate: %b", animate); if (mIsDialpadShown || mStateSaved) { return; } @@ -767,6 +781,7 @@ public class DialtactsActivity extends TransactionSafeActivity /** Callback from child DialpadFragment when the dialpad is shown. */ public void onDialpadShown() { + LogUtil.d("DialtactsActivity.onDialpadShown", ""); Assert.isNotNull(mDialpadFragment); if (mDialpadFragment.getAnimate()) { Assert.isNotNull(mDialpadFragment.getView()).startAnimation(mSlideIn); @@ -838,12 +853,21 @@ public class DialtactsActivity extends TransactionSafeActivity private void updateSearchFragmentPosition() { SearchFragment fragment = null; - if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { + if (mSmartDialSearchFragment != null) { fragment = mSmartDialSearchFragment; - } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { + } else if (mRegularSearchFragment != null) { fragment = mRegularSearchFragment; } - if (fragment != null && fragment.isVisible()) { + LogUtil.d( + "DialtactsActivity.updateSearchFragmentPosition", + "fragment: %s, isVisible: %b", + fragment, + fragment != null && fragment.isVisible()); + if (fragment != null) { + // We need to force animation here even when fragment is not visible since it might not be + // visible immediately after screen orientation change and dialpad height would not be + // available immediately which is required to update position. By forcing an animation, + // position will be updated after a delay by when the dialpad height would be available. fragment.updatePosition(true /* animate */); } } @@ -858,11 +882,6 @@ public class DialtactsActivity extends TransactionSafeActivity return !TextUtils.isEmpty(mSearchQuery); } - @Override - public boolean shouldShowActionBar() { - return mListsFragment.shouldShowActionBar(); - } - private void setNotInSearchUi() { mInDialpadSearch = false; mInRegularSearch = false; @@ -1056,7 +1075,8 @@ public class DialtactsActivity extends TransactionSafeActivity } // DialtactsActivity will provide the options menu fragment.setHasOptionsMenu(false); - fragment.setShowEmptyListForNullQuery(true); + // Will show empty list if P13nRanker is not enabled. Else, re-ranked list by the ranker. + fragment.setShowEmptyListForNullQuery(mP13nRanker.shouldShowEmptyListForNullQuery()); if (!smartDialSearch) { fragment.setQueryString(query); } @@ -1361,11 +1381,6 @@ public class DialtactsActivity extends TransactionSafeActivity } @Override - public ActionBarController getActionBarController() { - return mActionBarController; - } - - @Override public boolean isDialpadShown() { return mIsDialpadShown; } @@ -1379,11 +1394,6 @@ public class DialtactsActivity extends TransactionSafeActivity } @Override - public int getActionBarHideOffset() { - return getActionBarSafely().getHideOffset(); - } - - @Override public void setActionBarHideOffset(int offset) { getActionBarSafely().setHideOffset(offset); } @@ -1461,8 +1471,19 @@ public class DialtactsActivity extends TransactionSafeActivity && mListsFragment.getSpeedDialFragment().hasFrequents() && hasContactsPermission); - menu.findItem(R.id.menu_delete_all) + menu.findItem(R.id.menu_history) .setVisible(PermissionsUtil.hasPhonePermissions(DialtactsActivity.this)); + + Context context = DialtactsActivity.this.getApplicationContext(); + MenuItem simulatorMenuItem = menu.findItem(R.id.menu_simulator_submenu); + Simulator simulator = SimulatorComponent.get(context).getSimulator(); + if (simulator.shouldShow()) { + simulatorMenuItem.setVisible(true); + simulatorMenuItem.setActionProvider(simulator.getActionProvider(context)); + } else { + simulatorMenuItem.setVisible(false); + } + super.show(); } } diff --git a/java/com/android/dialer/app/SpecialCharSequenceMgr.java b/java/com/android/dialer/app/SpecialCharSequenceMgr.java index 2ae19704a..712659c12 100644 --- a/java/com/android/dialer/app/SpecialCharSequenceMgr.java +++ b/java/com/android/dialer/app/SpecialCharSequenceMgr.java @@ -28,15 +28,14 @@ import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.net.Uri; -import android.os.Looper; import android.provider.Settings; import android.support.annotation.Nullable; +import android.support.v4.os.BuildCompat; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.text.TextUtils; -import android.util.Log; import android.view.WindowManager; import android.widget.EditText; import android.widget.Toast; @@ -46,8 +45,11 @@ import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler; import com.android.contacts.common.util.ContactDisplayUtils; import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment; import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener; -import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.calllogutils.PhoneAccountUtils; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; import com.android.dialer.compat.CompatUtils; +import com.android.dialer.oem.MotorolaUtils; import com.android.dialer.telecom.TelecomUtil; import java.util.ArrayList; import java.util.List; @@ -100,12 +102,19 @@ public class SpecialCharSequenceMgr { //get rid of the separators so that the string gets parsed correctly String dialString = PhoneNumberUtils.stripSeparators(input); - return handleDeviceIdDisplay(context, dialString) + if (handleDeviceIdDisplay(context, dialString) || handleRegulatoryInfoDisplay(context, dialString) || handlePinEntry(context, dialString) || handleAdnEntry(context, dialString, textField) - || handleSecretCode(context, dialString); + || handleSecretCode(context, dialString)) { + return true; + } + + if (MotorolaUtils.handleSpecialCharSequence(context, input)) { + return true; + } + return false; } /** @@ -114,10 +123,7 @@ public class SpecialCharSequenceMgr { * <p>This should be called when the screen becomes background. */ public static void cleanup() { - if (Looper.myLooper() != Looper.getMainLooper()) { - Log.wtf(TAG, "cleanup() is called outside the main thread"); - return; - } + Assert.isMainThread(); if (sPreviousAdnQueryHandler != null) { sPreviousAdnQueryHandler.cancel(); @@ -126,14 +132,21 @@ public class SpecialCharSequenceMgr { } /** - * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*. If a secret - * code is encountered an Intent is started with the android_secret_code://<code> URI. + * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*. + * If a secret code is encountered, an Intent is started with the android_secret_code://<code> + * URI. * * @param context the context to use * @param input the text to check for a secret code in - * @return true if a secret code was encountered + * @return true if a secret code was encountered and intent is sent out */ static boolean handleSecretCode(Context context, String input) { + // Must use system service on O+ to avoid using broadcasts, which are not allowed on O+. + if (BuildCompat.isAtLeastO()) { + return context.getSystemService(TelephonyManager.class).sendDialerCode(input); + } + + // System service call is not supported pre-O, so must use a broadcast for N-. // Secret codes are in the form *#*#<code>#*#* int len = input.length(); if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) { @@ -144,7 +157,6 @@ public class SpecialCharSequenceMgr { context.sendBroadcast(intent); return true; } - return false; } @@ -237,7 +249,7 @@ public class SpecialCharSequenceMgr { private static void handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie, Uri uri) { if (handler == null || cookie == null || uri == null) { - Log.w(TAG, "queryAdn parameters incorrect"); + LogUtil.w("SpecialCharSequenceMgr.handleAdnQuery", "queryAdn parameters incorrect"); return; } @@ -325,12 +337,14 @@ public class SpecialCharSequenceMgr { private static boolean handleRegulatoryInfoDisplay(Context context, String input) { if (input.equals(MMI_REGULATORY_INFO_DISPLAY)) { - Log.d(TAG, "handleRegulatoryInfoDisplay() sending intent to settings app"); + LogUtil.i( + "SpecialCharSequenceMgr.handleRegulatoryInfoDisplay", "sending intent to settings app"); Intent showRegInfoIntent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO); try { context.startActivity(showRegInfoIntent); } catch (ActivityNotFoundException e) { - Log.e(TAG, "startActivity() failed: " + e); + LogUtil.e( + "SpecialCharSequenceMgr.handleRegulatoryInfoDisplay", "startActivity() failed: ", e); } return true; } diff --git a/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java b/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java deleted file mode 100644 index ab6ef7362..000000000 --- a/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (C) 2011 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.calllog; - -import android.content.Context; -import android.icu.lang.UCharacter; -import android.icu.text.BreakIterator; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; -import android.provider.CallLog.Calls; -import android.text.format.DateUtils; -import android.text.format.Formatter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.TextView; -import com.android.dialer.app.PhoneCallDetails; -import com.android.dialer.app.R; -import com.android.dialer.util.CallUtil; -import com.android.dialer.util.DialerUtils; -import java.util.ArrayList; -import java.util.Locale; - -/** Adapter for a ListView containing history items from the details of a call. */ -public class CallDetailHistoryAdapter extends BaseAdapter { - - /** Each history item shows the detail of a call. */ - private static final int VIEW_TYPE_HISTORY_ITEM = 1; - - private final Context mContext; - private final LayoutInflater mLayoutInflater; - private final CallTypeHelper mCallTypeHelper; - private final PhoneCallDetails[] mPhoneCallDetails; - - /** List of items to be concatenated together for duration strings. */ - private ArrayList<CharSequence> mDurationItems = new ArrayList<>(); - - public CallDetailHistoryAdapter( - Context context, - LayoutInflater layoutInflater, - CallTypeHelper callTypeHelper, - PhoneCallDetails[] phoneCallDetails) { - mContext = context; - mLayoutInflater = layoutInflater; - mCallTypeHelper = callTypeHelper; - mPhoneCallDetails = phoneCallDetails; - } - - @Override - public boolean isEnabled(int position) { - // None of history will be clickable. - return false; - } - - @Override - public int getCount() { - return mPhoneCallDetails.length; - } - - @Override - public Object getItem(int position) { - return mPhoneCallDetails[position]; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public int getViewTypeCount() { - return 1; - } - - @Override - public int getItemViewType(int position) { - return VIEW_TYPE_HISTORY_ITEM; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // Make sure we have a valid convertView to start with - final View result = - convertView == null - ? mLayoutInflater.inflate(R.layout.call_detail_history_item, parent, false) - : convertView; - - PhoneCallDetails details = mPhoneCallDetails[position]; - CallTypeIconsView callTypeIconView = - (CallTypeIconsView) result.findViewById(R.id.call_type_icon); - TextView callTypeTextView = (TextView) result.findViewById(R.id.call_type_text); - TextView dateView = (TextView) result.findViewById(R.id.date); - TextView durationView = (TextView) result.findViewById(R.id.duration); - - int callType = details.callTypes[0]; - boolean isVideoCall = - (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO - && CallUtil.isVideoEnabled(mContext); - boolean isPulledCall = - (details.features & Calls.FEATURES_PULLED_EXTERNALLY) == Calls.FEATURES_PULLED_EXTERNALLY; - - callTypeIconView.clear(); - callTypeIconView.add(callType); - callTypeIconView.setShowVideo(isVideoCall); - callTypeTextView.setText(mCallTypeHelper.getCallTypeText(callType, isVideoCall, isPulledCall)); - // Set the date. - dateView.setText(formatDate(details.date)); - // Set the duration - if (Calls.VOICEMAIL_TYPE == callType || CallTypeHelper.isMissedCallType(callType)) { - durationView.setVisibility(View.GONE); - } else { - durationView.setVisibility(View.VISIBLE); - durationView.setText(formatDurationAndDataUsage(details.duration, details.dataUsage)); - } - - return result; - } - - /** - * Formats the provided date into a value suitable for display in the current locale. - * - * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016 - * may 25,20:02". - * - * <p>For pre-N devices, the returned value may not start with a capital if the local convention - * is to not capitalize day names. On N+ devices, the returned value is always capitalized. - */ - private CharSequence formatDate(long callDateMillis) { - CharSequence dateValue = - DateUtils.formatDateRange( - mContext, - callDateMillis /* startDate */, - callDateMillis /* endDate */, - DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_SHOW_WEEKDAY - | DateUtils.FORMAT_SHOW_YEAR); - - // We want the beginning of the date string to be capitalized, even if the word at the beginning - // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba” - // (not capitalized). To handle this issue we apply title casing to the start of the sentence so - // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02". - // - // The ICU library was not available in Android until N, so we can only do this in N+ devices. - // Pre-N devices will still see incorrect capitalization in some languages. - if (VERSION.SDK_INT < VERSION_CODES.N) { - return dateValue; - } - - // Using the ICU library is safer than just applying toUpperCase() on the first letter of the - // word because in some languages, there can be multiple starting characters which should be - // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be - // capitalized together. - - // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized like the - // month ("May") are not lower-cased as part of the conversion. - return UCharacter.toTitleCase( - Locale.getDefault(), - dateValue.toString(), - BreakIterator.getSentenceInstance(), - UCharacter.TITLECASE_NO_LOWERCASE); - } - - private CharSequence formatDuration(long elapsedSeconds) { - long minutes = 0; - long seconds = 0; - - if (elapsedSeconds >= 60) { - minutes = elapsedSeconds / 60; - elapsedSeconds -= minutes * 60; - seconds = elapsedSeconds; - return mContext.getString(R.string.callDetailsDurationFormat, minutes, seconds); - } else { - seconds = elapsedSeconds; - return mContext.getString(R.string.callDetailsShortDurationFormat, seconds); - } - } - - /** - * Formats a string containing the call duration and the data usage (if specified). - * - * @param elapsedSeconds Total elapsed seconds. - * @param dataUsage Data usage in bytes, or null if not specified. - * @return String containing call duration and data usage. - */ - private CharSequence formatDurationAndDataUsage(long elapsedSeconds, Long dataUsage) { - CharSequence duration = formatDuration(elapsedSeconds); - - if (dataUsage != null) { - mDurationItems.clear(); - mDurationItems.add(duration); - mDurationItems.add(Formatter.formatShortFileSize(mContext, dataUsage)); - - return DialerUtils.join(mDurationItems); - } else { - return duration; - } - } -} diff --git a/java/com/android/dialer/app/calllog/CallLogActivity.java b/java/com/android/dialer/app/calllog/CallLogActivity.java new file mode 100644 index 000000000..719ab4369 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogActivity.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2013 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.calllog; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.Intent; +import android.os.Bundle; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.ViewGroup; +import com.android.contacts.common.list.ViewPagerTabs; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.R; +import com.android.dialer.database.CallLogQueryHandler; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.util.TransactionSafeActivity; +import com.android.dialer.util.ViewUtil; + +/** Activity for viewing call history. */ +public class CallLogActivity extends TransactionSafeActivity + implements ViewPager.OnPageChangeListener { + + private static final int TAB_INDEX_ALL = 0; + private static final int TAB_INDEX_MISSED = 1; + private static final int TAB_INDEX_COUNT = 2; + private ViewPager mViewPager; + private ViewPagerTabs mViewPagerTabs; + private ViewPagerAdapter mViewPagerAdapter; + private CallLogFragment mAllCallsFragment; + private CallLogFragment mMissedCallsFragment; + private String[] mTabTitles; + private boolean mIsResumed; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.call_log_activity); + getWindow().setBackgroundDrawable(null); + + final ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setElevation(0); + + int startingTab = TAB_INDEX_ALL; + final Intent intent = getIntent(); + if (intent != null) { + final int callType = intent.getIntExtra(CallLog.Calls.EXTRA_CALL_TYPE_FILTER, -1); + if (callType == CallLog.Calls.MISSED_TYPE) { + startingTab = TAB_INDEX_MISSED; + } + } + + mTabTitles = new String[TAB_INDEX_COUNT]; + mTabTitles[0] = getString(R.string.call_log_all_title); + mTabTitles[1] = getString(R.string.call_log_missed_title); + + mViewPager = (ViewPager) findViewById(R.id.call_log_pager); + + mViewPagerAdapter = new ViewPagerAdapter(getFragmentManager()); + mViewPager.setAdapter(mViewPagerAdapter); + mViewPager.setOffscreenPageLimit(1); + mViewPager.setOnPageChangeListener(this); + + mViewPagerTabs = (ViewPagerTabs) findViewById(R.id.viewpager_header); + + mViewPagerTabs.setViewPager(mViewPager); + mViewPager.setCurrentItem(startingTab); + } + + @Override + protected void onResume() { + mIsResumed = true; + super.onResume(); + sendScreenViewForChildFragment(mViewPager.getCurrentItem()); + } + + @Override + protected void onPause() { + mIsResumed = false; + super.onPause(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + final MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.call_log_options, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all); + if (mAllCallsFragment != null && itemDeleteAll != null) { + // If onPrepareOptionsMenu is called before fragments are loaded, don't do anything. + final CallLogAdapter adapter = mAllCallsFragment.getAdapter(); + itemDeleteAll.setVisible(adapter != null && !adapter.isEmpty()); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (!isSafeToCommitTransactions()) { + return true; + } + + if (item.getItemId() == android.R.id.home) { + final Intent intent = new Intent(this, DialtactsActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + } else if (item.getItemId() == R.id.delete_all) { + ClearCallLogDialog.show(getFragmentManager()); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mViewPagerTabs.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + + @Override + public void onPageSelected(int position) { + if (mIsResumed) { + sendScreenViewForChildFragment(position); + } + mViewPagerTabs.onPageSelected(position); + } + + @Override + public void onPageScrollStateChanged(int state) { + mViewPagerTabs.onPageScrollStateChanged(state); + } + + private void sendScreenViewForChildFragment(int position) { + Logger.get(this).logScreenView(ScreenEvent.Type.CALL_LOG_FILTER, this); + } + + private int getRtlPosition(int position) { + if (ViewUtil.isRtl()) { + return mViewPagerAdapter.getCount() - 1 - position; + } + return position; + } + + /** Adapter for the view pager. */ + public class ViewPagerAdapter extends FragmentPagerAdapter { + + public ViewPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public long getItemId(int position) { + return getRtlPosition(position); + } + + @Override + public Fragment getItem(int position) { + switch (getRtlPosition(position)) { + case TAB_INDEX_ALL: + return new CallLogFragment( + CallLogQueryHandler.CALL_TYPE_ALL, true /* isCallLogActivity */); + case TAB_INDEX_MISSED: + return new CallLogFragment(Calls.MISSED_TYPE, true /* isCallLogActivity */); + } + throw new IllegalStateException("No fragment at position " + position); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + final CallLogFragment fragment = (CallLogFragment) super.instantiateItem(container, position); + switch (position) { + case TAB_INDEX_ALL: + mAllCallsFragment = fragment; + break; + case TAB_INDEX_MISSED: + mMissedCallsFragment = fragment; + break; + } + return fragment; + } + + @Override + public CharSequence getPageTitle(int position) { + return mTabTitles[position]; + } + + @Override + public int getCount() { + return TAB_INDEX_COUNT; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java index ea09a8c0a..fc5ffbb29 100644 --- a/java/com/android/dialer/app/calllog/CallLogAdapter.java +++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java @@ -47,7 +47,6 @@ import com.android.contacts.common.compat.PhoneNumberUtilsCompat; import com.android.contacts.common.preference.ContactsPreferences; import com.android.dialer.app.Bindings; import com.android.dialer.app.DialtactsActivity; -import com.android.dialer.app.PhoneCallDetails; import com.android.dialer.app.R; import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator; import com.android.dialer.app.calllog.calllogcache.CallLogCache; @@ -55,13 +54,19 @@ import com.android.dialer.app.contactinfo.ContactInfoCache; import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener; import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.calldetails.nano.CallDetailsEntries; +import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry; +import com.android.dialer.calllogutils.PhoneAccountUtils; +import com.android.dialer.calllogutils.PhoneCallDetails; import com.android.dialer.common.Assert; import com.android.dialer.common.AsyncTaskExecutor; import com.android.dialer.common.AsyncTaskExecutors; import com.android.dialer.common.LogUtil; import com.android.dialer.enrichedcall.EnrichedCallCapabilities; +import com.android.dialer.enrichedcall.EnrichedCallComponent; import com.android.dialer.enrichedcall.EnrichedCallManager; import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener; +import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult; import com.android.dialer.logging.Logger; import com.android.dialer.logging.nano.DialerImpression; import com.android.dialer.phonenumbercache.CallLogQuery; @@ -70,6 +75,7 @@ import com.android.dialer.phonenumbercache.ContactInfoHelper; import com.android.dialer.phonenumberutil.PhoneNumberHelper; import com.android.dialer.spam.Spam; import com.android.dialer.util.PermissionsUtil; +import java.util.List; import java.util.Map; import java.util.Set; @@ -96,7 +102,7 @@ public class CallLogAdapter extends GroupingListAdapter protected final CallLogCache mCallLogCache; private final CallFetcher mCallFetcher; - private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + @NonNull private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; private final int mActivityType; /** Instance of helper class for managing views. */ @@ -182,8 +188,6 @@ public class CallLogAdapter extends GroupingListAdapter private boolean mIsSpamEnabled; - @NonNull private final EnrichedCallManager mEnrichedCallManager; - public CallLogAdapter( Activity activity, ViewGroup alertContainer, @@ -191,6 +195,7 @@ public class CallLogAdapter extends GroupingListAdapter CallLogCache callLogCache, ContactInfoCache contactInfoCache, VoicemailPlaybackPresenter voicemailPlaybackPresenter, + @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler, int activityType) { super(); @@ -218,7 +223,7 @@ public class CallLogAdapter extends GroupingListAdapter mCallLogListItemHelper = new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache); mCallLogGroupBuilder = new CallLogGroupBuilder(this); - mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mActivity); + mFilteredNumberAsyncQueryHandler = Assert.isNotNull(filteredNumberAsyncQueryHandler); mContactsPreferences = new ContactsPreferences(mActivity); @@ -232,7 +237,6 @@ public class CallLogAdapter extends GroupingListAdapter mCallLogAlertManager = new CallLogAlertManager(this, LayoutInflater.from(mActivity), alertContainer); - mEnrichedCallManager = EnrichedCallManager.Accessor.getInstance(activity.getApplication()); } private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) { @@ -296,7 +300,7 @@ public class CallLogAdapter extends GroupingListAdapter } mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); mIsSpamEnabled = Spam.get(mActivity).isSpamEnabled(); - mEnrichedCallManager.registerCapabilitiesListener(this); + getEnrichedCallManager().registerCapabilitiesListener(this); notifyDataSetChanged(); } @@ -305,11 +309,11 @@ public class CallLogAdapter extends GroupingListAdapter for (Uri uri : mHiddenItemUris) { CallLogAsyncTaskUtil.deleteVoicemail(mActivity, uri, null); } - mEnrichedCallManager.unregisterCapabilitiesListener(this); + getEnrichedCallManager().unregisterCapabilitiesListener(this); } public void onStop() { - mEnrichedCallManager.clearCachedData(); + getEnrichedCallManager().clearCachedData(); } public CallLogAlertManager getAlertManager() { @@ -420,7 +424,9 @@ public class CallLogAdapter extends GroupingListAdapter } CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; views.isLoaded = false; - PhoneCallDetails details = createPhoneCallDetails(c, getGroupSize(position), views); + int groupSize = getGroupSize(position); + CallDetailsEntries callDetailsEntries = createCallDetailsEntries(c, groupSize); + PhoneCallDetails details = createPhoneCallDetails(c, groupSize, views); if (mHiddenRowIds.contains(c.getLong(CallLogQuery.ID))) { views.callLogEntryView.setVisibility(View.GONE); views.dayGroupHeader.setVisibility(View.GONE); @@ -432,11 +438,14 @@ public class CallLogAdapter extends GroupingListAdapter if (mCurrentlyExpandedRowId == views.rowId) { views.inflateActionViewStub(); } - loadAndRender(views, views.rowId, details); + loadAndRender(views, views.rowId, details, callDetailsEntries); } private void loadAndRender( - final CallLogListItemViewHolder views, final long rowId, final PhoneCallDetails details) { + final CallLogListItemViewHolder views, + final long rowId, + final PhoneCallDetails details, + final CallDetailsEntries callDetailsEntries) { // Reset block and spam information since this view could be reused which may contain // outdated data. views.isSpam = false; @@ -464,12 +473,33 @@ public class CallLogAdapter extends GroupingListAdapter && Spam.get(mActivity) .checkSpamStatusSynchronous(views.number, views.countryIso); details.isSpam = views.isSpam; - if (isCancelled()) { - return false; + } + if (isCancelled()) { + return false; + } + setCallDetailsEntriesHistoryResults( + PhoneNumberUtils.formatNumberToE164(views.number, views.countryIso), + callDetailsEntries); + views.setDetailedPhoneDetails(callDetailsEntries); + return !isCancelled() && loadData(views, rowId, details); + } + + private void setCallDetailsEntriesHistoryResults( + @Nullable String number, CallDetailsEntries callDetailsEntries) { + if (number == null) { + return; + } + Map<CallDetailsEntry, List<HistoryResult>> mappedResults = + getEnrichedCallManager().getAllHistoricalData(number, callDetailsEntries); + for (CallDetailsEntry entry : callDetailsEntries.entries) { + List<HistoryResult> results = mappedResults.get(entry); + if (results != null) { + entry.historyResults = mappedResults.get(entry).toArray(new HistoryResult[0]); + LogUtil.v( + "CallLogAdapter.setCallDetailsEntriesHistoryResults", + "mapped %d results", + entry.historyResults.length); } - return loadData(views, rowId, details); - } else { - return loadData(views, rowId, details); } } @@ -499,9 +529,9 @@ public class CallLogAdapter extends GroupingListAdapter return false; } - EnrichedCallCapabilities capabilities = mEnrichedCallManager.getCapabilities(e164Number); + EnrichedCallCapabilities capabilities = getEnrichedCallManager().getCapabilities(e164Number); if (capabilities == null) { - mEnrichedCallManager.requestCapabilities(e164Number); + getEnrichedCallManager().requestCapabilities(e164Number); return false; } return capabilities.supportsCallComposer(); @@ -562,6 +592,27 @@ public class CallLogAdapter extends GroupingListAdapter return details; } + @MainThread + private static CallDetailsEntries createCallDetailsEntries(Cursor cursor, int count) { + Assert.isMainThread(); + int position = cursor.getPosition(); + CallDetailsEntries entries = new CallDetailsEntries(); + entries.entries = new CallDetailsEntry[count]; + for (int i = 0; i < count; i++) { + CallDetailsEntry entry = new CallDetailsEntry(); + entry.callId = cursor.getLong(CallLogQuery.ID); + entry.callType = cursor.getInt(CallLogQuery.CALL_TYPE); + entry.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE); + entry.date = cursor.getLong(CallLogQuery.DATE); + entry.duration = cursor.getLong(CallLogQuery.DURATION); + entry.features |= cursor.getInt(CallLogQuery.FEATURES); + entries.entries[i] = entry; + cursor.moveToNext(); + } + cursor.moveToPosition(position); + return entries; + } + /** * Load data for call log. Any expensive operation should be put here to avoid blocking main * thread. Do NOT put any cursor operation here since it's not thread safe. @@ -907,6 +958,11 @@ public class CallLogAdapter extends GroupingListAdapter notifyDataSetChanged(); } + @NonNull + private EnrichedCallManager getEnrichedCallManager() { + return EnrichedCallComponent.get(mActivity).getEnrichedCallManager(); + } + /** Interface used to initiate a refresh of the content. */ public interface CallFetcher { diff --git a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java index b4e6fc5ad..2198626d6 100644 --- a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java +++ b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java @@ -16,37 +16,22 @@ package com.android.dialer.app.calllog; -import android.Manifest.permission; import android.annotation.TargetApi; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.provider.CallLog; import android.provider.VoicemailContract.Voicemails; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.support.v4.content.ContextCompat; -import android.telecom.PhoneAccountHandle; import android.text.TextUtils; -import com.android.contacts.common.GeoUtil; -import com.android.dialer.app.PhoneCallDetails; import com.android.dialer.common.AsyncTaskExecutor; import com.android.dialer.common.AsyncTaskExecutors; -import com.android.dialer.common.LogUtil; -import com.android.dialer.phonenumbercache.ContactInfo; -import com.android.dialer.phonenumbercache.ContactInfoHelper; -import com.android.dialer.phonenumberutil.PhoneNumberHelper; -import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.PermissionsUtil; -import java.util.ArrayList; -import java.util.Arrays; +import com.android.voicemail.VoicemailClient; @TargetApi(VERSION_CODES.M) public class CallLogAsyncTaskUtil { @@ -58,166 +43,6 @@ public class CallLogAsyncTaskUtil { sAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor(); } - public static void getCallDetails( - @NonNull final Context context, - @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener, - @NonNull final Uri... callUris) { - if (sAsyncTaskExecutor == null) { - initTaskExecutor(); - } - - sAsyncTaskExecutor.submit( - Tasks.GET_CALL_DETAILS, - new AsyncTask<Void, Void, PhoneCallDetails[]>() { - @Override - public PhoneCallDetails[] doInBackground(Void... params) { - if (ContextCompat.checkSelfPermission(context, permission.READ_CALL_LOG) - != PackageManager.PERMISSION_GRANTED) { - LogUtil.w("CallLogAsyncTaskUtil.getCallDetails", "missing READ_CALL_LOG permission"); - return null; - } - // TODO: All calls correspond to the same person, so make a single lookup. - final int numCalls = callUris.length; - PhoneCallDetails[] details = new PhoneCallDetails[numCalls]; - try { - for (int index = 0; index < numCalls; ++index) { - details[index] = getPhoneCallDetailsForUri(context, callUris[index]); - } - return details; - } catch (IllegalArgumentException e) { - // Something went wrong reading in our primary data. - LogUtil.e( - "CallLogAsyncTaskUtil.getCallDetails", "invalid URI starting call details", e); - return null; - } - } - - @Override - public void onPostExecute(PhoneCallDetails[] phoneCallDetails) { - if (callLogAsyncTaskListener != null) { - callLogAsyncTaskListener.onGetCallDetails(phoneCallDetails); - } - } - }); - } - - /** Return the phone call details for a given call log URI. */ - private static PhoneCallDetails getPhoneCallDetailsForUri( - @NonNull Context context, @NonNull Uri callUri) { - Cursor cursor = - context - .getContentResolver() - .query(callUri, CallDetailQuery.CALL_LOG_PROJECTION, null, null, null); - - try { - if (cursor == null || !cursor.moveToFirst()) { - throw new IllegalArgumentException("Cannot find content: " + callUri); - } - - // Read call log. - final String countryIso = cursor.getString(CallDetailQuery.COUNTRY_ISO_COLUMN_INDEX); - final String number = cursor.getString(CallDetailQuery.NUMBER_COLUMN_INDEX); - final String postDialDigits = - (VERSION.SDK_INT >= VERSION_CODES.N) - ? cursor.getString(CallDetailQuery.POST_DIAL_DIGITS) - : ""; - final String viaNumber = - (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallDetailQuery.VIA_NUMBER) : ""; - final int numberPresentation = - cursor.getInt(CallDetailQuery.NUMBER_PRESENTATION_COLUMN_INDEX); - - final PhoneAccountHandle accountHandle = - PhoneAccountUtils.getAccount( - cursor.getString(CallDetailQuery.ACCOUNT_COMPONENT_NAME), - cursor.getString(CallDetailQuery.ACCOUNT_ID)); - - // If this is not a regular number, there is no point in looking it up in the contacts. - ContactInfoHelper contactInfoHelper = - new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)); - boolean isVoicemail = PhoneNumberHelper.isVoicemailNumber(context, accountHandle, number); - boolean shouldLookupNumber = - PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation) && !isVoicemail; - ContactInfo info = ContactInfo.EMPTY; - - if (shouldLookupNumber) { - ContactInfo lookupInfo = contactInfoHelper.lookupNumber(number, countryIso); - info = lookupInfo != null ? lookupInfo : ContactInfo.EMPTY; - } - - PhoneCallDetails details = new PhoneCallDetails(number, numberPresentation, postDialDigits); - details.updateDisplayNumber(context, info.formattedNumber, isVoicemail); - - details.viaNumber = viaNumber; - details.accountHandle = accountHandle; - details.contactUri = info.lookupUri; - details.namePrimary = info.name; - details.nameAlternative = info.nameAlternative; - details.numberType = info.type; - details.numberLabel = info.label; - details.photoUri = info.photoUri; - details.sourceType = info.sourceType; - details.objectId = info.objectId; - - details.callTypes = new int[] {cursor.getInt(CallDetailQuery.CALL_TYPE_COLUMN_INDEX)}; - details.date = cursor.getLong(CallDetailQuery.DATE_COLUMN_INDEX); - details.duration = cursor.getLong(CallDetailQuery.DURATION_COLUMN_INDEX); - details.features = cursor.getInt(CallDetailQuery.FEATURES); - details.geocode = cursor.getString(CallDetailQuery.GEOCODED_LOCATION_COLUMN_INDEX); - details.transcription = cursor.getString(CallDetailQuery.TRANSCRIPTION_COLUMN_INDEX); - - details.countryIso = - !TextUtils.isEmpty(countryIso) ? countryIso : GeoUtil.getCurrentCountryIso(context); - - if (!cursor.isNull(CallDetailQuery.DATA_USAGE)) { - details.dataUsage = cursor.getLong(CallDetailQuery.DATA_USAGE); - } - - return details; - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - /** - * Delete specified calls from the call log. - * - * @param context The context. - * @param callIds String of the callIds to delete from the call log, delimited by commas (","). - * @param callLogAsyncTaskListener The listener to invoke after the entries have been deleted. - */ - public static void deleteCalls( - @NonNull final Context context, - final String callIds, - @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener) { - if (sAsyncTaskExecutor == null) { - initTaskExecutor(); - } - - sAsyncTaskExecutor.submit( - Tasks.DELETE_CALL, - new AsyncTask<Void, Void, Void>() { - @Override - public Void doInBackground(Void... params) { - context - .getContentResolver() - .delete( - TelecomUtil.getCallLogUri(context), - CallLog.Calls._ID + " IN (" + callIds + ")", - null); - return null; - } - - @Override - public void onPostExecute(Void result) { - if (callLogAsyncTaskListener != null) { - callLogAsyncTaskListener.onDeleteCall(); - } - } - }); - } - public static void markVoicemailAsRead( @NonNull final Context context, @NonNull final Uri voicemailUri) { if (sAsyncTaskExecutor == null) { @@ -235,6 +60,8 @@ public class CallLogAsyncTaskUtil { .getContentResolver() .update(voicemailUri, values, Voicemails.IS_READ + " = 0", null); + uploadVoicemailLocalChangesToServer(context); + Intent intent = new Intent(context, CallLogNotificationsService.class); intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); context.startService(intent); @@ -256,7 +83,12 @@ public class CallLogAsyncTaskUtil { new AsyncTask<Void, Void, Void>() { @Override public Void doInBackground(Void... params) { - context.getContentResolver().delete(voicemailUri, null, null); + 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 + // to upload changes on foreign voicemails, they will get a PROVIDER_CHANGED + uploadVoicemailLocalChangesToServer(context); return null; } @@ -305,11 +137,6 @@ public class CallLogAsyncTaskUtil { }); } - @VisibleForTesting - public static void resetForTest() { - sAsyncTaskExecutor = null; - } - /** The enumeration of {@link AsyncTask} objects used in this class. */ public enum Tasks { DELETE_VOICEMAIL, @@ -321,56 +148,12 @@ public class CallLogAsyncTaskUtil { } public interface CallLogAsyncTaskListener { - - void onDeleteCall(); - void onDeleteVoicemail(); - - void onGetCallDetails(PhoneCallDetails[] details); } - private static final class CallDetailQuery { - - public static final String[] CALL_LOG_PROJECTION; - static final int DATE_COLUMN_INDEX = 0; - static final int DURATION_COLUMN_INDEX = 1; - static final int NUMBER_COLUMN_INDEX = 2; - static final int CALL_TYPE_COLUMN_INDEX = 3; - static final int COUNTRY_ISO_COLUMN_INDEX = 4; - static final int GEOCODED_LOCATION_COLUMN_INDEX = 5; - static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6; - static final int ACCOUNT_COMPONENT_NAME = 7; - static final int ACCOUNT_ID = 8; - static final int FEATURES = 9; - static final int DATA_USAGE = 10; - static final int TRANSCRIPTION_COLUMN_INDEX = 11; - static final int POST_DIAL_DIGITS = 12; - static final int VIA_NUMBER = 13; - private static final String[] CALL_LOG_PROJECTION_INTERNAL = - new String[] { - CallLog.Calls.DATE, - CallLog.Calls.DURATION, - CallLog.Calls.NUMBER, - CallLog.Calls.TYPE, - CallLog.Calls.COUNTRY_ISO, - CallLog.Calls.GEOCODED_LOCATION, - CallLog.Calls.NUMBER_PRESENTATION, - CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME, - CallLog.Calls.PHONE_ACCOUNT_ID, - CallLog.Calls.FEATURES, - CallLog.Calls.DATA_USAGE, - CallLog.Calls.TRANSCRIPTION - }; - - static { - ArrayList<String> projectionList = new ArrayList<>(); - projectionList.addAll(Arrays.asList(CALL_LOG_PROJECTION_INTERNAL)); - if (VERSION.SDK_INT >= VERSION_CODES.N) { - projectionList.add(CallLog.Calls.POST_DIAL_DIGITS); - projectionList.add(CallLog.Calls.VIA_NUMBER); - } - projectionList.trimToSize(); - CALL_LOG_PROJECTION = projectionList.toArray(new String[projectionList.size()]); - } + private static void uploadVoicemailLocalChangesToServer(Context context) { + Intent intent = new Intent(VoicemailClient.ACTION_UPLOAD); + intent.setPackage(context.getPackageName()); + context.sendBroadcast(intent); } } diff --git a/java/com/android/dialer/app/calllog/CallLogFragment.java b/java/com/android/dialer/app/calllog/CallLogFragment.java index 1ae68cd65..4abef3430 100644 --- a/java/com/android/dialer/app/calllog/CallLogFragment.java +++ b/java/com/android/dialer/app/calllog/CallLogFragment.java @@ -53,6 +53,7 @@ import com.android.dialer.app.list.ListsFragment.ListsPage; import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; import com.android.dialer.app.widget.EmptyContentView; import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; import com.android.dialer.common.LogUtil; import com.android.dialer.database.CallLogQueryHandler; import com.android.dialer.phonenumbercache.ContactInfoHelper; @@ -70,9 +71,17 @@ public class CallLogFragment extends Fragment FragmentCompat.OnRequestPermissionsResultCallback, CallLogModalAlertManager.Listener { private static final String KEY_FILTER_TYPE = "filter_type"; + private static final String KEY_LOG_LIMIT = "log_limit"; + private static final String KEY_DATE_LIMIT = "date_limit"; + private static final String KEY_IS_CALL_LOG_ACTIVITY = "is_call_log_activity"; private static final String KEY_HAS_READ_CALL_LOG_PERMISSION = "has_read_call_log_permission"; private static final String KEY_REFRESH_DATA_REQUIRED = "refresh_data_required"; + // No limit specified for the number of logs to show; use the CallLogQueryHandler's default. + private static final int NO_LOG_LIMIT = -1; + // No date-based filtering. + private static final int NO_DATE_LIMIT = 0; + private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1; private static final int EVENT_UPDATE_DISPLAY = 1; @@ -104,8 +113,17 @@ public class CallLogFragment extends Fragment // Exactly same variable is in Fragment as a package private. private boolean mMenuVisible = true; // Default to all calls. - protected int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; - + private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; + // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler} + // will be used. + private int mLogLimit = NO_LOG_LIMIT; + // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after + // the date filter are included. If zero, no date-based filtering occurs. + private long mDateLimit = NO_DATE_LIMIT; + /* + * True if this instance of the CallLogFragment shown in the CallLogActivity. + */ + private boolean mIsCallLogActivity = false; private final Handler mDisplayUpdateHandler = new Handler() { @Override @@ -121,6 +139,48 @@ public class CallLogFragment extends Fragment protected CallLogModalAlertManager mModalAlertManager; private ViewGroup mModalAlertView; + public CallLogFragment() { + this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT); + } + + public CallLogFragment(int filterType) { + this(filterType, NO_LOG_LIMIT); + } + + public CallLogFragment(int filterType, boolean isCallLogActivity) { + this(filterType, NO_LOG_LIMIT); + mIsCallLogActivity = isCallLogActivity; + } + + public CallLogFragment(int filterType, int logLimit) { + this(filterType, logLimit, NO_DATE_LIMIT); + } + + /** + * Creates a call log fragment, filtering to include only calls of the desired type, occurring + * after the specified date. + * + * @param filterType type of calls to include. + * @param dateLimit limits results to calls occurring on or after the specified date. + */ + public CallLogFragment(int filterType, long dateLimit) { + this(filterType, NO_LOG_LIMIT, dateLimit); + } + + /** + * Creates a call log fragment, filtering to include only calls of the desired type, occurring + * after the specified date. Also provides a means to limit the number of results returned. + * + * @param filterType type of calls to include. + * @param logLimit limits the number of results to return. + * @param dateLimit limits results to calls occurring on or after the specified date. + */ + public CallLogFragment(int filterType, int logLimit, long dateLimit) { + mCallTypeFilter = filterType; + mLogLimit = logLimit; + mDateLimit = dateLimit; + } + @Override public void onCreate(Bundle state) { LogUtil.d("CallLogFragment.onCreate", toString()); @@ -128,13 +188,16 @@ public class CallLogFragment extends Fragment mRefreshDataRequired = true; if (state != null) { mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter); + mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit); + mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit); + mIsCallLogActivity = state.getBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity); mHasReadCallLogPermission = state.getBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, false); mRefreshDataRequired = state.getBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired); } final Activity activity = getActivity(); final ContentResolver resolver = activity.getContentResolver(); - mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this); + mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit); mKeyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver); resolver.registerContentObserver( @@ -226,7 +289,10 @@ public class CallLogFragment extends Fragment } protected void setupData() { - int activityType = CallLogAdapter.ACTIVITY_TYPE_DIALTACTS; + int activityType = + mIsCallLogActivity + ? CallLogAdapter.ACTIVITY_TYPE_CALL_LOG + : CallLogAdapter.ACTIVITY_TYPE_DIALTACTS; String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); mContactInfoCache = @@ -244,6 +310,7 @@ public class CallLogFragment extends Fragment CallLogCache.getCallLogCache(getActivity()), mContactInfoCache, getVoicemailPlaybackPresenter(), + new FilteredNumberAsyncQueryHandler(getActivity()), activityType); mRecyclerView.setAdapter(mAdapter); fetchCalls(); @@ -324,6 +391,9 @@ public class CallLogFragment extends Fragment public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter); + outState.putInt(KEY_LOG_LIMIT, mLogLimit); + outState.putLong(KEY_DATE_LIMIT, mDateLimit); + outState.putBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity); outState.putBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, mHasReadCallLogPermission); outState.putBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired); @@ -334,8 +404,10 @@ public class CallLogFragment extends Fragment @Override public void fetchCalls() { - mCallLogQueryHandler.fetchCalls(mCallTypeFilter); - ((ListsFragment) getParentFragment()).updateTabUnreadCounts(); + mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit); + if (!mIsCallLogActivity) { + ((ListsFragment) getParentFragment()).updateTabUnreadCounts(); + } } private void updateEmptyMessage(int filterType) { @@ -366,7 +438,9 @@ public class CallLogFragment extends Fragment "Unexpected filter type in CallLogFragment: " + filterType); } mEmptyListView.setDescription(messageId); - if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) { + if (mIsCallLogActivity) { + mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL); + } else if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) { mEmptyListView.setActionLabel(R.string.call_log_all_empty_action); } } @@ -420,7 +494,7 @@ public class CallLogFragment extends Fragment if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode() && mCallTypeFilter == Calls.VOICEMAIL_TYPE) { - CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); + CallLogNotificationsQueryHelper.updateVoicemailNotifications(getActivity()); } } @@ -434,7 +508,8 @@ public class CallLogFragment extends Fragment if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) { FragmentCompat.requestPermissions( this, new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE); - } else { + } else if (!mIsCallLogActivity) { + // Show dialpad if we are not in the call log activity. ((HostInterface) activity).showDialpad(); } } diff --git a/java/com/android/dialer/app/calllog/CallLogListItemHelper.java b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java index ea2119c83..a5df8cca1 100644 --- a/java/com/android/dialer/app/calllog/CallLogListItemHelper.java +++ b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java @@ -21,18 +21,16 @@ import android.provider.CallLog.Calls; import android.support.annotation.WorkerThread; import android.text.SpannableStringBuilder; import android.text.TextUtils; -import android.util.Log; -import com.android.dialer.app.PhoneCallDetails; import com.android.dialer.app.R; import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.calllogutils.PhoneCallDetails; import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; import com.android.dialer.compat.AppCompatConstants; /** Helper class to fill in the views of a call log entry. */ /* package */ class CallLogListItemHelper { - private static final String TAG = "CallLogListItemHelper"; - /** Helper for populating the details of a phone call. */ private final PhoneCallDetailsHelper mPhoneCallDetailsHelper; /** Resources to look up strings. */ @@ -105,7 +103,9 @@ import com.android.dialer.compat.AppCompatConstants; */ public void setActionContentDescriptions(CallLogListItemViewHolder views) { if (views.nameOrNumber == null) { - Log.e(TAG, "setActionContentDescriptions; name or number is null."); + LogUtil.e( + "CallLogListItemHelper.setActionContentDescriptions", + "setActionContentDescriptions; name or number is null."); } // Calling expandTemplate with a null parameter will cause a NullPointerException. @@ -170,7 +170,6 @@ import com.android.dialer.compat.AppCompatConstants; * * <p>2 calls. Answered call from John Doe mobile 1 hour ago. * - * @param context The application context. * @param details Details of call. * @return Return call action description. */ diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java index 6abd36078..8a2d94499 100644 --- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java +++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java @@ -25,9 +25,11 @@ import android.os.AsyncTask; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.annotation.VisibleForTesting; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; @@ -56,7 +58,7 @@ import com.android.dialer.blocking.FilteredNumberCompat; import com.android.dialer.blocking.FilteredNumbersUtil; import com.android.dialer.callcomposer.CallComposerActivity; import com.android.dialer.callcomposer.nano.CallComposerContact; -import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.calldetails.nano.CallDetailsEntries; import com.android.dialer.common.LogUtil; import com.android.dialer.compat.CompatUtils; import com.android.dialer.logging.Logger; @@ -78,8 +80,6 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, MenuItem.OnMenuItemClickListener, View.OnCreateContextMenuListener { - private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed"; - /** The root view of the call log list item */ public final View rootView; /** The quick contact badge for the contact. */ @@ -201,6 +201,7 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder public boolean isAttachedToWindow; public AsyncTask<Void, Void, ?> asyncTask; + private CallDetailsEntries callDetailsEntries; private CallLogListItemViewHolder( Context context, @@ -549,10 +550,6 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder } } - private static boolean isShareVoicemailAllowed(Context context) { - return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true); - } - /** * Binds text titles, click handlers and intents to the voicemail, details and callback action * buttons. @@ -577,13 +574,14 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder unblockView.setVisibility(View.GONE); reportNotSpamView.setVisibility(View.GONE); - if (isShareVoicemailAllowed(mContext)) { - sendVoicemailButtonView.setVisibility(View.VISIBLE); - } voicemailPlaybackView.setVisibility(View.VISIBLE); Uri uri = Uri.parse(voicemailUri); mVoicemailPlaybackPresenter.setPlaybackView( - voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked); + voicemailPlaybackView, + rowId, + uri, + mVoicemailPrimaryActionButtonClicked, + sendVoicemailButtonView); mVoicemailPrimaryActionButtonClicked = false; CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri); return; @@ -621,14 +619,14 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder && mVoicemailPlaybackPresenter != null && !TextUtils.isEmpty(voicemailUri)) { voicemailPlaybackView.setVisibility(View.VISIBLE); - if (isShareVoicemailAllowed(mContext)) { - Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE); - sendVoicemailButtonView.setVisibility(View.VISIBLE); - } Uri uri = Uri.parse(voicemailUri); mVoicemailPlaybackPresenter.setPlaybackView( - voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked); + voicemailPlaybackView, + rowId, + uri, + mVoicemailPrimaryActionButtonClicked, + sendVoicemailButtonView); mVoicemailPrimaryActionButtonClicked = false; CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri); } else { @@ -640,7 +638,8 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder detailsButtonView.setVisibility(View.GONE); } else { detailsButtonView.setVisibility(View.VISIBLE); - detailsButtonView.setTag(IntentProvider.getCallDetailIntentProvider(rowId, callIds, null)); + detailsButtonView.setTag( + IntentProvider.getCallDetailIntentProvider(callDetailsEntries, buildContact())); } boolean isBlockedOrSpam = blockId != null || (isSpamFeatureEnabled && isSpam); @@ -776,6 +775,8 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder contactType = ContactPhotoManager.TYPE_VOICEMAIL; } else if (isBusiness) { contactType = ContactPhotoManager.TYPE_BUSINESS; + } else if (numberPresentation == TelecomManager.PRESENTATION_RESTRICTED) { + contactType = ContactPhotoManager.TYPE_GENERIC_AVATAR; } final String lookupKey = @@ -854,20 +855,9 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder } else if (view.getId() == R.id.call_compose_action) { LogUtil.i("CallLogListItemViewHolder.onClick", "share and call pressed"); Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SHARE_AND_CALL); - CallComposerContact contact = new CallComposerContact(); - contact.photoId = info.photoId; - contact.photoUri = info.photoUri == null ? null : info.photoUri.toString(); - contact.contactUri = info.lookupUri == null ? null : info.lookupUri.toString(); - contact.nameOrNumber = (String) nameOrNumber; - contact.isBusiness = isBusiness; - contact.number = number; - /* second line of contact view. */ - contact.displayNumber = TextUtils.isEmpty(info.name) ? null : displayNumber; - /* phone number type (e.g. mobile) in second line of contact view */ - contact.numberLabel = numberType; Activity activity = (Activity) mContext; activity.startActivityForResult( - CallComposerActivity.newIntent(activity, contact), + CallComposerActivity.newIntent(activity, buildContact()), DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_COMPOSE); } else if (view.getId() == R.id.share_voicemail) { Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED); @@ -885,6 +875,21 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder } } + private CallComposerContact buildContact() { + CallComposerContact contact = new CallComposerContact(); + contact.photoId = info.photoId; + contact.photoUri = info.photoUri == null ? null : info.photoUri.toString(); + contact.contactUri = info.lookupUri == null ? null : info.lookupUri.toString(); + contact.nameOrNumber = (String) nameOrNumber; + contact.isBusiness = isBusiness; + contact.number = number; + /* second line of contact view. */ + contact.displayNumber = TextUtils.isEmpty(info.name) ? null : displayNumber; + /* phone number type (e.g. mobile) in second line of contact view */ + contact.numberLabel = numberType; + return contact; + } + private void logCallLogAction(int id) { if (id == R.id.send_message_action) { Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SEND_MESSAGE); @@ -931,6 +936,15 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder } } + public void setDetailedPhoneDetails(CallDetailsEntries callDetailsEntries) { + this.callDetailsEntries = callDetailsEntries; + } + + @VisibleForTesting + public CallDetailsEntries getDetailedPhoneDetails() { + return callDetailsEntries; + } + public interface OnClickListener { void onBlockReportSpam( diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java index 8f664d1a4..f12837e6f 100644 --- a/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsQueryHelper.java @@ -18,37 +18,41 @@ package com.android.dialer.app.calllog; import android.Manifest; import android.annotation.TargetApi; +import android.app.NotificationManager; import android.content.ContentResolver; import android.content.ContentUris; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build.VERSION_CODES; import android.provider.CallLog.Calls; import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.support.v4.os.UserManagerCompat; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; -import android.util.Log; import com.android.contacts.common.GeoUtil; import com.android.dialer.app.R; +import com.android.dialer.calllogutils.PhoneNumberDisplayUtil; +import com.android.dialer.common.LogUtil; +import com.android.dialer.notification.GroupedNotificationUtil; import com.android.dialer.phonenumbercache.ContactInfo; import com.android.dialer.phonenumbercache.ContactInfoHelper; -import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.PermissionsUtil; import java.util.ArrayList; import java.util.List; /** Helper class operating on call log notifications. */ -public class CallLogNotificationsHelper { +public class CallLogNotificationsQueryHelper { private static final String TAG = "CallLogNotifHelper"; - private static CallLogNotificationsHelper sInstance; private final Context mContext; private final NewCallsQuery mNewCallsQuery; private final ContactInfoHelper mContactInfoHelper; private final String mCurrentCountryIso; - CallLogNotificationsHelper( + CallLogNotificationsQueryHelper( Context context, NewCallsQuery newCallsQuery, ContactInfoHelper contactInfoHelper, @@ -59,29 +63,60 @@ public class CallLogNotificationsHelper { mCurrentCountryIso = countryIso; } - /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */ - public static CallLogNotificationsHelper getInstance(Context context) { - if (sInstance == null) { - ContentResolver contentResolver = context.getContentResolver(); - String countryIso = GeoUtil.getCurrentCountryIso(context); - sInstance = - new CallLogNotificationsHelper( - context, - createNewCallsQuery(context, contentResolver), - new ContactInfoHelper(context, countryIso), - countryIso); - } - return sInstance; + /** Returns an instance of {@link CallLogNotificationsQueryHelper}. */ + public static CallLogNotificationsQueryHelper getInstance(Context context) { + ContentResolver contentResolver = context.getContentResolver(); + String countryIso = GeoUtil.getCurrentCountryIso(context); + return new CallLogNotificationsQueryHelper( + context, + createNewCallsQuery(context, contentResolver), + new ContactInfoHelper(context, countryIso), + countryIso); } - /** Removes the missed call notifications. */ - public static void removeMissedCallNotifications(Context context) { - TelecomUtil.cancelMissedCallsNotification(context); + /** + * Removes the missed call notifications and marks calls as read. If a callUri is provided, only + * that call is marked as read. + */ + @WorkerThread + public static void removeMissedCallNotifications(Context context, @Nullable Uri callUri) { + // Call log is only accessible when unlocked. If that's the case, clear the list of + // new missed calls from the call log. + if (UserManagerCompat.isUserUnlocked(context) && PermissionsUtil.hasPhonePermissions(context)) { + ContentValues values = new ContentValues(); + values.put(Calls.NEW, 0); + values.put(Calls.IS_READ, 1); + StringBuilder where = new StringBuilder(); + where.append(Calls.NEW); + where.append(" = 1 AND "); + where.append(Calls.TYPE); + where.append(" = ?"); + try { + context + .getContentResolver() + .update( + callUri == null ? Calls.CONTENT_URI : callUri, + values, + where.toString(), + new String[] {Integer.toString(Calls.MISSED_TYPE)}); + } catch (IllegalArgumentException e) { + LogUtil.e( + "CallLogNotificationsQueryHelper.removeMissedCallNotifications", + "contacts provider update command failed", + e); + } + } + + GroupedNotificationUtil.removeNotification( + context.getSystemService(NotificationManager.class), + callUri != null ? callUri.toString() : null, + R.id.notification_missed_call, + MissedCallNotifier.NOTIFICATION_TAG); } /** Update the voice mail notifications. */ public static void updateVoicemailNotifications(Context context) { - CallLogNotificationsService.updateVoicemailNotifications(context, null); + CallLogNotificationsService.updateVoicemailNotifications(context); } /** Create a new instance of {@link NewCallsQuery}. */ @@ -251,7 +286,7 @@ public class CallLogNotificationsHelper { @TargetApi(VERSION_CODES.M) public List<NewCall> query(int type) { if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) { - Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup."); + LogUtil.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup."); return null; } final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE); @@ -272,7 +307,7 @@ public class CallLogNotificationsHelper { } return newCalls; } catch (RuntimeException e) { - Log.w(TAG, "Exception when querying Contacts Provider for calls lookup"); + LogUtil.w(TAG, "Exception when querying Contacts Provider for calls lookup"); return null; } } diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java index 820528126..b0d48eee5 100644 --- a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java @@ -20,6 +20,7 @@ import android.app.IntentService; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.support.annotation.Nullable; import com.android.dialer.common.LogUtil; import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.PermissionsUtil; @@ -44,21 +45,10 @@ public class CallLogNotificationsService extends IntentService { /** Action to mark all the new voicemails as old. */ public static final String ACTION_MARK_NEW_VOICEMAILS_AS_OLD = "com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD"; - /** - * Action to update voicemail notifications. - * - * <p>May include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}. - */ + /** Action to update voicemail notifications. */ public static final String ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS = "com.android.dialer.calllog.UPDATE_VOICEMAIL_NOTIFICATIONS"; /** - * Extra to included with {@link #ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS} to identify the new - * voicemail that triggered an update. - * - * <p>It must be a {@link Uri}. - */ - public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI"; - /** * Action to update the missed call notifications. * * <p>Includes optional extras {@link #EXTRA_MISSED_CALL_NUMBER} and {@link @@ -66,9 +56,15 @@ public class CallLogNotificationsService extends IntentService { */ public static final String ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS = "com.android.dialer.calllog.UPDATE_MISSED_CALL_NOTIFICATIONS"; + /** Action to mark all the new missed calls as old. */ public static final String ACTION_MARK_NEW_MISSED_CALLS_AS_OLD = "com.android.dialer.calllog.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD"; + + /** Action to update missed call notifications with a post call note. */ + public static final String ACTION_INCOMING_POST_CALL = + "com.android.dialer.calllog.INCOMING_POST_CALL"; + /** Action to call back a missed call. */ public static final String ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION = "com.android.dialer.calllog.CALL_BACK_FROM_MISSED_CALL_NOTIFICATION"; @@ -92,6 +88,21 @@ public class CallLogNotificationsService extends IntentService { */ public static final String EXTRA_MISSED_CALL_COUNT = "MISSED_CALL_COUNT"; + /** + * Extra to be included with {@link #ACTION_INCOMING_POST_CALL} to represent a post call note. + * + * <p>It must be a {@link String} + */ + public static final String EXTRA_POST_CALL_NOTE = "POST_CALL_NOTE"; + + /** + * Extra to be included with {@link #ACTION_INCOMING_POST_CALL} to represent the phone number the + * post call note came from. + * + * <p>It must be a {@link String} + */ + public static final String EXTRA_POST_CALL_NUMBER = "POST_CALL_NUMBER"; + public static final int UNKNOWN_MISSED_CALL_COUNT = -1; private VoicemailQueryHandler mVoicemailQueryHandler; @@ -103,10 +114,8 @@ public class CallLogNotificationsService extends IntentService { * Updates notifications for any new voicemails. * * @param context a valid context. - * @param voicemailUri The uri pointing to the voicemail to update the notification for. If {@code - * null}, then notifications for all new voicemails will be updated. */ - public static void updateVoicemailNotifications(Context context, Uri voicemailUri) { + public static void updateVoicemailNotifications(Context context) { if (!TelecomUtil.isDefaultDialer(context)) { LogUtil.i( "CallLogNotificationsService.updateVoicemailNotifications", @@ -116,10 +125,6 @@ public class CallLogNotificationsService extends IntentService { if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) { Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS); - // If voicemailUri is null, then notifications for all voicemails will be updated. - if (voicemailUri != null) { - serviceIntent.putExtra(CallLogNotificationsService.EXTRA_NEW_VOICEMAIL_URI, voicemailUri); - } context.startService(serviceIntent); } } @@ -139,9 +144,25 @@ public class CallLogNotificationsService extends IntentService { context.startService(serviceIntent); } - public static void markNewVoicemailsAsOld(Context context) { + public static void insertPostCallNote(Context context, String number, String postCallNote) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(ACTION_INCOMING_POST_CALL); + serviceIntent.putExtra(EXTRA_POST_CALL_NUMBER, number); + serviceIntent.putExtra(EXTRA_POST_CALL_NOTE, postCallNote); + context.startService(serviceIntent); + } + + public static void markNewVoicemailsAsOld(Context context, @Nullable Uri voicemailUri) { Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); + serviceIntent.setData(voicemailUri); + context.startService(serviceIntent); + } + + public static void markNewMissedCallsAsOld(Context context, @Nullable Uri callUri) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(ACTION_MARK_NEW_MISSED_CALLS_AS_OLD); + serviceIntent.setData(callUri); context.startService(serviceIntent); } @@ -172,11 +193,10 @@ public class CallLogNotificationsService extends IntentService { if (mVoicemailQueryHandler == null) { mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver()); } - mVoicemailQueryHandler.markNewVoicemailsAsOld(); + mVoicemailQueryHandler.markNewVoicemailsAsOld(intent.getData()); break; case ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS: - Uri voicemailUri = intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI); - DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri); + DefaultVoicemailNotifier.getInstance(this).updateNotification(); break; case ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS: int count = intent.getIntExtra(EXTRA_MISSED_CALL_COUNT, UNKNOWN_MISSED_CALL_COUNT); @@ -184,16 +204,24 @@ public class CallLogNotificationsService extends IntentService { MissedCallNotifier.getInstance(this).updateMissedCallNotification(count, number); updateBadgeCount(this, count); break; + case ACTION_INCOMING_POST_CALL: + String note = intent.getStringExtra(EXTRA_POST_CALL_NOTE); + String phoneNumber = intent.getStringExtra(EXTRA_POST_CALL_NUMBER); + MissedCallNotifier.getInstance(this).insertPostCallNotification(phoneNumber, note); + break; case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD: - CallLogNotificationsHelper.removeMissedCallNotifications(this); + CallLogNotificationsQueryHelper.removeMissedCallNotifications(this, intent.getData()); + TelecomUtil.cancelMissedCallsNotification(this); break; case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION: MissedCallNotifier.getInstance(this) - .callBackFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER)); + .callBackFromMissedCall( + intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER), intent.getData()); break; case ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION: MissedCallNotifier.getInstance(this) - .sendSmsFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER)); + .sendSmsFromMissedCall( + intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER), intent.getData()); break; default: LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle: " + intent); diff --git a/java/com/android/dialer/app/calllog/CallLogReceiver.java b/java/com/android/dialer/app/calllog/CallLogReceiver.java index a781b0887..8fd1502bc 100644 --- a/java/com/android/dialer/app/calllog/CallLogReceiver.java +++ b/java/com/android/dialer/app/calllog/CallLogReceiver.java @@ -38,9 +38,9 @@ public class CallLogReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { if (VoicemailContract.ACTION_NEW_VOICEMAIL.equals(intent.getAction())) { checkVoicemailStatus(context); - CallLogNotificationsService.updateVoicemailNotifications(context, intent.getData()); + CallLogNotificationsService.updateVoicemailNotifications(context); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { - CallLogNotificationsService.updateVoicemailNotifications(context, null); + CallLogNotificationsService.updateVoicemailNotifications(context); } else { LogUtil.w("CallLogReceiver.onReceive", "could not handle: " + intent); } diff --git a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java index 651a0ccb8..cc1dc4f20 100644 --- a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java +++ b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java @@ -19,31 +19,33 @@ package com.android.dialer.app.calllog; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.v4.util.Pair; -import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.ArrayMap; -import android.util.Log; import com.android.contacts.common.compat.TelephonyManagerCompat; import com.android.contacts.common.util.ContactDisplayUtils; import com.android.dialer.app.DialtactsActivity; import com.android.dialer.app.R; -import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall; +import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; +import com.android.dialer.app.contactinfo.ContactPhotoLoader; import com.android.dialer.app.list.ListsFragment; import com.android.dialer.blocking.FilteredNumbersUtil; -import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.common.LogUtil; +import com.android.dialer.notification.NotificationChannelManager; +import com.android.dialer.notification.NotificationChannelManager.Channel; +import com.android.dialer.phonenumbercache.ContactInfo; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -54,26 +56,23 @@ public class DefaultVoicemailNotifier { public static final String TAG = "VoicemailNotifier"; /** The tag used to identify notifications from this class. */ - private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier"; + static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier"; /** The identifier of the notification of new voicemails. */ - private static final int NOTIFICATION_ID = 1; + private static final int NOTIFICATION_ID = R.id.notification_voicemail; - /** The singleton instance of {@link DefaultVoicemailNotifier}. */ - private static DefaultVoicemailNotifier sInstance; + private final Context context; + private final CallLogNotificationsQueryHelper queryHelper; - private final Context mContext; - - private DefaultVoicemailNotifier(Context context) { - mContext = context; + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + DefaultVoicemailNotifier(Context context, CallLogNotificationsQueryHelper queryHelper) { + this.context = context; + this.queryHelper = queryHelper; } - /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */ + /** Returns an instance of {@link DefaultVoicemailNotifier}. */ public static DefaultVoicemailNotifier getInstance(Context context) { - if (sInstance == null) { - ContentResolver contentResolver = context.getContentResolver(); - sInstance = new DefaultVoicemailNotifier(context); - } - return sInstance; + return new DefaultVoicemailNotifier( + context, CallLogNotificationsQueryHelper.getInstance(context)); } /** @@ -84,34 +83,23 @@ public class DefaultVoicemailNotifier { * * <p>It is not safe to call this method from the main thread. */ - public void updateNotification(Uri newCallUri) { + public void updateNotification() { // Lookup the list of new voicemails to include in the notification. - // TODO: Move this into a service, to avoid holding the receiver up. - final List<NewCall> newCalls = - CallLogNotificationsHelper.getInstance(mContext).getNewVoicemails(); + final List<NewCall> newCalls = queryHelper.getNewVoicemails(); if (newCalls == null) { // Query failed, just return. return; } - if (newCalls.isEmpty()) { - // No voicemails to notify about: clear the notification. - getNotificationManager().cancel(NOTIFICATION_TAG, NOTIFICATION_ID); - return; - } - - Resources resources = mContext.getResources(); + Resources resources = context.getResources(); // This represents a list of names to include in the notification. String callers = null; // Maps each number into a name: if a number is in the map, it has already left a more // recent voicemail. - final Map<String, String> names = new ArrayMap<>(); - - // Determine the call corresponding to the new voicemail we have to notify about. - NewCall callToNotify = null; + final Map<String, ContactInfo> contactInfos = new ArrayMap<>(); // Iterate over the new voicemails to determine all the information above. Iterator<NewCall> itr = newCalls.iterator(); @@ -120,95 +108,64 @@ public class DefaultVoicemailNotifier { // Skip notifying for numbers which are blocked. if (FilteredNumbersUtil.shouldBlockVoicemail( - mContext, newCall.number, newCall.countryIso, newCall.dateMs)) { + context, newCall.number, newCall.countryIso, newCall.dateMs)) { itr.remove(); // Delete the voicemail. - mContext.getContentResolver().delete(newCall.voicemailUri, null, null); + context.getContentResolver().delete(newCall.voicemailUri, null, null); continue; } // Check if we already know the name associated with this number. - String name = names.get(newCall.number); - if (name == null) { - name = - CallLogNotificationsHelper.getInstance(mContext) - .getName(newCall.number, newCall.numberPresentation, newCall.countryIso); - names.put(newCall.number, name); + ContactInfo contactInfo = contactInfos.get(newCall.number); + if (contactInfo == null) { + contactInfo = + queryHelper.getContactInfo( + newCall.number, newCall.numberPresentation, newCall.countryIso); + contactInfos.put(newCall.number, contactInfo); // This is a new caller. Add it to the back of the list of callers. if (TextUtils.isEmpty(callers)) { - callers = name; + callers = contactInfo.name; } else { callers = - resources.getString(R.string.notification_voicemail_callers_list, callers, name); + resources.getString( + R.string.notification_voicemail_callers_list, callers, contactInfo.name); } } - // Check if this is the new call we need to notify about. - if (newCallUri != null - && newCall.voicemailUri != null - && ContentUris.parseId(newCallUri) == ContentUris.parseId(newCall.voicemailUri)) { - callToNotify = newCall; - } } - // All the potential new voicemails have been removed, e.g. if they were spam. if (newCalls.isEmpty()) { + // No voicemails to notify about: clear the notification. + CallLogNotificationsService.markNewVoicemailsAsOld(context, null); return; } - // If there is only one voicemail, set its transcription as the "long text". - String transcription = null; - if (newCalls.size() == 1) { - transcription = newCalls.get(0).transcription; - } - - if (newCallUri != null && callToNotify == null) { - Log.e(TAG, "The new call could not be found in the call log: " + newCallUri); - } - - // Determine the title of the notification and the icon for it. - final String title = - resources.getQuantityString( - R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size()); - // TODO: Use the photo of contact if all calls are from the same person. - final int icon = android.R.drawable.stat_notify_voicemail; - - Pair<Uri, Integer> info = getNotificationInfo(callToNotify); - - Notification.Builder notificationBuilder = - new Notification.Builder(mContext) - .setSmallIcon(icon) - .setContentTitle(title) + Notification.Builder groupSummary = + createNotificationBuilder() + .setContentTitle( + resources.getQuantityString( + R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size())) .setContentText(callers) - .setColor(resources.getColor(R.color.dialer_theme_color)) - .setSound(info.first) - .setDefaults(info.second) - .setDeleteIntent(createMarkNewVoicemailsAsOldIntent()) - .setAutoCancel(true); - - if (!TextUtils.isEmpty(transcription)) { - notificationBuilder.setStyle(new Notification.BigTextStyle().bigText(transcription)); + .setDeleteIntent(createMarkNewVoicemailsAsOldIntent(null)) + .setGroupSummary(true) + .setContentIntent(newVoicemailIntent(null)); + + NotificationChannelManager.applyChannel( + groupSummary, + context, + Channel.VOICEMAIL, + PhoneAccountHandles.getAccount(context, newCalls.get(0))); + + LogUtil.i(TAG, "Creating voicemail notification"); + getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID, groupSummary.build()); + + for (NewCall voicemail : newCalls) { + getNotificationManager() + .notify( + voicemail.voicemailUri.toString(), + NOTIFICATION_ID, + createNotificationForVoicemail(voicemail, contactInfos)); } - - // Determine the intent to fire when the notification is clicked on. - final Intent contentIntent; - // Open the call log. - contentIntent = DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_VOICEMAIL); - contentIntent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true); - notificationBuilder.setContentIntent( - PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT)); - - // The text to show in the ticker, describing the new event. - if (callToNotify != null) { - CharSequence msg = - ContactDisplayUtils.getTtsSpannedPhoneNumber( - resources, - R.string.notification_new_voicemail_ticker, - names.get(callToNotify.number)); - notificationBuilder.setTicker(msg); - } - Log.i(TAG, "Creating voicemail notification"); - getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build()); } /** @@ -216,30 +173,15 @@ public class DefaultVoicemailNotifier { * for the given call. */ private Pair<Uri, Integer> getNotificationInfo(@Nullable NewCall callToNotify) { - Log.v(TAG, "getNotificationInfo"); + LogUtil.v(TAG, "getNotificationInfo"); if (callToNotify == null) { - Log.i(TAG, "callToNotify == null"); + LogUtil.i(TAG, "callToNotify == null"); return new Pair<>(null, 0); } - PhoneAccountHandle accountHandle; - if (callToNotify.accountComponentName == null || callToNotify.accountId == null) { - Log.v(TAG, "accountComponentName == null || callToNotify.accountId == null"); - accountHandle = TelecomUtil.getDefaultOutgoingPhoneAccount(mContext, PhoneAccount.SCHEME_TEL); - if (accountHandle == null) { - Log.i(TAG, "No default phone account found, using default notification ringtone"); - return new Pair<>(null, Notification.DEFAULT_ALL); - } - - } else { - accountHandle = - new PhoneAccountHandle( - ComponentName.unflattenFromString(callToNotify.accountComponentName), - callToNotify.accountId); - } - if (accountHandle.getComponentName() != null) { - Log.v(TAG, "PhoneAccountHandle.ComponentInfo:" + accountHandle.getComponentName()); - } else { - Log.i(TAG, "PhoneAccountHandle.ComponentInfo: null"); + PhoneAccountHandle accountHandle = PhoneAccountHandles.getAccount(context, callToNotify); + if (accountHandle == null) { + LogUtil.i(TAG, "No default phone account found, using default notification ringtone"); + return new Pair<>(null, Notification.DEFAULT_ALL); } return new Pair<>( TelephonyManagerCompat.getVoicemailRingtoneUri(getTelephonyManager(), accountHandle), @@ -257,17 +199,79 @@ public class DefaultVoicemailNotifier { } /** Creates a pending intent that marks all new voicemails as old. */ - private PendingIntent createMarkNewVoicemailsAsOldIntent() { - Intent intent = new Intent(mContext, CallLogNotificationsService.class); + private PendingIntent createMarkNewVoicemailsAsOldIntent(@Nullable Uri voicemailUri) { + Intent intent = new Intent(context, CallLogNotificationsService.class); intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); - return PendingIntent.getService(mContext, 0, intent, 0); + intent.setData(voicemailUri); + return PendingIntent.getService(context, 0, intent, 0); } private NotificationManager getNotificationManager() { - return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); } private TelephonyManager getTelephonyManager() { - return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + return (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + } + + private Notification createNotificationForVoicemail( + @NonNull NewCall voicemail, @NonNull Map<String, ContactInfo> contactInfos) { + Pair<Uri, Integer> notificationInfo = getNotificationInfo(voicemail); + ContactInfo contactInfo = contactInfos.get(voicemail.number); + + Notification.Builder notificationBuilder = + createNotificationBuilder() + .setContentTitle( + context + .getResources() + .getQuantityString(R.plurals.notification_voicemail_title, 1, 1)) + .setContentText( + ContactDisplayUtils.getTtsSpannedPhoneNumber( + context.getResources(), + R.string.notification_new_voicemail_ticker, + contactInfo.name)) + .setWhen(voicemail.dateMs) + .setSound(notificationInfo.first) + .setDefaults(notificationInfo.second) + .setDeleteIntent(createMarkNewVoicemailsAsOldIntent(voicemail.voicemailUri)); + + NotificationChannelManager.applyChannel( + notificationBuilder, + context, + Channel.VOICEMAIL, + PhoneAccountHandles.getAccount(context, voicemail)); + + ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); + Bitmap photoIcon = loader.loadPhotoIcon(); + if (photoIcon != null) { + notificationBuilder.setLargeIcon(photoIcon); + } + + if (!TextUtils.isEmpty(voicemail.transcription)) { + notificationBuilder.setStyle( + new Notification.BigTextStyle().bigText(voicemail.transcription)); + } + notificationBuilder.setContentIntent(newVoicemailIntent(voicemail)); + + return notificationBuilder.build(); + } + + private Notification.Builder createNotificationBuilder() { + return new Notification.Builder(context) + .setSmallIcon(android.R.drawable.stat_notify_voicemail) + .setColor(context.getColor(R.color.dialer_theme_color)) + .setGroup(NOTIFICATION_TAG) + .setOnlyAlertOnce(true) + .setAutoCancel(true); + } + + private PendingIntent newVoicemailIntent(@Nullable NewCall voicemail) { + Intent intent = DialtactsActivity.getShowTabIntent(context, ListsFragment.TAB_INDEX_VOICEMAIL); + // TODO (b/35486204): scroll to this voicemail + if (voicemail != null) { + intent.setData(voicemail.voicemailUri); + } + intent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true); + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } } diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java index 879ac353d..c53e3ec5e 100644 --- a/java/com/android/dialer/app/calllog/IntentProvider.java +++ b/java/com/android/dialer/app/calllog/IntentProvider.java @@ -16,7 +16,6 @@ package com.android.dialer.app.calllog; -import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -25,10 +24,11 @@ import android.provider.ContactsContract; import android.telecom.PhoneAccountHandle; import com.android.contacts.common.model.Contact; import com.android.contacts.common.model.ContactLoader; -import com.android.dialer.app.CallDetailActivity; +import com.android.dialer.callcomposer.nano.CallComposerContact; +import com.android.dialer.calldetails.CallDetailsActivity; +import com.android.dialer.calldetails.nano.CallDetailsEntries; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.callintent.nano.CallInitiationType; -import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.CallUtil; import com.android.dialer.util.IntentUtil; import java.util.ArrayList; @@ -97,29 +97,16 @@ public abstract class IntentProvider { /** * Retrieves the call details intent provider for an entry in the call log. * - * @param id The call ID of the first call in the call group. - * @param extraIds The call ID of the other calls grouped together with the call. - * @param voicemailUri If call log entry is for a voicemail, the voicemail URI. + * @param callDetailsEntries The call details of the other calls grouped together with the call. + * @param contact The contact with which this call details intent pertains to. * @return The call details intent provider. */ public static IntentProvider getCallDetailIntentProvider( - final long id, final long[] extraIds, final String voicemailUri) { + CallDetailsEntries callDetailsEntries, CallComposerContact contact) { return new IntentProvider() { @Override public Intent getIntent(Context context) { - Intent intent = new Intent(context, CallDetailActivity.class); - // Check if the first item is a voicemail. - if (voicemailUri != null) { - intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, Uri.parse(voicemailUri)); - } - - if (extraIds != null && extraIds.length > 0) { - intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, extraIds); - } else { - // If there is a single item, use the direct URI for it. - intent.setData(ContentUris.withAppendedId(TelecomUtil.getCallLogUri(context), id)); - } - return intent; + return CallDetailsActivity.newInstance(context, callDetailsEntries, contact); } }; } diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java index 2fa3dae65..5b5661615 100644 --- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java +++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java @@ -16,16 +16,20 @@ package com.android.dialer.app.calllog; import android.app.Notification; +import android.app.Notification.Builder; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.os.AsyncTask; +import android.graphics.drawable.Icon; +import android.net.Uri; import android.provider.CallLog.Calls; +import android.service.notification.StatusBarNotification; +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.text.BidiFormatter; import android.text.TextDirectionHeuristics; @@ -34,109 +38,117 @@ import com.android.contacts.common.ContactsUtils; import com.android.contacts.common.compat.PhoneNumberUtilsCompat; import com.android.dialer.app.DialtactsActivity; import com.android.dialer.app.R; -import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall; +import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; import com.android.dialer.app.contactinfo.ContactPhotoLoader; import com.android.dialer.app.list.ListsFragment; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.callintent.nano.CallInitiationType; -import com.android.dialer.common.ConfigProviderBindings; import com.android.dialer.common.LogUtil; +import com.android.dialer.notification.NotificationChannelManager; +import com.android.dialer.notification.NotificationChannelManager.Channel; import com.android.dialer.phonenumbercache.ContactInfo; import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.DialerUtils; import com.android.dialer.util.IntentUtil; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** Creates a notification for calls that the user missed (neither answered nor rejected). */ public class MissedCallNotifier { /** The tag used to identify notifications from this class. */ - private static final String NOTIFICATION_TAG = "MissedCallNotifier"; + static final String NOTIFICATION_TAG = "MissedCallNotifier"; /** The identifier of the notification of new missed calls. */ - private static final int NOTIFICATION_ID = 1; + private static final int NOTIFICATION_ID = R.id.notification_missed_call; - private static MissedCallNotifier sInstance; - private Context mContext; - private CallLogNotificationsHelper mCalllogNotificationsHelper; + private final Context context; + private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper; @VisibleForTesting - MissedCallNotifier(Context context, CallLogNotificationsHelper callLogNotificationsHelper) { - mContext = context; - mCalllogNotificationsHelper = callLogNotificationsHelper; + MissedCallNotifier( + Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper) { + this.context = context; + this.callLogNotificationsQueryHelper = callLogNotificationsQueryHelper; } - /** Returns the singleton instance of the {@link MissedCallNotifier}. */ + /** Returns an instance of {@link MissedCallNotifier}. */ public static MissedCallNotifier getInstance(Context context) { - if (sInstance == null) { - CallLogNotificationsHelper callLogNotificationsHelper = - CallLogNotificationsHelper.getInstance(context); - sInstance = new MissedCallNotifier(context, callLogNotificationsHelper); - } - return sInstance; + CallLogNotificationsQueryHelper callLogNotificationsQueryHelper = + CallLogNotificationsQueryHelper.getInstance(context); + return new MissedCallNotifier(context, callLogNotificationsQueryHelper); } /** - * Creates a missed call notification with a post call message if there are no existing missed - * calls. + * Update missed call notifications from the call log. Accepts default information in case call + * log cannot be accessed. + * + * @param count the number of missed calls to display if call log cannot be accessed. May be + * {@link CallLogNotificationsService#UNKNOWN_MISSED_CALL_COUNT} if unknown. + * @param number the phone number of the most recent call to display if the call log cannot be + * accessed. May be null if unknown. */ - public void createPostCallMessageNotification(String number, String message) { - int count = CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT; - if (ConfigProviderBindings.get(mContext).getBoolean("enable_call_compose", false)) { - updateMissedCallNotification(count, number, message); - } else { - updateMissedCallNotification(count, number, null); - } - } - - /** Creates a missed call notification. */ - public void updateMissedCallNotification(int count, String number) { - updateMissedCallNotification(count, number, null); - } - - private void updateMissedCallNotification( - int count, String number, @Nullable String postCallMessage) { + @WorkerThread + public void updateMissedCallNotification(int count, @Nullable String number) { final int titleResId; CharSequence expandedText; // The text in the notification's line 1 and 2. - final List<NewCall> newCalls = mCalllogNotificationsHelper.getNewMissedCalls(); + List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls(); - if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) { - if (newCalls == null) { - // If the intent did not contain a count, and we are unable to get a count from the - // call log, then no notification can be shown. - return; + if ((newCalls != null && newCalls.isEmpty()) || count == 0) { + // No calls to notify about: clear the notification. + CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, null); + return; + } + + if (newCalls != null) { + if (count != CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT + && count != newCalls.size()) { + LogUtil.w( + "MissedCallNotifier.updateMissedCallNotification", + "Call count does not match call log count." + + " count: " + + count + + " newCalls.size(): " + + newCalls.size()); } count = newCalls.size(); } - if (count == 0) { - // No voicemails to notify about: clear the notification. - clearMissedCalls(); + if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) { + // If the intent did not contain a count, and we are unable to get a count from the + // call log, then no notification can be shown. return; } - // The call log has been updated, use that information preferentially. - boolean useCallLog = newCalls != null && newCalls.size() == count; - NewCall newestCall = useCallLog ? newCalls.get(0) : null; - long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis(); - String missedNumber = useCallLog ? newestCall.number : number; + Notification.Builder groupSummary = createNotificationBuilder(); + boolean useCallList = newCalls != null; - Notification.Builder builder = new Notification.Builder(mContext); - // Display the first line of the notification: - // 1 missed call: <caller name || handle> - // More than 1 missed call: <number of calls> + "missed calls" if (count == 1) { + NewCall call = + useCallList + ? newCalls.get(0) + : new NewCall( + null, + null, + number, + Calls.PRESENTATION_ALLOWED, + null, + null, + null, + null, + System.currentTimeMillis()); + //TODO: look up caller ID that is not in contacts. ContactInfo contactInfo = - mCalllogNotificationsHelper.getContactInfo( - missedNumber, - useCallLog ? newestCall.numberPresentation : Calls.PRESENTATION_ALLOWED, - useCallLog ? newestCall.countryIso : null); - + callLogNotificationsQueryHelper.getContactInfo( + call.number, call.numberPresentation, call.countryIso); titleResId = contactInfo.userType == ContactsUtils.USER_TYPE_WORK ? R.string.notification_missedWorkCallTitle : R.string.notification_missedCallTitle; + if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber) || TextUtils.equals(contactInfo.name, contactInfo.number)) { expandedText = @@ -147,134 +159,195 @@ public class MissedCallNotifier { expandedText = contactInfo.name; } - if (!TextUtils.isEmpty(postCallMessage)) { - // Ex. "John Doe: Hey dude" - expandedText = - mContext.getString( - R.string.post_call_notification_message, expandedText, postCallMessage); - } - ContactPhotoLoader loader = new ContactPhotoLoader(mContext, contactInfo); + ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); Bitmap photoIcon = loader.loadPhotoIcon(); if (photoIcon != null) { - builder.setLargeIcon(photoIcon); + groupSummary.setLargeIcon(photoIcon); } } else { titleResId = R.string.notification_missedCallsTitle; - expandedText = mContext.getString(R.string.notification_missedCallsMsg, count); + expandedText = context.getString(R.string.notification_missedCallsMsg, count); } // Create a public viewable version of the notification, suitable for display when sensitive // notification content is hidden. - Notification.Builder publicBuilder = new Notification.Builder(mContext); - publicBuilder - .setSmallIcon(android.R.drawable.stat_notify_missed_call) - .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) - // Show "Phone" for notification title. - .setContentTitle(mContext.getText(R.string.userCallActivityLabel)) - // Notification details shows that there are missed call(s), but does not reveal - // the missed caller information. - .setContentText(mContext.getText(titleResId)) + Notification.Builder publicSummaryBuilder = createNotificationBuilder(); + publicSummaryBuilder + .setContentTitle(context.getText(titleResId)) .setContentIntent(createCallLogPendingIntent()) - .setAutoCancel(true) - .setWhen(timeMs) - .setShowWhen(true) - .setDeleteIntent(createClearMissedCallsPendingIntent()); + .setDeleteIntent(createClearMissedCallsPendingIntent(null)); + // Create the notification summary suitable for display when sensitive information is showing. + groupSummary + .setContentTitle(context.getText(titleResId)) + .setContentText(expandedText) + .setContentIntent(createCallLogPendingIntent()) + .setDeleteIntent(createClearMissedCallsPendingIntent(null)) + .setGroupSummary(useCallList) + .setOnlyAlertOnce(useCallList) + .setPublicVersion(publicSummaryBuilder.build()); + + NotificationChannelManager.applyChannel( + groupSummary, + context, + Channel.MISSED_CALL, + PhoneAccountHandles.getAccount(context, useCallList ? newCalls.get(0) : null)); + + Notification notification = groupSummary.build(); + configureLedOnNotification(notification); + + LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification"); + getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification); + + if (useCallList) { + // Do not repost active notifications to prevent erasing post call notes. + NotificationManager manager = getNotificationMgr(); + Set<String> activeTags = new HashSet<>(); + for (StatusBarNotification activeNotification : manager.getActiveNotifications()) { + activeTags.add(activeNotification.getTag()); + } + + for (NewCall call : newCalls) { + String callTag = call.callsUri.toString(); + if (!activeTags.contains(callTag)) { + manager.notify(callTag, NOTIFICATION_ID, getNotificationForCall(call, null)); + } + } + } + } + + public void insertPostCallNotification(@NonNull String number, @NonNull String note) { + List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls(); + if (newCalls != null && !newCalls.isEmpty()) { + for (NewCall call : newCalls) { + if (call.number.equals(number.replace("tel:", ""))) { + // Update the first notification that matches our post call note sender. + getNotificationMgr() + .notify( + call.callsUri.toString(), NOTIFICATION_ID, getNotificationForCall(call, note)); + break; + } + } + } + } + + private Notification getNotificationForCall( + @NonNull NewCall call, @Nullable String postCallMessage) { + ContactInfo contactInfo = + callLogNotificationsQueryHelper.getContactInfo( + call.number, call.numberPresentation, call.countryIso); + + // Create a public viewable version of the notification, suitable for display when sensitive + // notification content is hidden. + int titleResId = + contactInfo.userType == ContactsUtils.USER_TYPE_WORK + ? R.string.notification_missedWorkCallTitle + : R.string.notification_missedCallTitle; + Notification.Builder publicBuilder = + createNotificationBuilder(call).setContentTitle(context.getText(titleResId)); + + Notification.Builder builder = createNotificationBuilder(call); + CharSequence expandedText; + if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber) + || TextUtils.equals(contactInfo.name, contactInfo.number)) { + expandedText = + PhoneNumberUtilsCompat.createTtsSpannable( + BidiFormatter.getInstance() + .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR)); + } else { + expandedText = contactInfo.name; + } + + if (postCallMessage != null) { + expandedText = + context.getString(R.string.post_call_notification_message, expandedText, postCallMessage); + } + + ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); + Bitmap photoIcon = loader.loadPhotoIcon(); + if (photoIcon != null) { + builder.setLargeIcon(photoIcon); + } // Create the notification suitable for display when sensitive information is showing. builder - .setSmallIcon(android.R.drawable.stat_notify_missed_call) - .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) - .setContentTitle(mContext.getText(titleResId)) + .setContentTitle(context.getText(titleResId)) .setContentText(expandedText) - .setContentIntent(createCallLogPendingIntent()) - .setAutoCancel(true) - .setWhen(timeMs) - .setShowWhen(true) - .setDefaults(Notification.DEFAULT_VIBRATE) - .setDeleteIntent(createClearMissedCallsPendingIntent()) // Include a public version of the notification to be shown when the missed call // notification is shown on the user's lock screen and they have chosen to hide // sensitive notification information. .setPublicVersion(publicBuilder.build()); - // Add additional actions when there is only 1 missed call and the user isn't locked - if (UserManagerCompat.isUserUnlocked(mContext) && count == 1) { - if (!TextUtils.isEmpty(missedNumber) - && !TextUtils.equals(missedNumber, mContext.getString(R.string.handle_restricted))) { + // Add additional actions when the user isn't locked + if (UserManagerCompat.isUserUnlocked(context)) { + if (!TextUtils.isEmpty(call.number) + && !TextUtils.equals(call.number, context.getString(R.string.handle_restricted))) { builder.addAction( - R.drawable.ic_phone_24dp, - mContext.getString(R.string.notification_missedCall_call_back), - createCallBackPendingIntent(missedNumber)); + new Notification.Action.Builder( + Icon.createWithResource(context, R.drawable.ic_phone_24dp), + context.getString(R.string.notification_missedCall_call_back), + createCallBackPendingIntent(call.number, call.callsUri)) + .build()); - if (!PhoneNumberHelper.isUriNumber(missedNumber)) { + if (!PhoneNumberHelper.isUriNumber(call.number)) { builder.addAction( - R.drawable.ic_message_24dp, - mContext.getString(R.string.notification_missedCall_message), - createSendSmsFromNotificationPendingIntent(missedNumber)); + new Notification.Action.Builder( + Icon.createWithResource(context, R.drawable.ic_message_24dp), + context.getString(R.string.notification_missedCall_message), + createSendSmsFromNotificationPendingIntent(call.number, call.callsUri)) + .build()); } } } Notification notification = builder.build(); configureLedOnNotification(notification); + return notification; + } - LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification"); - getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification); + private Notification.Builder createNotificationBuilder() { + return new Notification.Builder(context) + .setGroup(NOTIFICATION_TAG) + .setSmallIcon(android.R.drawable.stat_notify_missed_call) + .setColor(context.getResources().getColor(R.color.dialer_theme_color, null)) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setShowWhen(true) + .setDefaults(Notification.DEFAULT_VIBRATE); } - private void clearMissedCalls() { - AsyncTask.execute( - new Runnable() { - @Override - public void run() { - // Call log is only accessible when unlocked. If that's the case, clear the list of - // new missed calls from the call log. - if (UserManagerCompat.isUserUnlocked(mContext)) { - ContentValues values = new ContentValues(); - values.put(Calls.NEW, 0); - values.put(Calls.IS_READ, 1); - StringBuilder where = new StringBuilder(); - where.append(Calls.NEW); - where.append(" = 1 AND "); - where.append(Calls.TYPE); - where.append(" = ?"); - try { - mContext - .getContentResolver() - .update( - Calls.CONTENT_URI, - values, - where.toString(), - new String[] {Integer.toString(Calls.MISSED_TYPE)}); - } catch (IllegalArgumentException e) { - LogUtil.e( - "MissedCallNotifier.clearMissedCalls", - "contacts provider update command failed", - e); - } - } - getNotificationMgr().cancel(NOTIFICATION_TAG, NOTIFICATION_ID); - } - }); + private Notification.Builder createNotificationBuilder(@NonNull NewCall call) { + Builder builder = + createNotificationBuilder() + .setWhen(call.dateMs) + .setDeleteIntent(createClearMissedCallsPendingIntent(call.callsUri)) + .setContentIntent(createCallLogPendingIntent(call.callsUri)); + + NotificationChannelManager.applyChannel( + builder, context, Channel.MISSED_CALL, PhoneAccountHandles.getAccount(context, call)); + return builder; } /** Trigger an intent to make a call from a missed call number. */ - public void callBackFromMissedCall(String number) { - closeSystemDialogs(mContext); - CallLogNotificationsHelper.removeMissedCallNotifications(mContext); + @WorkerThread + public void callBackFromMissedCall(String number, Uri callUri) { + closeSystemDialogs(context); + CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri); + TelecomUtil.cancelMissedCallsNotification(context); DialerUtils.startActivityWithErrorToast( - mContext, + context, new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION) .build() .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } /** Trigger an intent to send an sms from a missed call number. */ - public void sendSmsFromMissedCall(String number) { - closeSystemDialogs(mContext); - CallLogNotificationsHelper.removeMissedCallNotifications(mContext); + @WorkerThread + public void sendSmsFromMissedCall(String number, Uri callUri) { + closeSystemDialogs(context); + CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri); + TelecomUtil.cancelMissedCallsNotification(context); DialerUtils.startActivityWithErrorToast( - mContext, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } /** @@ -283,34 +356,50 @@ public class MissedCallNotifier { * @return The pending intent. */ private PendingIntent createCallLogPendingIntent() { + return createCallLogPendingIntent(null); + } + + /** + * Creates a new pending intent that sends the user to the call log. + * + * @return The pending intent. + * @param callUri Uri of the call to jump to. May be null + */ + private PendingIntent createCallLogPendingIntent(@Nullable Uri callUri) { Intent contentIntent = - DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_HISTORY); - return PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT); + DialtactsActivity.getShowTabIntent(context, ListsFragment.TAB_INDEX_HISTORY); + // TODO (b/35486204): scroll to call + contentIntent.setData(callUri); + return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT); } /** Creates a pending intent that marks all new missed calls as old. */ - private PendingIntent createClearMissedCallsPendingIntent() { - Intent intent = new Intent(mContext, CallLogNotificationsService.class); + private PendingIntent createClearMissedCallsPendingIntent(@Nullable Uri callUri) { + Intent intent = new Intent(context, CallLogNotificationsService.class); intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD); - return PendingIntent.getService(mContext, 0, intent, 0); + intent.setData(callUri); + return PendingIntent.getService(context, 0, intent, 0); } - private PendingIntent createCallBackPendingIntent(String number) { - Intent intent = new Intent(mContext, CallLogNotificationsService.class); + private PendingIntent createCallBackPendingIntent(String number, @NonNull Uri callUri) { + Intent intent = new Intent(context, CallLogNotificationsService.class); intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION); intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number); + intent.setData(callUri); // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new // extra. - return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } - private PendingIntent createSendSmsFromNotificationPendingIntent(String number) { - Intent intent = new Intent(mContext, CallLogNotificationsService.class); + private PendingIntent createSendSmsFromNotificationPendingIntent( + String number, @NonNull Uri callUri) { + Intent intent = new Intent(context, CallLogNotificationsService.class); intent.setAction(CallLogNotificationsService.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION); intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number); + intent.setData(callUri); // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new // extra. - return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } /** Configures a notification to emit the blinky notification light. */ @@ -325,6 +414,6 @@ public class MissedCallNotifier { } private NotificationManager getNotificationMgr() { - return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); } } diff --git a/java/com/android/dialer/app/calllog/PhoneAccountHandles.java b/java/com/android/dialer/app/calllog/PhoneAccountHandles.java new file mode 100644 index 000000000..6d51b853c --- /dev/null +++ b/java/com/android/dialer/app/calllog/PhoneAccountHandles.java @@ -0,0 +1,41 @@ +package com.android.dialer.app.calllog; + +import android.content.ComponentName; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; +import com.android.dialer.common.LogUtil; +import com.android.dialer.telecom.TelecomUtil; + +/** Methods to help extract {@link PhoneAccount} information from database and Telecomm sources. */ +class PhoneAccountHandles { + + @Nullable + public static PhoneAccountHandle getAccount(@NonNull Context context, @Nullable NewCall call) { + PhoneAccountHandle handle; + if (call == null || call.accountComponentName == null || call.accountId == null) { + LogUtil.v( + "PhoneAccountUtils.getAccount", + "accountComponentName == null || callToNotify.accountId == null"); + handle = TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL); + if (handle == null) { + return null; + } + } else { + handle = + new PhoneAccountHandle( + ComponentName.unflattenFromString(call.accountComponentName), call.accountId); + } + if (handle.getComponentName() != null) { + LogUtil.v( + "PhoneAccountUtils.getAccount", + "PhoneAccountHandle.ComponentInfo:" + handle.getComponentName()); + } else { + LogUtil.i("PhoneAccountUtils.getAccount", "PhoneAccountHandle.ComponentInfo: null"); + } + return handle; + } +} diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java index b18270bb3..acbccb39f 100644 --- a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java @@ -27,9 +27,10 @@ import android.text.TextUtils; import android.text.format.DateUtils; import android.view.View; import android.widget.TextView; -import com.android.dialer.app.PhoneCallDetails; import com.android.dialer.app.R; import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.calllogutils.PhoneCallDetails; +import com.android.dialer.oem.MotorolaUtils; import com.android.dialer.phonenumberutil.PhoneNumberHelper; import com.android.dialer.util.DialerUtils; import java.util.ArrayList; @@ -84,6 +85,8 @@ public class PhoneCallDetailsHelper { // Show the video icon if the call had video enabled. views.callTypeIcons.setShowVideo( (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO); + views.callTypeIcons.setShowHd( + MotorolaUtils.shouldShowHdIconInCallLog(mContext, details.features)); views.callTypeIcons.requestLayout(); views.callTypeIcons.setVisibility(View.VISIBLE); diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java index 476996826..e2e27a179 100644 --- a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java @@ -20,6 +20,7 @@ import android.content.Context; import android.view.View; import android.widget.TextView; import com.android.dialer.app.R; +import com.android.dialer.calllogutils.CallTypeIconsView; /** Encapsulates the views that are used to display the details of a phone call in the call log. */ public final class PhoneCallDetailsViews { diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java index e539ceef6..6f101f580 100644 --- a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java +++ b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java @@ -40,10 +40,13 @@ public class VisualVoicemailCallLogFragment extends CallLogFragment { private VoicemailErrorManager mVoicemailAlertManager; + public VisualVoicemailCallLogFragment() { + super(CallLog.Calls.VOICEMAIL_TYPE); + } + @Override public void onCreate(Bundle state) { super.onCreate(state); - mCallTypeFilter = CallLog.Calls.VOICEMAIL_TYPE; mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter.getInstance(getActivity(), state); getActivity() .getContentResolver() diff --git a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java index d6d8354ec..e73684e70 100644 --- a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java +++ b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java @@ -15,13 +15,18 @@ */ package com.android.dialer.app.calllog; +import android.app.NotificationManager; import android.content.AsyncQueryHandler; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.provider.CallLog.Calls; -import android.util.Log; +import android.support.annotation.Nullable; +import com.android.dialer.app.R; +import com.android.dialer.common.LogUtil; +import com.android.dialer.notification.GroupedNotificationUtil; /** Handles asynchronous queries to the call log for voicemail. */ public class VoicemailQueryHandler extends AsyncQueryHandler { @@ -39,7 +44,7 @@ public class VoicemailQueryHandler extends AsyncQueryHandler { } /** Updates all new voicemails to mark them as old. */ - public void markNewVoicemailsAsOld() { + public void markNewVoicemailsAsOld(@Nullable Uri voicemailUri) { // Mark all "new" voicemails as not new anymore. StringBuilder where = new StringBuilder(); where.append(Calls.NEW); @@ -47,6 +52,10 @@ public class VoicemailQueryHandler extends AsyncQueryHandler { where.append(Calls.TYPE); where.append(" = ?"); + if (voicemailUri != null) { + where.append(" AND ").append(Calls.VOICEMAIL_URI).append(" = ?"); + } + ContentValues values = new ContentValues(1); values.put(Calls.NEW, "0"); @@ -56,7 +65,15 @@ public class VoicemailQueryHandler extends AsyncQueryHandler { Calls.CONTENT_URI_WITH_VOICEMAIL, values, where.toString(), - new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)}); + voicemailUri == null + ? new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)} + : new String[] {Integer.toString(Calls.VOICEMAIL_TYPE), voicemailUri.toString()}); + + GroupedNotificationUtil.removeNotification( + mContext.getSystemService(NotificationManager.class), + voicemailUri != null ? voicemailUri.toString() : null, + R.id.notification_voicemail, + DefaultVoicemailNotifier.NOTIFICATION_TAG); } @Override @@ -67,7 +84,7 @@ public class VoicemailQueryHandler extends AsyncQueryHandler { serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS); mContext.startService(serviceIntent); } else { - Log.w(TAG, "Unknown update completed: ignoring: " + token); + LogUtil.w(TAG, "Unknown update completed: ignoring: " + token); } } } diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java index c342b7e3b..039998780 100644 --- a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java +++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java @@ -20,10 +20,10 @@ import android.content.Context; import android.support.annotation.VisibleForTesting; import android.telecom.PhoneAccountHandle; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Pair; -import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.calllogutils.PhoneAccountUtils; import com.android.dialer.phonenumberutil.PhoneNumberHelper; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -45,9 +45,9 @@ class CallLogCacheLollipopMr1 extends CallLogCache { final Map<Pair<PhoneAccountHandle, CharSequence>, Boolean> mVoicemailQueryCache = new ConcurrentHashMap<>(); - private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new HashMap<>(); - private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new HashMap<>(); - private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new HashMap<>(); + private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new ArrayMap<>(); + private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new ArrayMap<>(); + private final Map<PhoneAccountHandle, Boolean> mPhoneAccountCallWithNoteCache = new ArrayMap<>(); /* package */ CallLogCacheLollipopMr1(Context context) { super(context); diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java index 4135cb7b8..6c35711a8 100644 --- a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java +++ b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java @@ -29,8 +29,8 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.PriorityBlockingQueue; /** - * This is a cache of contact details for the phone numbers in the c all log. The key is the phone - * number with the country in which teh call was placed or received. The content of the cache is + * This is a cache of contact details for the phone numbers in the call log. The key is the phone + * number with the country in which the call was placed or received. The content of the cache is * expired (but not purged) whenever the application comes to the foreground. * * <p>This cache queues request for information and queries for information on a background thread, diff --git a/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java index a8c718502..71e4a16ad 100644 --- a/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java +++ b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java @@ -104,7 +104,7 @@ public class ContactPhotoLoader { final RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), bitmap); drawable.setAntiAlias(true); - drawable.setCornerRadius(bitmap.getHeight() / 2); + drawable.setCircular(true); return drawable; } catch (IOException e) { LogUtil.e("ContactPhotoLoader.createPhotoIconDrawable", e.toString()); diff --git a/java/com/android/dialer/app/dialpad/DialpadFragment.java b/java/com/android/dialer/app/dialpad/DialpadFragment.java index 18bb250ce..4785ab16f 100644 --- a/java/com/android/dialer/app/dialpad/DialpadFragment.java +++ b/java/com/android/dialer/app/dialpad/DialpadFragment.java @@ -78,9 +78,9 @@ import com.android.dialer.app.DialtactsActivity; import com.android.dialer.app.R; import com.android.dialer.app.SpecialCharSequenceMgr; import com.android.dialer.app.calllog.CallLogAsync; -import com.android.dialer.app.calllog.PhoneAccountUtils; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.calllogutils.PhoneAccountUtils; import com.android.dialer.common.LogUtil; import com.android.dialer.dialpadview.DialpadKeyButton; import com.android.dialer.dialpadview.DialpadView; @@ -598,6 +598,7 @@ public class DialpadFragment extends Fragment @Override public void onStart() { + LogUtil.d("DialpadFragment.onStart", "first launch: %b", mFirstLaunch); Trace.beginSection(TAG + " onStart"); super.onStart(); // if the mToneGenerator creation fails, just continue without it. It is @@ -624,6 +625,7 @@ public class DialpadFragment extends Fragment @Override public void onResume() { + LogUtil.d("DialpadFragment.onResume", ""); Trace.beginSection(TAG + " onResume"); super.onResume(); diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java index eef920710..9ec6042c0 100644 --- a/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java +++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java @@ -135,11 +135,6 @@ public class BlockedNumbersSettingsActivity extends AppCompatActivity } @Override - public int getActionBarHideOffset() { - return 0; - } - - @Override public int getActionBarHeight() { return 0; } diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java index 2125a1524..1cdeb2175 100644 --- a/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java +++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java @@ -17,12 +17,14 @@ package com.android.dialer.app.legacybindings; import android.app.Activity; +import android.support.annotation.NonNull; import android.view.ViewGroup; import com.android.dialer.app.calllog.CallLogAdapter; import com.android.dialer.app.calllog.calllogcache.CallLogCache; import com.android.dialer.app.contactinfo.ContactInfoCache; import com.android.dialer.app.list.RegularSearchFragment; import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; /** * These are old bindings between Dialer and the container application. All new bindings should be @@ -41,6 +43,7 @@ public interface DialerLegacyBindings { CallLogCache callLogCache, ContactInfoCache contactInfoCache, VoicemailPlaybackPresenter voicemailPlaybackPresenter, + @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler, int activityType); RegularSearchFragment newRegularSearchFragment(); diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java index f01df78f8..6e32843ba 100644 --- a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java +++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java @@ -17,12 +17,14 @@ package com.android.dialer.app.legacybindings; import android.app.Activity; +import android.support.annotation.NonNull; import android.view.ViewGroup; import com.android.dialer.app.calllog.CallLogAdapter; import com.android.dialer.app.calllog.calllogcache.CallLogCache; import com.android.dialer.app.contactinfo.ContactInfoCache; import com.android.dialer.app.list.RegularSearchFragment; import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; /** Default implementation for dialer legacy bindings. */ public class DialerLegacyBindingsStub implements DialerLegacyBindings { @@ -35,6 +37,7 @@ public class DialerLegacyBindingsStub implements DialerLegacyBindings { CallLogCache callLogCache, ContactInfoCache contactInfoCache, VoicemailPlaybackPresenter voicemailPlaybackPresenter, + @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler, int activityType) { return new CallLogAdapter( activity, @@ -43,6 +46,7 @@ public class DialerLegacyBindingsStub implements DialerLegacyBindings { callLogCache, contactInfoCache, voicemailPlaybackPresenter, + filteredNumberAsyncQueryHandler, activityType); } diff --git a/java/com/android/dialer/app/list/ListsFragment.java b/java/com/android/dialer/app/list/ListsFragment.java index 725ad3001..13938f29a 100644 --- a/java/com/android/dialer/app/list/ListsFragment.java +++ b/java/com/android/dialer/app/list/ListsFragment.java @@ -30,19 +30,16 @@ import android.support.annotation.Nullable; import android.support.v13.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.android.contacts.common.list.ViewPagerTabs; import com.android.dialer.app.R; import com.android.dialer.app.calllog.CallLogFragment; -import com.android.dialer.app.calllog.CallLogNotificationsHelper; +import com.android.dialer.app.calllog.CallLogNotificationsService; import com.android.dialer.app.calllog.VisualVoicemailCallLogFragment; import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler; import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler.Source; -import com.android.dialer.app.widget.ActionBarController; import com.android.dialer.common.LogUtil; import com.android.dialer.database.CallLogQueryHandler; import com.android.dialer.logging.Logger; @@ -92,7 +89,6 @@ public class ListsFragment extends Fragment public static final int TAB_COUNT_DEFAULT = 3; public static final int TAB_COUNT_WITH_VOICEMAIL = 4; private static final String TAG = "ListsFragment"; - private ActionBar mActionBar; private ViewPager mViewPager; private ViewPagerTabs mViewPagerTabs; private ViewPagerAdapter mViewPagerAdapter; @@ -108,8 +104,7 @@ public class ListsFragment extends Fragment private boolean mHasFetchedVoicemailStatus; private boolean mShowVoicemailTabAfterVoicemailStatusIsFetched; private VoicemailStatusHelper mVoicemailStatusHelper; - private ArrayList<OnPageChangeListener> mOnPageChangeListeners = - new ArrayList<OnPageChangeListener>(); + private final ArrayList<OnPageChangeListener> mOnPageChangeListeners = new ArrayList<>(); private String[] mTabTitles; private int[] mTabIcons; /** The position of the currently selected tab. */ @@ -149,7 +144,6 @@ public class ListsFragment extends Fragment Trace.beginSection(TAG + " onResume"); super.onResume(); - mActionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (getUserVisibleHint()) { sendScreenViewForCurrentPosition(); } @@ -329,7 +323,7 @@ public class ListsFragment extends Fragment .putBoolean( VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, hasActiveVoicemailProvider) - .commit(); + .apply(); } if (hasActiveVoicemailProvider) { @@ -403,7 +397,7 @@ public class ListsFragment extends Fragment public void markMissedCallsAsReadAndRemoveNotifications() { if (mCallLogQueryHandler != null) { mCallLogQueryHandler.markMissedCallsAsRead(); - CallLogNotificationsHelper.removeMissedCallNotifications(getActivity()); + CallLogNotificationsService.markNewMissedCallsAsOld(getContext(), null); } } @@ -413,11 +407,6 @@ public class ListsFragment extends Fragment mRemoveView.animate().alpha(show ? 1 : 0).start(); } - public boolean shouldShowActionBar() { - // TODO: Update this based on scroll state. - return mActionBar != null; - } - public SpeedDialFragment getSpeedDialFragment() { return mSpeedDialFragment; } @@ -486,11 +475,6 @@ public class ListsFragment extends Fragment throw new IllegalStateException("No fragment at position " + position); } - public interface HostInterface { - - ActionBarController getActionBarController(); - } - public class ViewPagerAdapter extends FragmentPagerAdapter { private final List<Fragment> mFragments = new ArrayList<>(); @@ -518,7 +502,7 @@ public class ListsFragment extends Fragment return mSpeedDialFragment; case TAB_INDEX_HISTORY: if (mHistoryFragment == null) { - mHistoryFragment = new CallLogFragment(); + mHistoryFragment = new CallLogFragment(CallLogQueryHandler.CALL_TYPE_ALL); } return mHistoryFragment; case TAB_INDEX_ALL_CONTACTS: diff --git a/java/com/android/dialer/app/list/SearchFragment.java b/java/com/android/dialer/app/list/SearchFragment.java index 4a7d48ae4..e6615aa8d 100644 --- a/java/com/android/dialer/app/list/SearchFragment.java +++ b/java/com/android/dialer/app/list/SearchFragment.java @@ -98,6 +98,7 @@ public class SearchFragment extends PhoneNumberPickerFragment { @Override public void onStart() { + LogUtil.d("SearchFragment.onStart", ""); super.onStart(); if (isSearchMode()) { getAdapter().setHasHeader(0, false); @@ -301,6 +302,7 @@ public class SearchFragment extends PhoneNumberPickerFragment { * shown. This can be optionally animated. */ public void updatePosition(boolean animate) { + LogUtil.d("SearchFragment.updatePosition", "animate: %b", animate); if (mActivity == null) { // Activity will be set in onStart, and this method will be called again return; @@ -363,6 +365,13 @@ public class SearchFragment extends PhoneNumberPickerFragment { return; } int spacerHeight = mActivity.isDialpadShown() ? mActivity.getDialpadHeight() : 0; + LogUtil.d( + "SearchFragment.resizeListView", + "spacerHeight: %d -> %d, isDialpadShown: %b, dialpad height: %d", + mSpacer.getHeight(), + spacerHeight, + mActivity.isDialpadShown(), + mActivity.getDialpadHeight()); if (spacerHeight != mSpacer.getHeight()) { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpacer.getLayoutParams(); lp.height = spacerHeight; @@ -418,8 +427,6 @@ public class SearchFragment extends PhoneNumberPickerFragment { int getDialpadHeight(); - int getActionBarHideOffset(); - int getActionBarHeight(); } } diff --git a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml index 247b34f4c..7e450c4cd 100644 --- a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml +++ b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml @@ -29,18 +29,6 @@ android:theme="@style/SettingsStyle"> </activity> - <activity - android:label="@string/callDetailTitle" - android:name="com.android.dialer.app.CallDetailActivity" - android:parentActivityName="com.android.dialer.calllog.CallLogActivity" - android:theme="@style/CallDetailActivityTheme"> - <intent-filter> - <action android:name="android.intent.action.VIEW"/> - <category android:name="android.intent.category.DEFAULT"/> - <data android:mimeType="vnd.android.cursor.item/calls"/> - </intent-filter> - </activity> - <!-- The entrance point for Phone UI. stateAlwaysHidden is set to suppress keyboard show up on dialpad screen. --> diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.png Binary files differnew file mode 100644 index 000000000..ff55620d0 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/search_shadow.9.png diff --git a/java/com/android/dialer/app/res/layout/call_detail.xml b/java/com/android/dialer/app/res/layout/call_detail.xml deleted file mode 100644 index 58a7bf0dc..000000000 --- a/java/com/android/dialer/app/res/layout/call_detail.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2009 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. ---> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/call_detail" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@color/background_dialer_call_log"> - - <!-- - The list view is under everything. - It contains a first header element which is hidden under the controls UI. - When scrolling, the controls move up until the name bar hits the top. - --> - <ListView - android:id="@+id/history" - android:layout_width="match_parent" - android:layout_height="fill_parent"/> - -</FrameLayout> diff --git a/java/com/android/dialer/app/res/layout/call_detail_footer.xml b/java/com/android/dialer/app/res/layout/call_detail_footer.xml deleted file mode 100644 index 57713448e..000000000 --- a/java/com/android/dialer/app/res/layout/call_detail_footer.xml +++ /dev/null @@ -1,52 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2015 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. ---> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> - - <View - android:layout_width="match_parent" - android:layout_height="@dimen/divider_line_thickness" - android:background="@color/call_log_action_divider"/> - - <TextView - android:id="@+id/call_detail_action_copy" - style="@style/CallDetailActionItemStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:drawableStart="@drawable/ic_call_detail_content_copy" - android:text="@string/action_copy_number_text"/> - - <TextView - android:id="@+id/call_detail_action_edit_before_call" - style="@style/CallDetailActionItemStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:drawableStart="@drawable/ic_call_detail_edit" - android:text="@string/action_edit_number_before_call" - android:visibility="gone"/> - - <TextView - android:id="@+id/call_detail_action_report" - style="@style/CallDetailActionItemStyle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:drawableStart="@drawable/ic_call_detail_report" - android:text="@string/action_report_number" - android:visibility="gone"/> - -</LinearLayout> diff --git a/java/com/android/dialer/app/res/layout/call_detail_header.xml b/java/com/android/dialer/app/res/layout/call_detail_header.xml deleted file mode 100644 index fd85f0af1..000000000 --- a/java/com/android/dialer/app/res/layout/call_detail_header.xml +++ /dev/null @@ -1,89 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2015 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. ---> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/caller_information" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingTop="@dimen/call_detail_top_margin" - android:paddingBottom="@dimen/call_detail_bottom_margin" - android:paddingStart="@dimen/call_detail_horizontal_margin" - android:background="@color/background_dialer_white" - android:baselineAligned="false" - android:elevation="@dimen/call_detail_elevation" - android:focusable="true" - android:orientation="horizontal"> - - <QuickContactBadge - android:id="@+id/quick_contact_photo" - android:layout_width="@dimen/contact_photo_size" - android:layout_height="@dimen/contact_photo_size" - android:layout_marginTop="3dp" - android:layout_alignParentStart="true" - android:layout_gravity="top" - android:focusable="true"/> - - <LinearLayout - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:layout_marginStart="@dimen/call_detail_horizontal_margin" - android:gravity="center_vertical" - android:orientation="vertical"> - - <TextView - android:id="@+id/caller_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="2dp" - android:layout_marginBottom="3dp" - android:includeFontPadding="false" - android:singleLine="true" - android:textColor="?android:textColorPrimary" - android:textSize="@dimen/call_log_primary_text_size"/> - - <TextView - android:id="@+id/caller_number" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="1dp" - android:singleLine="true" - android:textColor="?android:textColorSecondary" - android:textSize="@dimen/call_log_detail_text_size"/> - - <TextView - android:id="@+id/phone_account_label" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:singleLine="true" - android:textColor="?android:textColorSecondary" - android:textSize="@dimen/call_log_detail_text_size" - android:visibility="gone"/> - - </LinearLayout> - - <ImageView - android:id="@+id/call_back_button" - android:layout_width="@dimen/call_log_list_item_primary_action_dimen" - android:layout_height="@dimen/call_log_list_item_primary_action_dimen" - android:layout_marginEnd="4dp" - android:background="?android:attr/selectableItemBackgroundBorderless" - android:contentDescription="@string/description_call_log_call_action" - android:scaleType="center" - android:src="@drawable/ic_call_24dp" - android:tint="@color/call_log_list_item_primary_action_icon_tint" - android:visibility="gone"/> - -</LinearLayout> diff --git a/java/com/android/dialer/app/res/layout/call_detail_history_item.xml b/java/com/android/dialer/app/res/layout/call_detail_history_item.xml index 5958ee81c..0184a42f2 100644 --- a/java/com/android/dialer/app/res/layout/call_detail_history_item.xml +++ b/java/com/android/dialer/app/res/layout/call_detail_history_item.xml @@ -27,9 +27,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> - <view + <com.android.dialer.calllogutils.CallTypeIconsView android:id="@+id/call_type_icon" - class="com.android.dialer.app.calllog.CallTypeIconsView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical"/> diff --git a/java/com/android/dialer/app/res/layout/call_log_activity.xml b/java/com/android/dialer/app/res/layout/call_log_activity.xml new file mode 100644 index 000000000..4e2b1887c --- /dev/null +++ b/java/com/android/dialer/app/res/layout/call_log_activity.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/calllog_frame" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <com.android.contacts.common.list.ViewPagerTabs + android:id="@+id/viewpager_header" + style="@style/DialtactsActionBarTabTextStyle" + android:layout_width="match_parent" + android:layout_height="@dimen/tab_height" + android:layout_gravity="top" + android:elevation="@dimen/tab_elevation" + android:orientation="horizontal" + android:textAllCaps="true"/> + <android.support.v4.view.ViewPager + android:id="@+id/call_log_pager" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"/> + <RelativeLayout + android:id="@+id/floating_action_button_container" + android:layout_width="0dp" + android:layout_height="0dp"/> +</LinearLayout> 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 c22ac861d..1592aa928 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 @@ -93,8 +93,7 @@ android:layout_height="wrap_content" android:orientation="horizontal"> - <view - class="com.android.dialer.app.calllog.CallTypeIconsView" + <com.android.dialer.calllogutils.CallTypeIconsView android:id="@+id/call_type_icons" android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/java/com/android/dialer/app/res/menu/call_log_options.xml b/java/com/android/dialer/app/res/menu/call_log_options.xml new file mode 100644 index 000000000..e78b72e3c --- /dev/null +++ b/java/com/android/dialer/app/res/menu/call_log_options.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/delete_all" + android:orderInCategory="1" + android:showAsAction="never" + android:title="@string/call_log_delete_all"/> +</menu> diff --git a/java/com/android/dialer/app/res/menu/dialtacts_options.xml b/java/com/android/dialer/app/res/menu/dialtacts_options.xml index 434aa81d9..25a3e1811 100644 --- a/java/com/android/dialer/app/res/menu/dialtacts_options.xml +++ b/java/com/android/dialer/app/res/menu/dialtacts_options.xml @@ -16,13 +16,17 @@ <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item - android:id="@+id/menu_delete_all" - android:title="@string/call_log_delete_all"/> + android:id="@+id/menu_history" + android:icon="@drawable/ic_menu_history_lt" + android:title="@string/action_menu_call_history_description"/> <item android:id="@+id/menu_clear_frequents" android:title="@string/menu_clear_frequents"/> <item android:id="@+id/menu_call_settings" android:title="@string/dialer_settings_label"/> + <item + android:id="@+id/menu_simulator_submenu" + android:title="@string/simulator_submenu_label"/> </menu> diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml index b88e55276..cf6b926be 100644 --- a/java/com/android/dialer/app/res/values/colors.xml +++ b/java/com/android/dialer/app/res/values/colors.xml @@ -16,7 +16,6 @@ <resources> <color name="dialer_red_highlight_color">#ff1744</color> - <color name="dialer_green_highlight_color">#00c853</color> <color name="dialer_button_text_color">#fff</color> <color name="dialer_flat_button_text_color">@color/dialer_theme_color</color> @@ -84,13 +83,6 @@ as call back, play voicemail, etc. --> <color name="call_log_action_text">@color/dialer_theme_color</color> - <!-- Color for missed call icons. --> - <color name="missed_call">#ff2e58</color> - <!-- Color for answered or outgoing call icons. --> - <color name="answered_call">@color/dialer_green_highlight_color</color> - <!-- Color for blocked call icons. --> - <color name="blocked_call">@color/dialer_secondary_text_color</color> - <color name="dialer_dialpad_touch_tint">@color/dialer_theme_color_20pct</color> <color name="floating_action_button_touch_tint">#80ffffff</color> diff --git a/java/com/android/dialer/app/res/values/dimens.xml b/java/com/android/dialer/app/res/values/dimens.xml index f3fd63350..7da29c7a3 100644 --- a/java/com/android/dialer/app/res/values/dimens.xml +++ b/java/com/android/dialer/app/res/values/dimens.xml @@ -28,7 +28,6 @@ <dimen name="call_log_horizontal_margin">8dp</dimen> <dimen name="call_log_call_action_size">32dp</dimen> <dimen name="call_log_call_action_width">54dp</dimen> - <dimen name="call_log_icon_margin">4dp</dimen> <dimen name="call_log_inner_margin">13dp</dimen> <dimen name="call_log_outer_margin">8dp</dimen> <dimen name="call_log_start_margin">8dp</dimen> @@ -68,7 +67,7 @@ <item name="contact_tile_height_to_width_ratio" type="dimen">76%</item> <dimen name="contact_tile_text_side_padding">12dp</dimen> <dimen name="contact_tile_text_bottom_padding">9dp</dimen> - <dimen name="favorites_row_top_padding">2dp</dimen> + <dimen name="favorites_row_top_padding">1dp</dimen> <dimen name="favorites_row_bottom_padding">0dp</dimen> <dimen name="favorites_row_start_padding">1dp</dimen> @@ -143,6 +142,4 @@ <dimen name="blocked_number_search_text_size">14sp</dimen> <dimen name="blocked_number_settings_description_text_size">14sp</dimen> <dimen name="blocked_number_header_height">48dp</dimen> - - <dimen name="call_type_icon_size">12dp</dimen> </resources> diff --git a/java/com/android/dialer/app/res/values/strings.xml b/java/com/android/dialer/app/res/values/strings.xml index 689ee1ba8..66bf70f1a 100644 --- a/java/com/android/dialer/app/res/values/strings.xml +++ b/java/com/android/dialer/app/res/values/strings.xml @@ -55,9 +55,6 @@ <!-- Label for action to unblock a number [CHAR LIMIT=48]--> <string name="action_unblock_number">Unblock number</string> - <!-- Menu item in call details used to remove a call or voicemail from the call log. --> - <string name="call_details_delete">Delete</string> - <!-- Label for action to edit a number before calling it. [CHAR LIMIT=48] --> <string name="action_edit_number_before_call">Edit number before call</string> @@ -94,7 +91,7 @@ <!-- Missed call notification label, used when there are two or more missed calls --> <string name="notification_missedCallsTitle">Missed calls</string> <!-- Missed call notification message used when there are multiple missed calls --> - <string name="notification_missedCallsMsg"><xliff:g id="num_missed_calls">%s</xliff:g> missed calls</string> + <string name="notification_missedCallsMsg"><xliff:g id="num_missed_calls">%d</xliff:g> missed calls</string> <!-- Message for "call back" Action, which is displayed in the missed call notificaiton. The user will be able to call back to the person or the phone number. [CHAR LIMIT=18] --> @@ -251,15 +248,13 @@ <!-- Label for the dialer app setting page [CHAR LIMIT=30]--> <string name="dialer_settings_label">Settings</string> + <!-- Label for the simulator submenu. This is used to show actions that are useful for development + and testing. [CHAR LIMIT=30]--> + <string name="simulator_submenu_label">Simulator</string> + <!-- Menu item to display all contacts [CHAR LIMIT=30] --> <string name="menu_allContacts">All contacts</string> - <!-- Title bar for call detail screen --> - <string name="callDetailTitle">Call details</string> - - <!-- Toast for call detail screen when couldn't read the requested details --> - <string name="toast_call_detail_error">Details not available</string> - <!-- Item label: jump to the in-call DTMF dialpad. (Part of a list of options shown in the dialer when another call is already in progress.) --> @@ -275,52 +270,6 @@ is already in progress.) --> <string name="dialer_addAnotherCall">Add call</string> - <!-- Title for incoming call type. [CHAR LIMIT=40] --> - <string name="type_incoming">Incoming call</string> - - <!-- Title for incoming call which was transferred to another device. [CHAR LIMIT=60] --> - <string name="type_incoming_pulled">Incoming call transferred to another device</string> - - <!-- Title for outgoing call type. [CHAR LIMIT=40] --> - <string name="type_outgoing">Outgoing call</string> - - <!-- Title for outgoing call which was transferred to another device. [CHAR LIMIT=60] --> - <string name="type_outgoing_pulled">Outgoing call transferred to another device</string> - - <!-- Title for missed call type. [CHAR LIMIT=40] --> - <string name="type_missed">Missed call</string> - - <!-- Title for incoming video call in call details screen [CHAR LIMIT=60] --> - <string name="type_incoming_video">Incoming video call</string> - - <!-- Title for incoming video call in call details screen which was transferred to another device. - [CHAR LIMIT=60] --> - <string name="type_incoming_video_pulled">Incoming video call transferred to another device</string> - - <!-- Title for outgoing video call in call details screen [CHAR LIMIT=60] --> - <string name="type_outgoing_video">Outgoing video call</string> - - <!-- Title for outgoing video call in call details screen which was transferred to another device. - [CHAR LIMIT=60] --> - <string name="type_outgoing_video_pulled">Outgoing video call transferred to another device</string> - - <!-- Title for missed video call in call details screen [CHAR LIMIT=60] --> - <string name="type_missed_video">Missed video call</string> - - <!-- Title for voicemail details screen --> - <string name="type_voicemail">Voicemail</string> - - <!-- Title for rejected call type. [CHAR LIMIT=40] --> - <string name="type_rejected">Declined call</string> - - <!-- Title for blocked call type. [CHAR LIMIT=40] --> - <string name="type_blocked">Blocked call</string> - - <!-- Title for "answered elsewhere" call type. This will happen if a call was ringing - simultaneously on multiple devices, and the user answered it on a device other than the - current device. [CHAR LIMIT=60] --> - <string name="type_answered_elsewhere">Call answered on another device</string> - <!-- Description for incoming calls going to voice mail vs. not --> <string name="actionIncomingCall">Incoming calls</string> @@ -623,28 +572,10 @@ [CHAR LIMIT=NONE] --> <string name="description_outgoing_call">Call to <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string> - <!-- String describing the phone account the call was made on or to. This string will be used - in description_incoming_missed_call, description_incoming_answered_call, and - description_outgoing_call. - Note: AccessibilityServices uses this attribute to announce what the view represents. - [CHAR LIMIT=NONE] --> - <string name="description_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">^1</xliff:g></string> - - <!-- String describing the secondary line number the call was received via. - Note: AccessibilityServices use this attribute to announce what the view represents. - [CHAR LIMIT=NONE]--> - <string name="description_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string> - <!-- TextView text item showing the secondary line number the call was received via. [CHAR LIMIT=NONE]--> <string name="call_log_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string> - <!-- String describing the PhoneAccount and via number that a call was received on, if both are - visible. - Note: AccessibilityServices use this attribute to announce what the view represents. - [CHAR LIMIT=NONE]--> - <string name="description_via_number_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g>, via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g></string> - <!-- The order of the PhoneAccount and via number that a call was received on, if both are visible. [CHAR LIMIT=NONE]--> @@ -827,6 +758,9 @@ <!-- Label for the blocked numbers settings section [CHAR LIMIT=30] --> <string name="manage_blocked_numbers_label">Call blocking</string> + <!-- Label for the voicemail settings section [CHAR LIMIT=30] --> + <string name="voicemail_settings_label">Voicemail</string> + <!-- Label for a section describing that call blocking is temporarily disabled because an emergency call was made. [CHAR LIMIT=50] --> <string name="blocked_numbers_disabled_emergency_header_label"> @@ -955,6 +889,6 @@ <string name="spam_number_call_log_label">Spam</string> <!-- Shown as a message that notifies the user enriched calling isn't working --> - <string name="call_composer_connection_failed"><xliff:g id="feature">%1$s</xliff:g> unavailable right now</string> + <string name="call_composer_connection_failed"><xliff:g id="name">%1$s</xliff:g> is offline and can\'t be reached</string> </resources> diff --git a/java/com/android/dialer/app/res/values/styles.xml b/java/com/android/dialer/app/res/values/styles.xml index ac4422ba2..24521ddaf 100644 --- a/java/com/android/dialer/app/res/values/styles.xml +++ b/java/com/android/dialer/app/res/values/styles.xml @@ -111,11 +111,6 @@ <item name="android:fastScrollTrackDrawable">@null</item> </style> - <style name="CallDetailActivityTheme" parent="DialtactsThemeWithoutActionBarOverlay"> - <item name="android:windowBackground">@color/background_dialer_results</item> - <item name="android:actionOverflowButtonStyle">@style/DialtactsActionBarOverflowWhite</item> - </style> - <style name="CallDetailActionItemStyle"> <item name="android:foreground">?android:attr/selectableItemBackground</item> <item name="android:clickable">true</item> diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java index b04674013..bbf1cfae5 100644 --- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java +++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java @@ -33,8 +33,11 @@ import com.android.dialer.app.R; import com.android.dialer.blocking.FilteredNumberCompat; import com.android.dialer.compat.CompatUtils; import com.android.dialer.proguard.UsedByReflection; +import com.android.voicemail.VoicemailComponent; import java.util.List; +/** Activity for dialer settings. */ +@SuppressWarnings("FragmentInjection") // Activity not exported @UsedByReflection(value = "AndroidManifest-app.xml") public class DialerSettingsActivity extends AppCompatPreferenceActivity { @@ -115,6 +118,16 @@ public class DialerSettingsActivity extends AppCompatPreferenceActivity { target.add(blockedCallsHeader); migrationStatusOnBuildHeaders = FilteredNumberCompat.hasMigratedToNewBlocking(this); } + + String voicemailSettingsFragment = + VoicemailComponent.get(this).getVoicemailClient().getSettingsFragment(); + if (isPrimaryUser && voicemailSettingsFragment != null) { + Header voicemailSettings = new Header(); + voicemailSettings.titleRes = R.string.voicemail_settings_label; + voicemailSettings.fragment = voicemailSettingsFragment; + target.add(voicemailSettings); + } + if (isPrimaryUser && (TelephonyManagerCompat.isTtyModeSupported(telephonyManager) || TelephonyManagerCompat.isHearingAidCompatibilitySupported(telephonyManager))) { diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java index fc6a37608..f40ed2794 100644 --- a/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java +++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java @@ -30,7 +30,6 @@ import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; -import com.android.dialer.app.PhoneCallDetails; import com.android.dialer.app.R; import com.android.dialer.app.calllog.CallLogAsyncTaskUtil; import com.android.dialer.app.calllog.CallLogListItemViewHolder; @@ -347,16 +346,10 @@ public class VoicemailPlaybackLayout extends LinearLayout } @Override - public void onDeleteCall() {} - - @Override public void onDeleteVoicemail() { mPresenter.onVoicemailDeletedInDatabase(); } - @Override - public void onGetCallDetails(PhoneCallDetails[] details) {} - private String getString(int resId) { return mContext.getString(resId); } diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java index 657022291..994160ff9 100644 --- a/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java +++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java @@ -39,6 +39,7 @@ import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v4.content.FileProvider; import android.text.TextUtils; +import android.view.View; import android.view.WindowManager.LayoutParams; import android.webkit.MimeTypeMap; import com.android.common.io.MoreCloseables; @@ -47,8 +48,11 @@ import com.android.dialer.app.calllog.CallLogListItemViewHolder; import com.android.dialer.common.Assert; import com.android.dialer.common.AsyncTaskExecutor; import com.android.dialer.common.AsyncTaskExecutors; +import com.android.dialer.common.ConfigProviderBindings; import com.android.dialer.common.LogUtil; import com.android.dialer.constants.Constants; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; import com.android.dialer.phonenumbercache.CallLogQuery; import com.google.common.io.ByteStreams; import java.io.File; @@ -71,9 +75,9 @@ import javax.annotation.concurrent.ThreadSafe; * assumptions about the behaviors and lifecycle of the call log, in particular in the {@link * CallLogFragment} and {@link CallLogAdapter}. * - * <p>This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single - * instance can be reused for different such layouts, using {@link #setPlaybackView}. This is to - * facilitate reuse across different voicemail call log entries. + * <p>This controls a single {@link com.android.dialer.app.voicemail.VoicemailPlaybackLayout}. A + * single instance can be reused for different such layouts, using {@link #setPlaybackView}. This is + * to facilitate reuse across different voicemail call log entries. * * <p>This class is not thread safe. The thread policy for this class is thread-confinement, all * calls into this class from outside must be done from the main UI thread. @@ -103,6 +107,8 @@ public class VoicemailPlaybackPresenter private static final String IS_SPEAKERPHONE_ON_KEY = VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON"; private static final String VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT = "MM-dd-yy_hhmmaa"; + private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed"; + private static VoicemailPlaybackPresenter sInstance; private static ScheduledExecutorService mScheduledExecutorService; /** @@ -138,6 +144,7 @@ public class VoicemailPlaybackPresenter private PowerManager.WakeLock mProximityWakeLock; private VoicemailAudioManager mVoicemailAudioManager; private OnVoicemailDeletedListener mOnVoicemailDeletedListener; + private View shareVoicemailButtonView; /** Initialize variables which are activity-independent and state-independent. */ protected VoicemailPlaybackPresenter(Activity activity) { @@ -222,11 +229,17 @@ public class VoicemailPlaybackPresenter /** Specify the view which this presenter controls and the voicemail to prepare to play. */ public void setPlaybackView( - PlaybackView view, long rowId, Uri voicemailUri, final boolean startPlayingImmediately) { + PlaybackView view, + long rowId, + Uri voicemailUri, + final boolean startPlayingImmediately, + View shareVoicemailButtonView) { mRowId = rowId; mView = view; mView.setPresenter(this, voicemailUri); mView.onSpeakerphoneOn(mIsSpeakerphoneOn); + this.shareVoicemailButtonView = shareVoicemailButtonView; + showShareVoicemailButton(false); // Handles cases where the same entry is binded again when scrolling in list, or where // the MediaPlayer was retained after an orientation change. @@ -236,6 +249,7 @@ public class VoicemailPlaybackPresenter // media player. mPosition = mMediaPlayer.getCurrentPosition(); onPrepared(mMediaPlayer); + showShareVoicemailButton(true); } else { if (!voicemailUri.equals(mVoicemailUri)) { mVoicemailUri = voicemailUri; @@ -247,19 +261,17 @@ public class VoicemailPlaybackPresenter * it if the content is not available. */ checkForContent( - new OnContentCheckedListener() { - @Override - public void onContentChecked(boolean hasContent) { - if (hasContent) { - prepareContent(); - } else { - if (startPlayingImmediately) { - requestContent(PLAYBACK_REQUEST); - } - if (mView != null) { - mView.resetSeekBar(); - mView.setClipPosition(0, mDuration.get()); - } + hasContent -> { + if (hasContent) { + showShareVoicemailButton(true); + prepareContent(); + } else { + if (startPlayingImmediately) { + requestContent(PLAYBACK_REQUEST); + } + if (mView != null) { + mView.resetSeekBar(); + mView.setClipPosition(0, mDuration.get()); } } }); @@ -547,6 +559,7 @@ public class VoicemailPlaybackPresenter mPosition = 0; mIsPlaying = false; + showShareVoicemailButton(false); } /** After done playing the voicemail clip, reset the clip position to the start. */ @@ -600,18 +613,16 @@ public class VoicemailPlaybackPresenter * timeout, but succeeded. */ checkForContent( - new OnContentCheckedListener() { - @Override - public void onContentChecked(boolean hasContent) { - if (!hasContent) { - // No local content, download from server. Queue playing if the request was - // issued, - mIsPlaying = requestContent(PLAYBACK_REQUEST); - } else { - // Queue playing once the media play loaded the content. - mIsPlaying = true; - prepareContent(); - } + hasContent -> { + if (!hasContent) { + // No local content, download from server. Queue playing if the request was + // issued, + mIsPlaying = requestContent(PLAYBACK_REQUEST); + } else { + showShareVoicemailButton(true); + // Queue playing once the media play loaded the content. + mIsPlaying = true; + prepareContent(); } }); return; @@ -813,6 +824,20 @@ public class VoicemailPlaybackPresenter sInstance = null; } + private void showShareVoicemailButton(boolean show) { + if (isShareVoicemailAllowed(mContext) && shareVoicemailButtonView != null) { + if (show) { + Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE); + } + LogUtil.d("VoicemailPlaybackPresenter.showShareVoicemailButton", "show: %b", show); + shareVoicemailButtonView.setVisibility(show ? View.VISIBLE : View.GONE); + } + } + + private static boolean isShareVoicemailAllowed(Context context) { + return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true); + } + /** * Share voicemail to be opened by user selected apps. This method will collect information, copy * voicemail to a temporary file in background and launch a chooser intent to share it. @@ -1041,6 +1066,7 @@ public class VoicemailPlaybackPresenter public void onPostExecute(Boolean hasContent) { if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) { mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this); + showShareVoicemailButton(true); prepareContent(); } } diff --git a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java index e36406d17..190426e6e 100644 --- a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java +++ b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java @@ -17,10 +17,18 @@ package com.android.dialer.app.voicemail.error; import android.content.Context; +import android.preference.PreferenceManager; import android.provider.VoicemailContract.Status; import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action; +import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.PerAccountSharedPreferences; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.voicemail.VoicemailClient; +import com.android.voicemail.VoicemailComponent; import java.util.ArrayList; import java.util.List; @@ -32,14 +40,18 @@ public class OmtpVoicemailMessageCreator { private static final float QUOTA_NEAR_FULL_THRESHOLD = 0.9f; private static final float QUOTA_FULL_THRESHOLD = 0.99f; + protected static final String VOICEMAIL_PROMO_DISMISSED_KEY = + "voicemail_archive_promo_was_dismissed"; + protected static final String VOICEMAIL_PROMO_ALMOST_FULL_DISMISSED_KEY = + "voicemail_archive_almost_full_promo_was_dismissed"; @Nullable - public static VoicemailErrorMessage create(Context context, VoicemailStatus status) { + public static VoicemailErrorMessage create( + Context context, VoicemailStatus status, final VoicemailStatusReader statusReader) { if (Status.CONFIGURATION_STATE_OK == status.configurationState && Status.DATA_CHANNEL_STATE_OK == status.dataChannelState && Status.NOTIFICATION_CHANNEL_STATE_OK == status.notificationChannelState) { - - return checkQuota(context, status); + return checkQuota(context, status, statusReader); } // Initial state when the source is activating. Other error might be written into data and // notification channel during activation. @@ -120,24 +132,98 @@ public class OmtpVoicemailMessageCreator { } @Nullable - private static VoicemailErrorMessage checkQuota(Context context, VoicemailStatus status) { + private static VoicemailErrorMessage checkQuota( + Context context, VoicemailStatus status, VoicemailStatusReader statusReader) { if (status.quotaOccupied != Status.QUOTA_UNAVAILABLE && status.quotaTotal != Status.QUOTA_UNAVAILABLE) { + + PhoneAccountHandle phoneAccountHandle = status.getPhoneAccountHandle(); + + VoicemailClient voicemailClient = VoicemailComponent.get(context).getVoicemailClient(); + + PerAccountSharedPreferences sharedPreferenceForAccount = + new PerAccountSharedPreferences( + context, phoneAccountHandle, PreferenceManager.getDefaultSharedPreferences(context)); + + boolean isVoicemailArchiveEnabled = + VoicemailComponent.get(context) + .getVoicemailClient() + .isVoicemailArchiveEnabled(context, phoneAccountHandle); + if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_FULL_THRESHOLD) { - return new VoicemailErrorMessage( + return createInboxErrorMessage( + context, + status, + status.getPhoneAccountHandle(), + statusReader, + sharedPreferenceForAccount, + voicemailClient, + isVoicemailArchiveEnabled, + context.getString(R.string.voicemail_error_inbox_full_turn_archive_on_title), + context.getString(R.string.voicemail_error_inbox_full_turn_archive_on_message), context.getString(R.string.voicemail_error_inbox_full_title), - context.getString(R.string.voicemail_error_inbox_full_message)); + context.getString(R.string.voicemail_error_inbox_full_message), + VOICEMAIL_PROMO_DISMISSED_KEY); } if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_NEAR_FULL_THRESHOLD) { - return new VoicemailErrorMessage( + return createInboxErrorMessage( + context, + status, + status.getPhoneAccountHandle(), + statusReader, + sharedPreferenceForAccount, + voicemailClient, + isVoicemailArchiveEnabled, + context.getString(R.string.voicemail_error_inbox_almost_full_turn_archive_on_title), + context.getString(R.string.voicemail_error_inbox_almost_full_turn_archive_on_message), context.getString(R.string.voicemail_error_inbox_near_full_title), - context.getString(R.string.voicemail_error_inbox_near_full_message)); + context.getString(R.string.voicemail_error_inbox_near_full_message), + VOICEMAIL_PROMO_ALMOST_FULL_DISMISSED_KEY); } } return null; } + private static VoicemailErrorMessage createInboxErrorMessage( + Context context, + VoicemailStatus status, + PhoneAccountHandle phoneAccountHandle, + VoicemailStatusReader statusReader, + PerAccountSharedPreferences sharedPreferenceForAccount, + VoicemailClient voicemailClient, + boolean isVoicemailArchiveEnabled, + String promoTitle, + String promoMessage, + String nonPromoTitle, + String nonPromoMessage, + String preferenceKey) { + + boolean wasPromoDismissed = sharedPreferenceForAccount.getBoolean(preferenceKey, false); + + if (!wasPromoDismissed && !isVoicemailArchiveEnabled) { + logArchiveImpression( + context, + preferenceKey, + DialerImpression.Type.VVM_USER_SHOWN_VM_ALMOST_FULL_PROMO, + DialerImpression.Type.VVM_USER_SHOWN_VM_FULL_PROMO); + return new VoicemailErrorMessage( + promoTitle, + promoMessage, + VoicemailErrorMessage.createDismissTurnArchiveOnAction( + context, statusReader, sharedPreferenceForAccount, preferenceKey), + VoicemailErrorMessage.createTurnArchiveOnAction( + context, status, voicemailClient, phoneAccountHandle, preferenceKey)); + } else { + logArchiveImpression( + context, + preferenceKey, + DialerImpression.Type.VVM_USER_SHOWN_VM_ALMOST_FULL_ERROR_MESSAGE, + DialerImpression.Type.VVM_USER_SHOWN_VM_FULL_ERROR_MESSAGE); + return new VoicemailErrorMessage(nonPromoTitle, nonPromoMessage); + } + } + @Nullable private static VoicemailErrorMessage createNoSignalMessage( Context context, VoicemailStatus status) { @@ -174,4 +260,15 @@ public class OmtpVoicemailMessageCreator { } return new VoicemailErrorMessage(title, description, actions); } + + protected static void logArchiveImpression( + Context context, String preference, int vmAlmostFullImpression, int vmFullImpression) { + if (preference.equals(VOICEMAIL_PROMO_DISMISSED_KEY)) { + Logger.get(context).logImpression(vmAlmostFullImpression); + } else if (preference.equals(VOICEMAIL_PROMO_ALMOST_FULL_DISMISSED_KEY)) { + Logger.get(context).logImpression(vmFullImpression); + } else { + throw Assert.createAssertionFailException("Invalid preference key " + preference); + } + } } diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java index 61572008b..f85d91186 100644 --- a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java +++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java @@ -22,12 +22,15 @@ import android.provider.Settings; import android.provider.VoicemailContract; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; import android.telephony.TelephonyManager; import android.view.View; import android.view.View.OnClickListener; +import com.android.dialer.common.PerAccountSharedPreferences; import com.android.dialer.logging.Logger; import com.android.dialer.logging.nano.DialerImpression; import com.android.dialer.util.CallUtil; +import com.android.voicemail.VoicemailClient; import java.util.Arrays; import java.util.List; @@ -175,4 +178,52 @@ public class VoicemailErrorMessage { } }); } + + @NonNull + public static Action createTurnArchiveOnAction( + final Context context, + final VoicemailStatus status, + VoicemailClient voicemailClient, + PhoneAccountHandle phoneAccountHandle, + String preference) { + return new Action( + context.getString(R.string.voicemail_action_turn_archive_on), + new OnClickListener() { + @Override + public void onClick(View v) { + OmtpVoicemailMessageCreator.logArchiveImpression( + context, + preference, + DialerImpression.Type.VVM_USER_ENABLED_ARCHIVE_FROM_VM_FULL_PROMO, + DialerImpression.Type.VVM_USER_ENABLED_ARCHIVE_FROM_VM_ALMOST_FULL_PROMO); + + voicemailClient.setVoicemailArchiveEnabled(context, phoneAccountHandle, true); + Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL); + intent.setPackage(status.sourcePackage); + context.sendBroadcast(intent); + } + }); + } + + @NonNull + public static Action createDismissTurnArchiveOnAction( + final Context context, + VoicemailStatusReader statusReader, + PerAccountSharedPreferences sharedPreferenceForAccount, + String preferenceKeyToUpdate) { + return new Action( + context.getString(R.string.voicemail_action_dimiss), + new OnClickListener() { + @Override + public void onClick(View v) { + OmtpVoicemailMessageCreator.logArchiveImpression( + context, + preferenceKeyToUpdate, + DialerImpression.Type.VVM_USER_DISMISSED_VM_FULL_PROMO, + DialerImpression.Type.VVM_USER_DISMISSED_VM_ALMOST_FULL_PROMO); + sharedPreferenceForAccount.edit().putBoolean(preferenceKeyToUpdate, true); + statusReader.refresh(); + } + }); + } } diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java index 5ebef801d..7dc18f043 100644 --- a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java +++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java @@ -39,7 +39,7 @@ public class VoicemailErrorMessageCreator { case Vvm3VoicemailMessageCreator.VVM_TYPE_VVM3: return Vvm3VoicemailMessageCreator.create(context, status, statusReader); default: - return OmtpVoicemailMessageCreator.create(context, status); + return OmtpVoicemailMessageCreator.create(context, status, statusReader); } } } diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java index a09941de2..c429d6dcc 100644 --- a/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java +++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java @@ -16,6 +16,7 @@ package com.android.dialer.app.voicemail.error; +import android.content.ComponentName; import android.content.Context; import android.database.Cursor; import android.net.Uri; @@ -25,6 +26,7 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.provider.VoicemailContract.Status; import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; import android.telephony.TelephonyManager; import com.android.dialer.database.VoicemailStatusQuery; @@ -257,4 +259,9 @@ public class VoicemailStatus { } return cursor.getString(index); } + + public PhoneAccountHandle getPhoneAccountHandle() { + return new PhoneAccountHandle( + ComponentName.unflattenFromString(phoneAccountComponentName), phoneAccountId); + } } diff --git a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java index 6e9405cbf..356131bb3 100644 --- a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java +++ b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java @@ -269,7 +269,7 @@ public class Vvm3VoicemailMessageCreator { VoicemailErrorMessage.createSetPinAction(context)); } - return OmtpVoicemailMessageCreator.create(context, status); + return OmtpVoicemailMessageCreator.create(context, status, statusReader); } @NonNull diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml index 0dfb1c2fd..4a40857a0 100644 --- a/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml +++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml @@ -48,7 +48,6 @@ <TextView android:id="@+id/error_card_details" - android:autoLink="web" android:layout_width="wrap_content" android:layout_height="wrap_content" android:lineSpacingExtra="@dimen/alert_line_spacing" 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 2b9d17328..c193eaa47 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 @@ -31,7 +31,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingBottom="16dp" - android:autoLink="web" android:text="@string/verizon_terms_and_conditions_1.1_english" android:textColor="@color/secondary_text_color" android:textSize="@dimen/call_log_detail_text_size"/> 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 1d39b9dcb..94d3dba11 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 @@ -54,6 +54,11 @@ <string name="voicemail_error_inbox_full_title">Can\'t receive new voicemails</string> <string name="voicemail_error_inbox_full_message">Your inbox is full. Try deleting some messages to receive new voicemail.</string> + <string name="voicemail_error_inbox_full_turn_archive_on_title">Turn on extra storage and backup</string> + <string name="voicemail_error_inbox_full_turn_archive_on_message">Your mailbox is full. To free up space, turn on extra storage so Google can manage and backup your voicemail messages.</string> + + <string name="voicemail_error_inbox_almost_full_turn_archive_on_title">Turn on extra storage and backup</string> + <string name="voicemail_error_inbox_almost_full_turn_archive_on_message">Your mailbox is almost full. To free up space, turn on extra storage so Google can manage and backup your voicemail messages.</string> <string name="voicemail_error_pin_not_set_title">Set your voicemail PIN</string> <string name="voicemail_error_pin_not_set_message">You\'ll need a voicemail PIN anytime you call to access your voicemail.</string> @@ -63,6 +68,8 @@ <string name="voicemail_action_turn_off_airplane_mode">Airplane Mode Settings</string> <string name="voicemail_action_set_pin">Set PIN</string> <string name="voicemail_action_retry">Try Again</string> + <string name="voicemail_action_turn_archive_on">Turn on</string> + <string name="voicemail_action_dimiss">No Thanks</string> <string name="voicemail_action_sync">Sync</string> <string name="voicemail_action_call_voicemail">Call Voicemail</string> <string name="voicemail_action_call_customer_support">Call Customer Support</string> diff --git a/java/com/android/dialer/app/widget/ActionBarController.java b/java/com/android/dialer/app/widget/ActionBarController.java index 7fe056c51..d0eb326ab 100644 --- a/java/com/android/dialer/app/widget/ActionBarController.java +++ b/java/com/android/dialer/app/widget/ActionBarController.java @@ -16,12 +16,9 @@ package com.android.dialer.app.widget; import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; import android.os.Bundle; -import android.support.annotation.VisibleForTesting; -import android.util.Log; import com.android.dialer.animation.AnimUtils.AnimationCallback; -import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.common.LogUtil; /** * Controls the various animated properties of the actionBar: showing/hiding, fading/revealing, and @@ -30,8 +27,6 @@ import com.android.dialer.app.DialtactsActivity; */ public class ActionBarController { - public static final boolean DEBUG = DialtactsActivity.DEBUG; - public static final String TAG = "ActionBarController"; private static final String KEY_IS_SLID_UP = "key_actionbar_is_slid_up"; private static final String KEY_IS_FADED_OUT = "key_actionbar_is_faded_out"; private static final String KEY_IS_EXPANDED = "key_actionbar_is_expanded"; @@ -66,9 +61,8 @@ public class ActionBarController { /** Called when the user has tapped on the collapsed search box, to start a new search query. */ public void onSearchBoxTapped() { - if (DEBUG) { - Log.d(TAG, "OnSearchBoxTapped: isInSearchUi " + mActivityUi.isInSearchUi()); - } + LogUtil.d( + "ActionBarController.onSearchBoxTapped", "isInSearchUi " + mActivityUi.isInSearchUi()); if (!mActivityUi.isInSearchUi()) { mSearchBox.expand(true /* animate */, true /* requestFocus */); } @@ -76,16 +70,11 @@ public class ActionBarController { /** Called when search UI has been exited for some reason. */ public void onSearchUiExited() { - if (DEBUG) { - Log.d( - TAG, - "OnSearchUIExited: isExpanded " - + mSearchBox.isExpanded() - + " isFadedOut: " - + mSearchBox.isFadedOut() - + " shouldShowActionBar: " - + mActivityUi.shouldShowActionBar()); - } + LogUtil.d( + "ActionBarController.onSearchUIExited", + "isExpanded: %b, isFadedOut %b", + mSearchBox.isExpanded(), + mSearchBox.isFadedOut()); if (mSearchBox.isExpanded()) { mSearchBox.collapse(true /* animate */); } @@ -93,11 +82,7 @@ public class ActionBarController { mSearchBox.fadeIn(); } - if (mActivityUi.shouldShowActionBar()) { - slideActionBar(false /* slideUp */, false /* animate */); - } else { - slideActionBar(true /* slideUp */, false /* animate */); - } + slideActionBar(false /* slideUp */, false /* animate */); } /** @@ -105,18 +90,13 @@ public class ActionBarController { * state changes have actually occurred. */ public void onDialpadDown() { - if (DEBUG) { - Log.d( - TAG, - "OnDialpadDown: isInSearchUi " - + mActivityUi.isInSearchUi() - + " hasSearchQuery: " - + mActivityUi.hasSearchQuery() - + " isFadedOut: " - + mSearchBox.isFadedOut() - + " isExpanded: " - + mSearchBox.isExpanded()); - } + LogUtil.d( + "ActionBarController.onDialpadDown", + "isInSearchUi: %b, hasSearchQuery: %b, isFadedOut: %b, isExpanded: %b", + mActivityUi.isInSearchUi(), + mActivityUi.hasSearchQuery(), + mSearchBox.isFadedOut(), + mSearchBox.isExpanded()); if (mActivityUi.isInSearchUi()) { if (mActivityUi.hasSearchQuery()) { if (mSearchBox.isFadedOut()) { @@ -137,9 +117,7 @@ public class ActionBarController { * state changes have actually occurred. */ public void onDialpadUp() { - if (DEBUG) { - Log.d(TAG, "OnDialpadUp: isInSearchUi " + mActivityUi.isInSearchUi()); - } + LogUtil.d("ActionBarController.onDialpadUp", "isInSearchUi " + mActivityUi.isInSearchUi()); if (mActivityUi.isInSearchUi()) { slideActionBar(true /* slideUp */, true /* animate */); } else { @@ -149,18 +127,14 @@ public class ActionBarController { } public void slideActionBar(boolean slideUp, boolean animate) { - if (DEBUG) { - Log.d(TAG, "Sliding actionBar - up: " + slideUp + " animate: " + animate); - } + LogUtil.d("ActionBarController.slidingActionBar", "up: %b, animate: %b", slideUp, animate); + if (animate) { ValueAnimator animator = slideUp ? ValueAnimator.ofFloat(0, 1) : ValueAnimator.ofFloat(1, 0); animator.addUpdateListener( - new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - final float value = (float) animation.getAnimatedValue(); - setHideOffset((int) (mActivityUi.getActionBarHeight() * value)); - } + animation -> { + final float value = (float) animation.getAnimatedValue(); + setHideOffset((int) (mActivityUi.getActionBarHeight() * value)); }); animator.start(); } else { @@ -173,20 +147,11 @@ public class ActionBarController { mSearchBox.animate().alpha(alphaValue).start(); } - /** @return The offset the action bar is being translated upwards by */ - public int getHideOffset() { - return mActivityUi.getActionBarHideOffset(); - } - public void setHideOffset(int offset) { mIsActionBarSlidUp = offset >= mActivityUi.getActionBarHeight(); mActivityUi.setActionBarHideOffset(offset); } - public int getActionBarHeight() { - return mActivityUi.getActionBarHeight(); - } - /** Saves the current state of the action bar into a provided {@link Bundle} */ public void saveInstanceState(Bundle outState) { outState.putBoolean(KEY_IS_SLID_UP, mIsActionBarSlidUp); @@ -225,23 +190,14 @@ public class ActionBarController { slideActionBar(mIsActionBarSlidUp /* slideUp */, false /* animate */); } - @VisibleForTesting - public boolean getIsActionBarSlidUp() { - return mIsActionBarSlidUp; - } - public interface ActivityUi { boolean isInSearchUi(); boolean hasSearchQuery(); - boolean shouldShowActionBar(); - int getActionBarHeight(); - int getActionBarHideOffset(); - void setActionBarHideOffset(int offset); } } diff --git a/java/com/android/dialer/backup/AndroidManifest.xml b/java/com/android/dialer/backup/AndroidManifest.xml index cfdb3d93d..1cbbe5339 100644 --- a/java/com/android/dialer/backup/AndroidManifest.xml +++ b/java/com/android/dialer/backup/AndroidManifest.xml @@ -21,7 +21,6 @@ android:backupAgent="com.android.dialer.backup.DialerBackupAgent" android:fullBackupOnly="true" android:restoreAnyVersion="true" - android:name="com.android.dialer.app.DialerApplication" /> -</manifest>
\ No newline at end of file +</manifest> diff --git a/java/com/android/dialer/backup/DialerBackupAgent.java b/java/com/android/dialer/backup/DialerBackupAgent.java index 391a93f29..2f8684aa2 100644 --- a/java/com/android/dialer/backup/DialerBackupAgent.java +++ b/java/com/android/dialer/backup/DialerBackupAgent.java @@ -31,6 +31,7 @@ import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.VoicemailContract; import android.provider.VoicemailContract.Voicemails; +import android.telecom.PhoneAccountHandle; import android.util.Pair; import com.android.dialer.backup.nano.VoicemailInfo; import com.android.dialer.common.Assert; @@ -42,6 +43,7 @@ import com.android.dialer.telecom.TelecomUtil; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.util.List; import java.util.Locale; /** @@ -100,9 +102,11 @@ public class DialerBackupAgent extends BackupAgent { ConfigProviderBindings.get(this).getBoolean("enable_autobackup", true); boolean vmBackupEnabled = ConfigProviderBindings.get(this).getBoolean("enable_vm_backup", false); + List<PhoneAccountHandle> phoneAccountsToArchive = + DialerBackupUtils.getPhoneAccountsToArchive(this); if (autoBackupEnabled) { - if (!maxVoicemailBackupReached && vmBackupEnabled) { + if (!maxVoicemailBackupReached && vmBackupEnabled && !phoneAccountsToArchive.isEmpty()) { voicemailsBackedupSoFar = 0; sizeOfVoicemailsBackedupSoFar = 0; @@ -123,9 +127,12 @@ public class DialerBackupAgent extends BackupAgent { uri, null, String.format( - "(%s = ? AND deleted = 0 AND %s = ?)", Calls.TYPE, Voicemails.SOURCE_PACKAGE), + "(%s = ? AND deleted = 0 AND %s = ? AND ?)", + Calls.TYPE, Voicemails.SOURCE_PACKAGE), new String[] { - Integer.toString(CallLog.Calls.VOICEMAIL_TYPE), VOICEMAIL_SOURCE_PACKAGE + Integer.toString(CallLog.Calls.VOICEMAIL_TYPE), + VOICEMAIL_SOURCE_PACKAGE, + DialerBackupUtils.getPhoneAccountClause(phoneAccountsToArchive) }, ORDER_BY_DATE, null)) { @@ -150,11 +157,12 @@ public class DialerBackupAgent extends BackupAgent { LogUtil.i( "DialerBackupAgent.onFullBackup", "vm files backed up: %d, vm size backed up:%d, " - + "max vm backup reached:%b, vm backup enabled:%b", + + "max vm backup reached:%b, vm backup enabled:%b phone accounts to archive: %d", voicemailsBackedupSoFar, sizeOfVoicemailsBackedupSoFar, maxVoicemailBackupReached, - vmBackupEnabled); + vmBackupEnabled, + phoneAccountsToArchive.size()); super.onFullBackup(data); Logger.get(this).logImpression(DialerImpression.Type.BACKUP_FULL_BACKED_UP); } else { diff --git a/java/com/android/dialer/backup/DialerBackupUtils.java b/java/com/android/dialer/backup/DialerBackupUtils.java index ff0cb4f7c..410772ff0 100644 --- a/java/com/android/dialer/backup/DialerBackupUtils.java +++ b/java/com/android/dialer/backup/DialerBackupUtils.java @@ -27,10 +27,14 @@ import android.provider.VoicemailContract; import android.provider.VoicemailContract.Voicemails; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; import android.util.Pair; import com.android.dialer.backup.nano.VoicemailInfo; +import com.android.dialer.common.Assert; import com.android.dialer.common.ConfigProviderBindings; import com.android.dialer.common.LogUtil; +import com.android.voicemail.VoicemailComponent; import com.google.common.io.ByteStreams; import com.google.common.io.Files; import com.google.protobuf.nano.MessageNano; @@ -40,6 +44,8 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; /** Helper functions for DialerBackupAgent */ public class DialerBackupUtils { @@ -317,4 +323,42 @@ public class DialerBackupUtils { } return false; } + + public static String getPhoneAccountClause(List<PhoneAccountHandle> phoneAccountsToArchive) { + Assert.checkArgument(!phoneAccountsToArchive.isEmpty()); + StringBuilder whereQuery = new StringBuilder(); + + whereQuery.append("("); + + for (int i = 0; i < phoneAccountsToArchive.size(); i++) { + whereQuery.append( + Voicemails.PHONE_ACCOUNT_ID + " = " + phoneAccountsToArchive.get(i).getId()); + + if (phoneAccountsToArchive.size() > 1 && i < phoneAccountsToArchive.size() - 1) { + whereQuery.append(" OR "); + } + } + whereQuery.append(")"); + return whereQuery.toString(); + } + + public static List<PhoneAccountHandle> getPhoneAccountsToArchive(Context context) { + List<PhoneAccountHandle> phoneAccountsToBackUp = new ArrayList<>(); + + for (PhoneAccountHandle handle : + context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) { + + if (VoicemailComponent.get(context) + .getVoicemailClient() + .isVoicemailArchiveEnabled(context, handle)) { + phoneAccountsToBackUp.add(handle); + LogUtil.i( + "DialerBackupUtils.getPhoneAccountsToArchive", "enabled for: " + handle.toString()); + } else { + LogUtil.i( + "DialerBackupUtils.getPhoneAccountsToArchive", "not enabled for: " + handle.toString()); + } + } + return phoneAccountsToBackUp; + } } diff --git a/java/com/android/dialer/backup/proto/VoicemailInfo.java b/java/com/android/dialer/backup/nano/VoicemailInfo.java index 9ff8423f3..f11595ec2 100644 --- a/java/com/android/dialer/backup/proto/VoicemailInfo.java +++ b/java/com/android/dialer/backup/nano/VoicemailInfo.java @@ -18,16 +18,17 @@ package com.android.dialer.backup.nano; +/** This file is autogenerated, but javadoc required. */ @SuppressWarnings("hiding") -public final class VoicemailInfo extends - com.google.protobuf.nano.ExtendableMessageNano<VoicemailInfo> { +public final class VoicemailInfo + extends com.google.protobuf.nano.ExtendableMessageNano<VoicemailInfo> { private static volatile VoicemailInfo[] _emptyArray; + public static VoicemailInfo[] emptyArray() { // Lazily initializes the empty array if (_emptyArray == null) { - synchronized ( - com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { + synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { if (_emptyArray == null) { _emptyArray = new VoicemailInfo[0]; } @@ -178,7 +179,8 @@ public final class VoicemailInfo extends if (this.voicemailUri != null && !this.voicemailUri.equals("")) { output.writeString(17, this.voicemailUri); } - if (!java.util.Arrays.equals(this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) { + if (!java.util.Arrays.equals( + this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) { output.writeBytes(18, this.encodedVoicemailKey); } if (this.archived != null && !this.archived.equals("")) { @@ -191,175 +193,196 @@ public final class VoicemailInfo extends protected int computeSerializedSize() { int size = super.computeSerializedSize(); if (this.date != null && !this.date.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(1, this.date); + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(1, this.date); } if (this.deleted != null && !this.deleted.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(2, this.deleted); + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(2, this.deleted); } if (this.dirty != null && !this.dirty.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(3, this.dirty); + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(3, this.dirty); } if (this.dirType != null && !this.dirType.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(4, this.dirType); + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(4, this.dirType); } if (this.duration != null && !this.duration.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(5, this.duration); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(5, this.duration); } if (this.hasContent != null && !this.hasContent.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(6, this.hasContent); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(6, this.hasContent); } if (this.isRead != null && !this.isRead.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(7, this.isRead); + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(7, this.isRead); } if (this.itemType != null && !this.itemType.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(8, this.itemType); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(8, this.itemType); } if (this.lastModified != null && !this.lastModified.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(9, this.lastModified); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize( + 9, this.lastModified); } if (this.mimeType != null && !this.mimeType.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(10, this.mimeType); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(10, this.mimeType); } if (this.number != null && !this.number.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(11, this.number); + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(11, this.number); } if (this.phoneAccountComponentName != null && !this.phoneAccountComponentName.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(12, this.phoneAccountComponentName); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize( + 12, this.phoneAccountComponentName); } if (this.phoneAccountId != null && !this.phoneAccountId.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(13, this.phoneAccountId); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize( + 13, this.phoneAccountId); } if (this.sourceData != null && !this.sourceData.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(14, this.sourceData); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(14, this.sourceData); } if (this.sourcePackage != null && !this.sourcePackage.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(15, this.sourcePackage); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize( + 15, this.sourcePackage); } if (this.transcription != null && !this.transcription.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(16, this.transcription); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize( + 16, this.transcription); } if (this.voicemailUri != null && !this.voicemailUri.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(17, this.voicemailUri); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize( + 17, this.voicemailUri); } - if (!java.util.Arrays.equals(this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeBytesSize(18, this.encodedVoicemailKey); + if (!java.util.Arrays.equals( + this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) { + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeBytesSize( + 18, this.encodedVoicemailKey); } if (this.archived != null && !this.archived.equals("")) { - size += com.google.protobuf.nano.CodedOutputByteBufferNano - .computeStringSize(19, this.archived); + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(19, this.archived); } return size; } @Override - public VoicemailInfo mergeFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) + public VoicemailInfo mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { while (true) { int tag = input.readTag(); switch (tag) { case 0: return this; - default: { - if (!super.storeUnknownField(input, tag)) { - return this; + default: + { + if (!super.storeUnknownField(input, tag)) { + return this; + } + break; + } + case 10: + { + this.date = input.readString(); + break; + } + case 18: + { + this.deleted = input.readString(); + break; + } + case 26: + { + this.dirty = input.readString(); + break; + } + case 34: + { + this.dirType = input.readString(); + break; + } + case 42: + { + this.duration = input.readString(); + break; + } + case 50: + { + this.hasContent = input.readString(); + break; + } + case 58: + { + this.isRead = input.readString(); + break; + } + case 66: + { + this.itemType = input.readString(); + break; + } + case 74: + { + this.lastModified = input.readString(); + break; + } + case 82: + { + this.mimeType = input.readString(); + break; + } + case 90: + { + this.number = input.readString(); + break; + } + case 98: + { + this.phoneAccountComponentName = input.readString(); + break; + } + case 106: + { + this.phoneAccountId = input.readString(); + break; + } + case 114: + { + this.sourceData = input.readString(); + break; + } + case 122: + { + this.sourcePackage = input.readString(); + break; + } + case 130: + { + this.transcription = input.readString(); + break; + } + case 138: + { + this.voicemailUri = input.readString(); + break; + } + case 146: + { + this.encodedVoicemailKey = input.readBytes(); + break; + } + case 154: + { + this.archived = input.readString(); + break; } - break; - } - case 10: { - this.date = input.readString(); - break; - } - case 18: { - this.deleted = input.readString(); - break; - } - case 26: { - this.dirty = input.readString(); - break; - } - case 34: { - this.dirType = input.readString(); - break; - } - case 42: { - this.duration = input.readString(); - break; - } - case 50: { - this.hasContent = input.readString(); - break; - } - case 58: { - this.isRead = input.readString(); - break; - } - case 66: { - this.itemType = input.readString(); - break; - } - case 74: { - this.lastModified = input.readString(); - break; - } - case 82: { - this.mimeType = input.readString(); - break; - } - case 90: { - this.number = input.readString(); - break; - } - case 98: { - this.phoneAccountComponentName = input.readString(); - break; - } - case 106: { - this.phoneAccountId = input.readString(); - break; - } - case 114: { - this.sourceData = input.readString(); - break; - } - case 122: { - this.sourcePackage = input.readString(); - break; - } - case 130: { - this.transcription = input.readString(); - break; - } - case 138: { - this.voicemailUri = input.readString(); - break; - } - case 146: { - this.encodedVoicemailKey = input.readBytes(); - break; - } - case 154: { - this.archived = input.readString(); - break; - } } } } @@ -369,8 +392,7 @@ public final class VoicemailInfo extends return com.google.protobuf.nano.MessageNano.mergeFrom(new VoicemailInfo(), data); } - public static VoicemailInfo parseFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) + public static VoicemailInfo parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { return new VoicemailInfo().mergeFrom(input); } diff --git a/java/com/android/dialer/binary/aosp/AndroidManifest.xml b/java/com/android/dialer/binary/aosp/AndroidManifest.xml new file mode 100644 index 000000000..63edb8397 --- /dev/null +++ b/java/com/android/dialer/binary/aosp/AndroidManifest.xml @@ -0,0 +1,116 @@ +<!-- Copyright (C) 2016 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" + coreApp="true" + package="com.android.dialer" + android:versionCode="100000" + android:versionName="10.0"> + + <uses-sdk + android:minSdkVersion="23" + android:targetSdkVersion="25"/> + + <uses-permission android:name="android.permission.CALL_PHONE"/> + <uses-permission android:name="android.permission.READ_CONTACTS"/> + <uses-permission android:name="android.permission.WRITE_CONTACTS"/> + <uses-permission android:name="android.permission.READ_CALL_LOG"/> + <uses-permission android:name="android.permission.WRITE_CALL_LOG"/> + <uses-permission android:name="android.permission.READ_PROFILE"/> + <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/> + <uses-permission android:name="android.permission.GET_ACCOUNTS"/> + <uses-permission android:name="android.permission.GET_ACCOUNTS_PRIVILEGED"/> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/> + <uses-permission android:name="android.permission.NFC"/> + <uses-permission android:name="android.permission.READ_PHONE_STATE"/> + <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> + <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/> + <uses-permission android:name="android.permission.WAKE_LOCK"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.WRITE_SETTINGS"/> + <uses-permission android:name="android.permission.USE_CREDENTIALS"/> + <uses-permission android:name="android.permission.VIBRATE"/> + <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/> + <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/> + <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL"/> + <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL"/> + <uses-permission android:name="android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK"/> + <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> + <uses-permission android:name="android.permission.BROADCAST_STICKY"/> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> + <uses-permission android:name="android.permission.SEND_SMS"/> + + <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE"/> + <!-- We use this to disable the status bar buttons of home, back and recent + during an incoming call. By doing so this allows us to not show the user + is viewing the activity in full screen alert, on a fresh system/factory + reset state of the app. --> + <uses-permission android:name="android.permission.STATUS_BAR"/> + <uses-permission android:name="android.permission.CAMERA"/> + + <!-- This tells the activity manager to not delay any of our activity + start requests, even if they happen immediately after the user + presses home. --> + <uses-permission android:name="android.permission.STOP_APP_SWITCHES"/> + + <!-- Permissions needed for badger count showing on launch icon. --> + + <!--for Samsung--> + <uses-permission android:name="com.sec.android.provider.badge.permission.READ"/> + <uses-permission android:name="com.sec.android.provider.badge.permission.WRITE"/> + + <!--for htc--> + <uses-permission android:name="com.htc.launcher.permission.READ_SETTINGS"/> + <uses-permission android:name="com.htc.launcher.permission.UPDATE_SHORTCUT"/> + + <!--for sony--> + <uses-permission android:name="com.sonyericsson.home.permission.BROADCAST_BADGE"/> + <uses-permission android:name="com.sonymobile.home.permission.PROVIDER_INSERT_BADGE"/> + + <!--for apex--> + <uses-permission android:name="com.anddoes.launcher.permission.UPDATE_COUNT"/> + + <!--for solid--> + <uses-permission android:name="com.majeur.launcher.permission.UPDATE_BADGE"/> + + <!--for huawei--> + <uses-permission android:name="com.huawei.android.launcher.permission.CHANGE_BADGE"/> + <uses-permission android:name="com.huawei.android.launcher.permission.READ_SETTINGS"/> + <uses-permission android:name="com.huawei.android.launcher.permission.WRITE_SETTINGS"/> + + <!--for ZUK--> + <uses-permission android:name="android.permission.READ_APP_BADGE"/> + + <!--for OPPO--> + <uses-permission android:name="com.oppo.launcher.permission.READ_SETTINGS"/> + <uses-permission android:name="com.oppo.launcher.permission.WRITE_SETTINGS"/> + + <application + android:backupAgent='com.android.dialer.backup.DialerBackupAgent' + android:fullBackupOnly="true" + android:restoreAnyVersion="true" + android:hardwareAccelerated="true" + android:icon="@mipmap/ic_launcher_phone" + android:label="@string/applicationLabel" + android:name="com.android.dialer.binary.aosp.AospDialerApplication" + android:supportsRtl="true" + android:usesCleartextTraffic="false"> + </application> + +</manifest> diff --git a/java/com/android/dialer/binary/aosp/AospDialerApplication.java b/java/com/android/dialer/binary/aosp/AospDialerApplication.java new file mode 100644 index 000000000..f657a3987 --- /dev/null +++ b/java/com/android/dialer/binary/aosp/AospDialerApplication.java @@ -0,0 +1,30 @@ +/* + * 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.binary.aosp; + +import android.support.annotation.NonNull; +import com.android.dialer.binary.common.DialerApplication; +import com.android.dialer.inject.ContextModule; + +/** + * The application class for the AOSP Dialer. This is a version of the Dialer app that has no + * dependency on Google Play Services. + */ +public class AospDialerApplication extends DialerApplication { + + +} diff --git a/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java new file mode 100644 index 000000000..8628e90c2 --- /dev/null +++ b/java/com/android/dialer/binary/aosp/AospDialerRootComponent.java @@ -0,0 +1,30 @@ +/* + * 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.binary.aosp; + +import com.android.dialer.binary.basecomponent.BaseDialerRootComponent; +import com.android.dialer.enrichedcall.stub.StubEnrichedCallModule; +import com.android.dialer.inject.ContextModule; +import com.android.dialer.simulator.impl.SimulatorModule; +import com.android.incallui.calllocation.stub.StubCallLocationModule; +import com.android.incallui.maps.stub.StubMapsModule; +import com.android.voicemail.impl.VoicemailModule; +import dagger.Component; +import javax.inject.Singleton; + +/** Root component for the AOSP Dialer application. */ +public interface AospDialerRootComponent extends BaseDialerRootComponent {} diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java new file mode 100644 index 000000000..907671b01 --- /dev/null +++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java @@ -0,0 +1,34 @@ +/* + * 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.binary.basecomponent; + +import com.android.dialer.enrichedcall.EnrichedCallComponent; +import com.android.dialer.simulator.SimulatorComponent; +import com.android.incallui.calllocation.CallLocationComponent; +import com.android.incallui.maps.MapsComponent; +import com.android.voicemail.VoicemailComponent; + +/** + * Base class for the core application-wide {@link Component}. All variants of the Dialer app should + * extend from this component. + */ +public interface BaseDialerRootComponent + extends CallLocationComponent.HasComponent, + EnrichedCallComponent.HasComponent, + MapsComponent.HasComponent, + SimulatorComponent.HasComponent, + VoicemailComponent.HasComponent {} diff --git a/java/com/android/dialer/binary/common/DialerApplication.java b/java/com/android/dialer/binary/common/DialerApplication.java new file mode 100644 index 000000000..c0be4328c --- /dev/null +++ b/java/com/android/dialer/binary/common/DialerApplication.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2013 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.binary.common; + +import android.app.Application; +import android.os.Trace; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import com.android.dialer.blocking.BlockedNumbersAutoMigrator; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; + +/** A common application subclass for all Dialer build variants. */ +public abstract class DialerApplication extends Application { + + private volatile Object rootComponent; + + @Override + public void onCreate() { + Trace.beginSection("DialerApplication.onCreate"); + super.onCreate(); + new BlockedNumbersAutoMigrator( + this, + PreferenceManager.getDefaultSharedPreferences(this), + new FilteredNumberAsyncQueryHandler(this)) + .autoMigrate(); + Trace.endSection(); + } + +} diff --git a/java/com/android/dialer/blocking/FilteredNumbersUtil.java b/java/com/android/dialer/blocking/FilteredNumbersUtil.java index 61ecf1886..f09370e6e 100644 --- a/java/com/android/dialer/blocking/FilteredNumbersUtil.java +++ b/java/com/android/dialer/blocking/FilteredNumbersUtil.java @@ -36,6 +36,8 @@ import com.android.dialer.database.FilteredNumberContract.FilteredNumber; import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; import com.android.dialer.logging.Logger; import com.android.dialer.logging.nano.InteractionEvent; +import com.android.dialer.notification.NotificationChannelManager; +import com.android.dialer.notification.NotificationChannelManager.Channel; import com.android.dialer.util.PermissionsUtil; import java.util.concurrent.TimeUnit; @@ -43,7 +45,8 @@ import java.util.concurrent.TimeUnit; public class FilteredNumbersUtil { public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking"; - public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID = 10; + public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID = + R.id.notification_call_blocking_disabled_by_emergency_call; // Pref key for storing the time of end of the last emergency call in milliseconds after epoch. protected static final String LAST_EMERGENCY_CALL_MS_PREF_KEY = "last_emergency_call_ms"; // Pref key for storing whether a notification has been dispatched to notify the user that call @@ -289,6 +292,7 @@ public class FilteredNumbersUtil { context.getString(R.string.call_blocking_disabled_notification_text)) .setAutoCancel(true); + NotificationChannelManager.applyChannel(builder, context, Channel.MISC, null); builder.setContentIntent( PendingIntent.getActivity( context, diff --git a/java/com/android/dialer/buildtype/BuildType.java b/java/com/android/dialer/buildtype/BuildType.java index c0a54a519..6b6bc2906 100644 --- a/java/com/android/dialer/buildtype/BuildType.java +++ b/java/com/android/dialer/buildtype/BuildType.java @@ -28,7 +28,7 @@ public class BuildType { /** The type of build. */ @Retention(RetentionPolicy.SOURCE) @IntDef({ - BUGFOOD, FISHFOOD, DOGFOOD, RELEASE, + BUGFOOD, FISHFOOD, DOGFOOD, RELEASE, TEST, }) public @interface Type {} @@ -36,6 +36,7 @@ public class BuildType { public static final int FISHFOOD = 2; public static final int DOGFOOD = 3; public static final int RELEASE = 4; + public static final int TEST = 5; private static int cachedBuildType; private static boolean didInitializeBuildType; diff --git a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/release/BuildTypeAccessorImpl.java index e1f2cdc79..70b9f9e37 100644 --- a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java +++ b/java/com/android/dialer/buildtype/release/BuildTypeAccessorImpl.java @@ -25,6 +25,6 @@ public class BuildTypeAccessorImpl implements BuildTypeAccessor { @Override @BuildType.Type public int getBuildType() { - return BuildType.DOGFOOD; + return BuildType.RELEASE; } } diff --git a/java/com/android/dialer/callcomposer/AndroidManifest.xml b/java/com/android/dialer/callcomposer/AndroidManifest.xml index c99f22b90..369db6f4a 100644 --- a/java/com/android/dialer/callcomposer/AndroidManifest.xml +++ b/java/com/android/dialer/callcomposer/AndroidManifest.xml @@ -20,9 +20,8 @@ <application> <activity android:name="com.android.dialer.callcomposer.CallComposerActivity" - android:exported="false" + android:exported="true" android:theme="@style/Theme.AppCompat.CallComposer" - android:windowSoftInputMode="adjustResize" - android:screenOrientation="portrait"/> + android:windowSoftInputMode="adjustPan"/> </application> </manifest> diff --git a/java/com/android/dialer/callcomposer/CallComposerActivity.java b/java/com/android/dialer/callcomposer/CallComposerActivity.java index eef3d210d..f73563ff8 100644 --- a/java/com/android/dialer/callcomposer/CallComposerActivity.java +++ b/java/com/android/dialer/callcomposer/CallComposerActivity.java @@ -21,9 +21,9 @@ import android.animation.Animator.AnimatorListener; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; @@ -35,6 +35,7 @@ import android.support.v4.view.ViewPager.OnPageChangeListener; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; +import android.util.Base64; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLayoutChangeListener; @@ -60,6 +61,7 @@ import com.android.dialer.common.LogUtil; import com.android.dialer.common.UiUtil; import com.android.dialer.compat.CompatUtils; import com.android.dialer.constants.Constants; +import com.android.dialer.enrichedcall.EnrichedCallComponent; import com.android.dialer.enrichedcall.EnrichedCallManager; import com.android.dialer.enrichedcall.EnrichedCallManager.State; import com.android.dialer.enrichedcall.Session; @@ -70,6 +72,7 @@ import com.android.dialer.multimedia.MultimediaData; import com.android.dialer.protos.ProtoParsers; import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.ViewUtil; +import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import java.io.File; /** @@ -88,18 +91,18 @@ public class CallComposerActivity extends AppCompatActivity OnPageChangeListener, CallComposerListener, OnLayoutChangeListener, - AnimatorListener, EnrichedCallManager.StateChangedListener { - private static final int VIEW_PAGER_ANIMATION_DURATION_MILLIS = 300; + public static final String KEY_CONTACT_NAME = "contact_name"; + private static final int ENTRANCE_ANIMATION_DURATION_MILLIS = 500; + private static final int EXIT_ANIMATION_DURATION_MILLIS = 500; private static final String ARG_CALL_COMPOSER_CONTACT = "CALL_COMPOSER_CONTACT"; private static final String ENTRANCE_ANIMATION_KEY = "entrance_animation_key"; private static final String CURRENT_INDEX_KEY = "current_index_key"; private static final String VIEW_PAGER_STATE_KEY = "view_pager_state_key"; - private static final String LOCATIONS_KEY = "locations_key"; private static final String SESSION_ID_KEY = "session_id_key"; private CallComposerContact contact; @@ -111,6 +114,7 @@ public class CallComposerActivity extends AppCompatActivity private RelativeLayout contactContainer; private Toolbar toolbar; private View sendAndCall; + private TextView sendAndCallText; private ImageView cameraIcon; private ImageView galleryIcon; @@ -125,13 +129,8 @@ public class CallComposerActivity extends AppCompatActivity private boolean shouldAnimateEntrance = true; private boolean inFullscreenMode; private boolean isSendAndCallHidingOrHidden = true; - private boolean isAnimatingContactBar; private boolean layoutChanged; private int currentIndex; - private int[] locations; - private int currentLocation; - - @NonNull private EnrichedCallManager enrichedCallManager; public static Intent newIntent(Context context, CallComposerContact contact) { Intent intent = new Intent(context, CallComposerActivity.class); @@ -156,6 +155,7 @@ public class CallComposerActivity extends AppCompatActivity windowContainer = (LinearLayout) findViewById(R.id.call_composer_container); toolbar = (Toolbar) findViewById(R.id.toolbar); sendAndCall = findViewById(R.id.send_and_call_button); + sendAndCallText = (TextView) findViewById(R.id.send_and_call_text); interpolator = new FastOutSlowInInterpolator(); adapter = @@ -166,14 +166,8 @@ public class CallComposerActivity extends AppCompatActivity pager.addOnPageChangeListener(this); setActionBar(toolbar); - toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_white_24); - toolbar.setNavigationOnClickListener( - new OnClickListener() { - @Override - public void onClick(View v) { - finish(); - } - }); + toolbar.setNavigationIcon(R.drawable.quantum_ic_close_white_24); + toolbar.setNavigationOnClickListener(v -> finish()); background.addOnLayoutChangeListener(this); cameraIcon.setOnClickListener(this); @@ -183,20 +177,12 @@ public class CallComposerActivity extends AppCompatActivity onHandleIntent(getIntent()); - enrichedCallManager = EnrichedCallManager.Accessor.getInstance(getApplication()); if (savedInstanceState != null) { shouldAnimateEntrance = savedInstanceState.getBoolean(ENTRANCE_ANIMATION_KEY); - locations = savedInstanceState.getIntArray(LOCATIONS_KEY); pager.onRestoreInstanceState(savedInstanceState.getParcelable(VIEW_PAGER_STATE_KEY)); currentIndex = savedInstanceState.getInt(CURRENT_INDEX_KEY); - sessionId = savedInstanceState.getLong(SESSION_ID_KEY); + sessionId = savedInstanceState.getLong(SESSION_ID_KEY, Session.NO_SESSION_ID); onPageSelected(currentIndex); - } else { - locations = new int[adapter.getCount()]; - for (int i = 0; i < locations.length; i++) { - locations[i] = CallComposerFragment.CONTENT_TOP_UNSET; - } - sessionId = enrichedCallManager.startCallComposerSession(contact.number); } // Since we can't animate the views until they are ready to be drawn, we use this listener to @@ -204,37 +190,39 @@ public class CallComposerActivity extends AppCompatActivity ViewUtil.doOnPreDraw( windowContainer, false, - new Runnable() { - @Override - public void run() { - runEntranceAnimation(); - } + () -> { + showFullscreen(inFullscreenMode); + runEntranceAnimation(); }); setMediaIconSelected(0); - - // This activity is started using startActivityForResult. By default, mark this as succeeded - // and flip this to RESULT_CANCELED if something goes wrong. - setResult(RESULT_OK); - - if (sessionId == Session.NO_SESSION_ID) { - LogUtil.w("CallComposerActivity.onCreate", "failed to create call composer session"); - setResult(RESULT_CANCELED); - finish(); - } } @Override protected void onResume() { super.onResume(); - enrichedCallManager.registerStateChangedListener(this); + getEnrichedCallManager().registerStateChangedListener(this); + if (sessionId == Session.NO_SESSION_ID) { + LogUtil.i("CallComposerActivity.onResume", "creating new session"); + sessionId = getEnrichedCallManager().startCallComposerSession(contact.number); + } else if (getEnrichedCallManager().getSession(sessionId) == null) { + LogUtil.i( + "CallComposerActivity.onResume", "session closed while activity paused, creating new"); + sessionId = getEnrichedCallManager().startCallComposerSession(contact.number); + } else { + LogUtil.i("CallComposerActivity.onResume", "session still open, using old"); + } + if (sessionId == Session.NO_SESSION_ID) { + LogUtil.w("CallComposerActivity.onResume", "failed to create call composer session"); + setFailedResultAndFinish(); + } refreshUiForCallComposerState(); } @Override protected void onPause() { super.onPause(); - enrichedCallManager.unregisterStateChangedListener(this); + getEnrichedCallManager().unregisterStateChangedListener(this); } @Override @@ -243,7 +231,7 @@ public class CallComposerActivity extends AppCompatActivity } private void refreshUiForCallComposerState() { - Session session = enrichedCallManager.getSession(sessionId); + Session session = getEnrichedCallManager().getSession(sessionId); if (session == null) { return; } @@ -256,8 +244,7 @@ public class CallComposerActivity extends AppCompatActivity if (state == EnrichedCallManager.STATE_START_FAILED || state == EnrichedCallManager.STATE_CLOSED) { - setResult(RESULT_CANCELED); - finish(); + setFailedResultAndFinish(); } } @@ -293,7 +280,7 @@ public class CallComposerActivity extends AppCompatActivity if (fragment instanceof MessageComposerFragment) { MessageComposerFragment messageComposerFragment = (MessageComposerFragment) fragment; - builder.setSubject(messageComposerFragment.getMessage()); + builder.setText(messageComposerFragment.getMessage()); placeRCSCall(builder); } if (fragment instanceof GalleryComposerFragment) { @@ -351,7 +338,7 @@ public class CallComposerActivity extends AppCompatActivity } private boolean sessionReady() { - Session session = enrichedCallManager.getSession(sessionId); + Session session = getEnrichedCallManager().getSession(sessionId); if (session == null) { return false; } @@ -362,9 +349,10 @@ public class CallComposerActivity extends AppCompatActivity private void placeRCSCall(MultimediaData.Builder builder) { LogUtil.i("CallComposerActivity.placeRCSCall", "placing enriched call"); Logger.get(this).logImpression(DialerImpression.Type.CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL); - enrichedCallManager.sendCallComposerData(sessionId, builder.build()); + getEnrichedCallManager().sendCallComposerData(sessionId, builder.build()); TelecomUtil.placeCall( this, new CallIntentBuilder(contact.number, CallInitiationType.Type.CALL_COMPOSER).build()); + setResult(RESULT_OK); finish(); } @@ -379,24 +367,26 @@ public class CallComposerActivity extends AppCompatActivity /** Animates {@code contactContainer} to align with content inside viewpager. */ @Override public void onPageSelected(int position) { + if (position == CallComposerPagerAdapter.INDEX_MESSAGE) { + sendAndCallText.setText(R.string.send_and_call); + } else { + sendAndCallText.setText(R.string.share_and_call); + } if (currentIndex == CallComposerPagerAdapter.INDEX_MESSAGE) { UiUtil.hideKeyboardFrom(this, windowContainer); - } else if (position == CallComposerPagerAdapter.INDEX_MESSAGE && inFullscreenMode) { + } else if (position == CallComposerPagerAdapter.INDEX_MESSAGE + && inFullscreenMode + && !isLandscapeLayout()) { UiUtil.openKeyboardFrom(this, windowContainer); } currentIndex = position; CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position); - locations[currentIndex] = fragment.getContentTopPx(); - animateContactContainer(locations[currentIndex]); animateSendAndCall(fragment.shouldHide()); setMediaIconSelected(position); } @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position); - animateContactContainer(fragment.getContentTopPx()); - } + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {} @Override public void onPageScrollStateChanged(int state) {} @@ -407,13 +397,19 @@ public class CallComposerActivity extends AppCompatActivity outState.putParcelable(VIEW_PAGER_STATE_KEY, pager.onSaveInstanceState()); outState.putBoolean(ENTRANCE_ANIMATION_KEY, shouldAnimateEntrance); outState.putInt(CURRENT_INDEX_KEY, currentIndex); - outState.putIntArray(LOCATIONS_KEY, locations); outState.putLong(SESSION_ID_KEY, sessionId); } @Override public void onBackPressed() { - runExitAnimation(); + if (!isSendAndCallHidingOrHidden) { + ((CallComposerFragment) adapter.instantiateItem(pager, currentIndex)).clearComposer(); + } else { + // Unregister first to avoid receiving a callback when the session closes + getEnrichedCallManager().unregisterStateChangedListener(this); + getEnrichedCallManager().endCallComposerSession(sessionId); + runExitAnimation(); + } } @Override @@ -445,29 +441,9 @@ public class CallComposerActivity extends AppCompatActivity } layoutChanged = true; - if (pager.getTop() < 0 || inFullscreenMode) { - ViewGroup.LayoutParams layoutParams = pager.getLayoutParams(); - layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.getHeight(); - pager.setLayoutParams(layoutParams); - } - } - - @Override - public void onAnimationStart(Animator animation) { - isAnimatingContactBar = true; - } - - @Override - public void onAnimationEnd(Animator animation) { - isAnimatingContactBar = false; + showFullscreen(contactContainer.getTop() < 0 || inFullscreenMode); } - @Override - public void onAnimationCancel(Animator animation) {} - - @Override - public void onAnimationRepeat(Animator animation) {} - /** * Reads arguments from the fragment arguments and populates the necessary instance variables. * Copied from {@link com.android.contacts.common.dialog.CallSubjectDialog}. @@ -477,12 +453,26 @@ public class CallComposerActivity extends AppCompatActivity if (arguments == null) { throw new RuntimeException("CallComposerActivity.onHandleIntent, Arguments cannot be null."); } - contact = - ProtoParsers.getFromInstanceState( - arguments, ARG_CALL_COMPOSER_CONTACT, new CallComposerContact()); + if (arguments.get(ARG_CALL_COMPOSER_CONTACT) instanceof String) { + byte[] bytes = Base64.decode(arguments.getString(ARG_CALL_COMPOSER_CONTACT), Base64.DEFAULT); + try { + contact = CallComposerContact.parseFrom(bytes); + } catch (InvalidProtocolBufferNanoException e) { + Assert.fail(e.toString()); + } + } else { + contact = + ProtoParsers.getFromInstanceState( + arguments, ARG_CALL_COMPOSER_CONTACT, new CallComposerContact()); + } updateContactInfo(); } + @Override + public boolean isLandscapeLayout() { + return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + /** * Populates the contact info fields based on the current contact information. Copied from {@link * com.android.contacts.common.dialog.CallSubjectDialog}. @@ -552,25 +542,6 @@ public class CallComposerActivity extends AppCompatActivity } } - private void animateContactContainer(int toY) { - if (toY == CallComposerFragment.CONTENT_TOP_UNSET - || toY == currentLocation - || (toY != locations[currentIndex] - && locations[currentIndex] != CallComposerFragment.CONTENT_TOP_UNSET) - || isAnimatingContactBar - || inFullscreenMode) { - return; - } - currentLocation = toY; - contactContainer - .animate() - .translationY(toY) - .setInterpolator(interpolator) - .setDuration(VIEW_PAGER_ANIMATION_DURATION_MILLIS) - .setListener(this) - .start(); - } - /** Animates compose UI into view */ private void runEntranceAnimation() { if (!shouldAnimateEntrance) { @@ -578,84 +549,90 @@ public class CallComposerActivity extends AppCompatActivity } shouldAnimateEntrance = false; - int colorFrom = ContextCompat.getColor(this, android.R.color.transparent); - int colorTo = ContextCompat.getColor(this, R.color.call_composer_background_color); - ValueAnimator backgroundAnimation = - ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); - backgroundAnimation.setInterpolator(interpolator); - backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds - backgroundAnimation.addUpdateListener( - new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animator) { - background.setBackgroundColor((int) animator.getAnimatedValue()); - } - }); - - ValueAnimator contentAnimation = ValueAnimator.ofFloat(windowContainer.getHeight(), 0); + int value = isLandscapeLayout() ? windowContainer.getWidth() : windowContainer.getHeight(); + ValueAnimator contentAnimation = ValueAnimator.ofFloat(value, 0); contentAnimation.setInterpolator(interpolator); contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); contentAnimation.addUpdateListener( - new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { + animation -> { + if (isLandscapeLayout()) { + windowContainer.setX((Float) animation.getAnimatedValue()); + } else { windowContainer.setY((Float) animation.getAnimatedValue()); } }); - AnimatorSet set = new AnimatorSet(); - set.play(contentAnimation).with(backgroundAnimation); - set.start(); + if (!isLandscapeLayout()) { + int colorFrom = ContextCompat.getColor(this, android.R.color.transparent); + int colorTo = ContextCompat.getColor(this, R.color.call_composer_background_color); + ValueAnimator backgroundAnimation = + ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + backgroundAnimation.setInterpolator(interpolator); + backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds + backgroundAnimation.addUpdateListener( + animator -> background.setBackgroundColor((int) animator.getAnimatedValue())); + + AnimatorSet set = new AnimatorSet(); + set.play(contentAnimation).with(backgroundAnimation); + set.start(); + } else { + contentAnimation.start(); + } } /** Animates compose UI out of view and ends the activity. */ private void runExitAnimation() { - int colorTo = ContextCompat.getColor(this, android.R.color.transparent); - int colorFrom = ContextCompat.getColor(this, R.color.call_composer_background_color); - ValueAnimator backgroundAnimation = - ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); - backgroundAnimation.setInterpolator(interpolator); - backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds - backgroundAnimation.addUpdateListener( - new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animator) { - background.setBackgroundColor((int) animator.getAnimatedValue()); - } - }); - - ValueAnimator contentAnimation = ValueAnimator.ofFloat(0, windowContainer.getHeight()); + int value = isLandscapeLayout() ? windowContainer.getWidth() : windowContainer.getHeight(); + ValueAnimator contentAnimation = ValueAnimator.ofFloat(0, value); contentAnimation.setInterpolator(interpolator); - contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); + contentAnimation.setDuration(EXIT_ANIMATION_DURATION_MILLIS); contentAnimation.addUpdateListener( - new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { + animation -> { + if (isLandscapeLayout()) { + windowContainer.setX((Float) animation.getAnimatedValue()); + } else { windowContainer.setY((Float) animation.getAnimatedValue()); - if (animation.getAnimatedFraction() > .75) { - finish(); - } + } + if (animation.getAnimatedFraction() > .95) { + finish(); } }); - AnimatorSet set = new AnimatorSet(); - set.play(contentAnimation).with(backgroundAnimation); - set.start(); + + if (!isLandscapeLayout()) { + int colorTo = ContextCompat.getColor(this, android.R.color.transparent); + int colorFrom = ContextCompat.getColor(this, R.color.call_composer_background_color); + ValueAnimator backgroundAnimation = + ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + backgroundAnimation.setInterpolator(interpolator); + backgroundAnimation.setDuration(EXIT_ANIMATION_DURATION_MILLIS); + backgroundAnimation.addUpdateListener( + animator -> background.setBackgroundColor((int) animator.getAnimatedValue())); + + AnimatorSet set = new AnimatorSet(); + set.play(contentAnimation).with(backgroundAnimation); + set.start(); + } else { + contentAnimation.start(); + } } @Override - public void showFullscreen(boolean show) { - if (inFullscreenMode == show) { - return; - } - inFullscreenMode = show; - toolbar.setVisibility(show ? View.VISIBLE : View.INVISIBLE); - contactContainer.setVisibility(show ? View.GONE : View.VISIBLE); + public void showFullscreen(boolean fullscreen) { + inFullscreenMode = fullscreen; ViewGroup.LayoutParams layoutParams = pager.getLayoutParams(); - if (show) { + if (isLandscapeLayout()) { + layoutParams.height = background.getHeight() - messageIcon.getHeight(); + toolbar.setVisibility(View.INVISIBLE); + contactContainer.setVisibility(View.GONE); + } else if (fullscreen || getResources().getBoolean(R.bool.show_toolbar)) { layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.getHeight(); + toolbar.setVisibility(View.VISIBLE); + contactContainer.setVisibility(View.GONE); } else { layoutParams.height = getResources().getDimensionPixelSize(R.dimen.call_composer_view_pager_height); + toolbar.setVisibility(View.INVISIBLE); + contactContainer.setVisibility(View.VISIBLE); } pager.setLayoutParams(layoutParams); } @@ -686,35 +663,32 @@ public class CallComposerActivity extends AppCompatActivity ViewUtil.doOnPreDraw( sendAndCall, true, - new Runnable() { - @Override - public void run() { - Animator animator = - ViewAnimationUtils.createCircularReveal( - sendAndCall, centerX, centerY, startRadius, endRadius); - animator.addListener( - new AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - isSendAndCallHidingOrHidden = shouldHide; - sendAndCall.setVisibility(View.VISIBLE); - } - - @Override - public void onAnimationEnd(Animator animation) { - if (isSendAndCallHidingOrHidden) { - sendAndCall.setVisibility(View.INVISIBLE); - } + () -> { + Animator animator = + ViewAnimationUtils.createCircularReveal( + sendAndCall, centerX, centerY, startRadius, endRadius); + animator.addListener( + new AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + isSendAndCallHidingOrHidden = shouldHide; + sendAndCall.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animator animation) { + if (isSendAndCallHidingOrHidden) { + sendAndCall.setVisibility(View.INVISIBLE); } + } - @Override - public void onAnimationCancel(Animator animation) {} + @Override + public void onAnimationCancel(Animator animation) {} - @Override - public void onAnimationRepeat(Animator animation) {} - }); - animator.start(); - } + @Override + public void onAnimationRepeat(Animator animation) {} + }); + animator.start(); }); } } @@ -725,4 +699,14 @@ public class CallComposerActivity extends AppCompatActivity galleryIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_GALLERY ? 1 : alpha); messageIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_MESSAGE ? 1 : alpha); } + + private void setFailedResultAndFinish() { + setResult(RESULT_FIRST_USER, new Intent().putExtra(KEY_CONTACT_NAME, contact.nameOrNumber)); + finish(); + } + + @NonNull + private EnrichedCallManager getEnrichedCallManager() { + return EnrichedCallComponent.get(this).getEnrichedCallManager(); + } } diff --git a/java/com/android/dialer/callcomposer/CallComposerFragment.java b/java/com/android/dialer/callcomposer/CallComposerFragment.java index d6f944955..ee1eb462a 100644 --- a/java/com/android/dialer/callcomposer/CallComposerFragment.java +++ b/java/com/android/dialer/callcomposer/CallComposerFragment.java @@ -17,13 +17,8 @@ package com.android.dialer.callcomposer; import android.content.Context; -import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; import com.android.dialer.common.Assert; import com.android.dialer.common.FragmentUtils; import com.android.dialer.common.LogUtil; @@ -34,26 +29,10 @@ public abstract class CallComposerFragment extends Fragment { protected static final int CAMERA_PERMISSION = 1; protected static final int STORAGE_PERMISSION = 2; - private static final String LOCATION_KEY = "location_key"; - public static final int CONTENT_TOP_UNSET = Integer.MAX_VALUE; - - private View topView; - private int contentTopPx = CONTENT_TOP_UNSET; - private CallComposerListener testListener; - - @Nullable - @Override - public View onCreateView( - LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) { - View view = super.onCreateView(layoutInflater, viewGroup, bundle); - Assert.isNotNull(topView); - return view; - } - @Override public void onAttach(Context context) { super.onAttach(context); - if (!(context instanceof CallComposerListener) && testListener == null) { + if (FragmentUtils.getParent(this, CallComposerListener.class) == null) { LogUtil.e( "CallComposerFragment.onAttach", "Container activity must implement CallComposerListener."); @@ -61,56 +40,15 @@ public abstract class CallComposerFragment extends Fragment { } } - /** Call this method to declare which view is located at the top of the fragment's layout. */ - public void setTopView(View view) { - topView = view; - // For each fragment that extends CallComposerFragment, the heights may vary and since - // ViewPagers cannot have their height set to wrap_content, we have to adjust the top of our - // container to match the top of the fragment. This listener populates {@code contentTopPx} as - // it's available. - topView - .getViewTreeObserver() - .addOnGlobalLayoutListener( - new OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - topView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - contentTopPx = topView.getTop(); - } - }); - } - - public int getContentTopPx() { - return contentTopPx; - } - - public void setParentForTesting(CallComposerListener listener) { - testListener = listener; - } - + @Nullable public CallComposerListener getListener() { - if (testListener != null) { - return testListener; - } - return FragmentUtils.getParentUnsafe(this, CallComposerListener.class); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt(LOCATION_KEY, contentTopPx); - } - - @Override - public void onViewStateRestored(Bundle savedInstanceState) { - super.onViewStateRestored(savedInstanceState); - if (savedInstanceState != null) { - contentTopPx = savedInstanceState.getInt(LOCATION_KEY); - } + return FragmentUtils.getParent(this, CallComposerListener.class); } public abstract boolean shouldHide(); + public abstract void clearComposer(); + /** Interface used to listen to CallComposeFragments */ public interface CallComposerListener { /** Let the listener know when a call is ready to be composed. */ @@ -121,5 +59,8 @@ public abstract class CallComposerFragment extends Fragment { /** True is the listener is in fullscreen. */ boolean isFullscreen(); + + /** True if the layout is in landscape mode. */ + boolean isLandscapeLayout(); } } diff --git a/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java b/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java index 4d4058a0a..edf980ee9 100644 --- a/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java +++ b/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java @@ -18,11 +18,11 @@ package com.android.dialer.callcomposer; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.app.FragmentPagerAdapter; import com.android.dialer.common.Assert; /** ViewPager adapter for call compose UI. */ -public class CallComposerPagerAdapter extends FragmentStatePagerAdapter { +public class CallComposerPagerAdapter extends FragmentPagerAdapter { public static final int INDEX_CAMERA = 0; public static final int INDEX_GALLERY = 1; diff --git a/java/com/android/dialer/callcomposer/CameraComposerFragment.java b/java/com/android/dialer/callcomposer/CameraComposerFragment.java index f2d0a94a7..583fb5446 100644 --- a/java/com/android/dialer/callcomposer/CameraComposerFragment.java +++ b/java/com/android/dialer/callcomposer/CameraComposerFragment.java @@ -56,6 +56,9 @@ import com.android.dialer.util.PermissionsUtil; public class CameraComposerFragment extends CallComposerFragment implements CameraManagerListener, OnClickListener, CameraManager.MediaCallback { + private static final String CAMERA_DIRECTION_KEY = "camera_direction"; + private static final String CAMERA_URI_KEY = "camera_key"; + private View permissionView; private ImageButton exitFullscreen; private ImageButton fullscreen; @@ -68,11 +71,13 @@ public class CameraComposerFragment extends CallComposerFragment private View allowPermission; private CameraPreviewHost preview; private ProgressBar loading; + private ImageView previewImageView; private Uri cameraUri; private boolean processingUri; private String[] permissions = new String[] {Manifest.permission.CAMERA}; private CameraUriCallback uriCallback; + private int cameraDirection = CameraInfo.CAMERA_FACING_BACK; public static CameraComposerFragment newInstance() { return new CameraComposerFragment(); @@ -94,6 +99,7 @@ public class CameraComposerFragment extends CallComposerFragment cancel = (ImageButton) cameraView.findViewById(R.id.camera_cancel_button); focus = (RenderOverlay) cameraView.findViewById(R.id.focus_visual); preview = (CameraPreviewHost) cameraView.findViewById(R.id.camera_preview); + previewImageView = (ImageView) root.findViewById(R.id.preview_image_view); exitFullscreen.setOnClickListener(this); fullscreen.setOnClickListener(this); @@ -115,10 +121,12 @@ public class CameraComposerFragment extends CallComposerFragment ContextCompat.getColor(getContext(), R.color.dialer_theme_color)); permissionView.setVisibility(View.VISIBLE); } else { + if (bundle != null) { + cameraDirection = bundle.getInt(CAMERA_DIRECTION_KEY); + cameraUri = bundle.getParcelable(CAMERA_URI_KEY); + } setupCamera(); } - - setTopView(cameraView); return root; } @@ -126,8 +134,8 @@ public class CameraComposerFragment extends CallComposerFragment CameraManager.get().setListener(this); preview.setShown(); CameraManager.get().setRenderOverlay(focus); - CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK); - setCameraUri(null); + CameraManager.get().selectCamera(cameraDirection); + setCameraUri(cameraUri); } @Override @@ -146,10 +154,16 @@ public class CameraComposerFragment extends CallComposerFragment } @Override + public void clearComposer() { + processingUri = false; + setCameraUri(null); + } + + @Override public void onClick(View view) { if (view == capture) { float heightPercent = 1; - if (!getListener().isFullscreen()) { + if (!getListener().isFullscreen() && !getListener().isLandscapeLayout()) { heightPercent = Math.min((float) cameraView.getHeight() / preview.getView().getHeight(), 1); } @@ -162,8 +176,7 @@ public class CameraComposerFragment extends CallComposerFragment ((Animatable) swapCamera.getDrawable()).start(); CameraManager.get().swapCamera(); } else if (view == cancel) { - processingUri = false; - setCameraUri(null); + clearComposer(); } else if (view == exitFullscreen) { getListener().showFullscreen(false); fullscreen.setVisibility(View.VISIBLE); @@ -314,6 +327,13 @@ public class CameraComposerFragment extends CallComposerFragment boolean isCameraAvailable = CameraManager.get().isCameraAvailable(); boolean uriReadyOrProcessing = cameraUri != null || processingUri; + if (cameraUri != null) { + previewImageView.setImageURI(cameraUri); + previewImageView.setVisibility(View.VISIBLE); + } else { + previewImageView.setVisibility(View.GONE); + } + if (cameraUri == null && isCameraAvailable) { CameraManager.get().resetPreview(); cancel.setVisibility(View.GONE); @@ -328,7 +348,7 @@ public class CameraComposerFragment extends CallComposerFragment capture.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE); cancel.setVisibility(uriReadyOrProcessing ? View.VISIBLE : View.GONE); - if (uriReadyOrProcessing) { + if (uriReadyOrProcessing || getListener().isLandscapeLayout()) { fullscreen.setVisibility(View.GONE); exitFullscreen.setVisibility(View.GONE); } else if (getListener().isFullscreen()) { @@ -344,6 +364,13 @@ public class CameraComposerFragment extends CallComposerFragment } @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(CAMERA_DIRECTION_KEY, CameraManager.get().getCameraInfo().facing); + outState.putParcelable(CAMERA_URI_KEY, cameraUri); + } + + @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) { diff --git a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java index 623127945..b53d6a9d6 100644 --- a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java +++ b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java @@ -24,6 +24,7 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.os.Parcelable; import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -45,11 +46,17 @@ import com.android.dialer.logging.Logger; import com.android.dialer.logging.nano.DialerImpression; import com.android.dialer.util.PermissionsUtil; import java.io.File; +import java.util.ArrayList; +import java.util.List; /** Fragment used to compose call with image from the user's gallery. */ public class GalleryComposerFragment extends CallComposerFragment implements LoaderCallbacks<Cursor>, OnClickListener { + private static final String SELECTED_DATA_KEY = "selected_data"; + private static final String IS_COPY_KEY = "is_copy"; + private static final String INSERTED_IMAGES_KEY = "inserted_images"; + private static final int RESULT_LOAD_IMAGE = 1; private static final int RESULT_OPEN_SETTINGS = 2; @@ -62,6 +69,7 @@ public class GalleryComposerFragment extends CallComposerFragment private CursorLoader cursorLoader; private GalleryGridItemData selectedData = null; private boolean selectedDataIsCopy; + private List<GalleryGridItemData> insertedImages = new ArrayList<>(); public static GalleryComposerFragment newInstance() { return new GalleryComposerFragment(); @@ -89,10 +97,13 @@ public class GalleryComposerFragment extends CallComposerFragment ContextCompat.getColor(getContext(), R.color.dialer_theme_color)); permissionView.setVisibility(View.VISIBLE); } else { + if (bundle != null) { + selectedData = bundle.getParcelable(SELECTED_DATA_KEY); + selectedDataIsCopy = bundle.getBoolean(IS_COPY_KEY); + insertedImages = bundle.getParcelableArrayList(INSERTED_IMAGES_KEY); + } setupGallery(); } - - setTopView(galleryGridView); return view; } @@ -110,6 +121,10 @@ public class GalleryComposerFragment extends CallComposerFragment @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { adapter.swapCursor(cursor); + if (insertedImages != null && !insertedImages.isEmpty()) { + adapter.insertEntries(insertedImages); + } + setSelected(selectedData, selectedDataIsCopy); } @Override @@ -147,7 +162,7 @@ public class GalleryComposerFragment extends CallComposerFragment intent.addCategory(Intent.CATEGORY_OPENABLE); startActivityForResult(intent, RESULT_LOAD_IMAGE); } else if (itemView.getData().equals(selectedData)) { - setSelected(null, false); + clearComposer(); } else { setSelected(new GalleryGridItemData(itemView.getData()), false); } @@ -179,7 +194,10 @@ public class GalleryComposerFragment extends CallComposerFragment selectedData = data; selectedDataIsCopy = isCopy; adapter.setSelected(selectedData); - getListener().composeCall(this); + CallComposerListener listener = getListener(); + if (listener != null) { + getListener().composeCall(this); + } } @Override @@ -190,6 +208,20 @@ public class GalleryComposerFragment extends CallComposerFragment } @Override + public void clearComposer() { + setSelected(null, false); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(SELECTED_DATA_KEY, selectedData); + outState.putBoolean(IS_COPY_KEY, selectedDataIsCopy); + outState.putParcelableArrayList( + INSERTED_IMAGES_KEY, (ArrayList<? extends Parcelable>) insertedImages); + } + + @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) { @@ -238,7 +270,9 @@ public class GalleryComposerFragment extends CallComposerFragment new Callback() { @Override public void onCopySuccessful(File file, String mimeType) { - setSelected(adapter.insertEntry(file.getAbsolutePath(), mimeType), true); + GalleryGridItemData data = adapter.insertEntry(file.getAbsolutePath(), mimeType); + insertedImages.add(0, data); + setSelected(data, true); } @Override diff --git a/java/com/android/dialer/callcomposer/GalleryGridAdapter.java b/java/com/android/dialer/callcomposer/GalleryGridAdapter.java index 0a7fd769b..84257b2af 100644 --- a/java/com/android/dialer/callcomposer/GalleryGridAdapter.java +++ b/java/com/android/dialer/callcomposer/GalleryGridAdapter.java @@ -104,6 +104,18 @@ public class GalleryGridAdapter extends CursorAdapter { } } + public void insertEntries(@NonNull List<GalleryGridItemData> entries) { + Assert.checkArgument(entries.size() != 0); + LogUtil.i("GalleryGridAdapter.insertRows", "inserting %d rows", entries.size()); + MatrixCursor extraRow = new MatrixCursor(GalleryGridItemData.IMAGE_PROJECTION); + for (GalleryGridItemData entry : entries) { + extraRow.addRow(new Object[] {0L, entry.getFilePath(), entry.getMimeType(), ""}); + } + extraRow.moveToFirst(); + Cursor extendedCursor = new MergeCursor(new Cursor[] {extraRow, getCursor()}); + swapCursor(extendedCursor); + } + public GalleryGridItemData insertEntry(String filePath, String mimeType) { LogUtil.i("GalleryGridAdapter.insertRow", mimeType + " " + filePath); diff --git a/java/com/android/dialer/callcomposer/GalleryGridItemData.java b/java/com/android/dialer/callcomposer/GalleryGridItemData.java index 402c6ce6d..43db96dd5 100644 --- a/java/com/android/dialer/callcomposer/GalleryGridItemData.java +++ b/java/com/android/dialer/callcomposer/GalleryGridItemData.java @@ -18,6 +18,8 @@ package com.android.dialer.callcomposer; import android.database.Cursor; import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; import android.provider.MediaStore.Images.Media; import android.support.annotation.Nullable; import android.text.TextUtils; @@ -26,7 +28,7 @@ import java.io.File; import java.util.Objects; /** Provides data for GalleryGridItemView */ -public final class GalleryGridItemData { +public final class GalleryGridItemData implements Parcelable { public static final String[] IMAGE_PROJECTION = new String[] {Media._ID, Media.DATA, Media.MIME_TYPE, Media.DATE_MODIFIED}; @@ -88,4 +90,35 @@ public final class GalleryGridItemData { public int hashCode() { return Objects.hash(filePath, mimeType, dateModifiedSeconds); } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(filePath); + dest.writeString(mimeType); + dest.writeLong(dateModifiedSeconds); + } + + public static final Creator<GalleryGridItemData> CREATOR = + new Creator<GalleryGridItemData>() { + @Override + public GalleryGridItemData createFromParcel(Parcel in) { + return new GalleryGridItemData(in); + } + + @Override + public GalleryGridItemData[] newArray(int size) { + return new GalleryGridItemData[size]; + } + }; + + private GalleryGridItemData(Parcel in) { + filePath = in.readString(); + mimeType = in.readString(); + dateModifiedSeconds = in.readLong(); + } } diff --git a/java/com/android/dialer/callcomposer/MessageComposerFragment.java b/java/com/android/dialer/callcomposer/MessageComposerFragment.java index 521b71402..d8100033f 100644 --- a/java/com/android/dialer/callcomposer/MessageComposerFragment.java +++ b/java/com/android/dialer/callcomposer/MessageComposerFragment.java @@ -77,6 +77,7 @@ public class MessageComposerFragment extends CallComposerFragment customMessage.addTextChangedListener( new TextWatcher() { @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} @Override @@ -90,8 +91,6 @@ public class MessageComposerFragment extends CallComposerFragment } view.findViewById(R.id.message_chat).setOnClickListener(this); view.findViewById(R.id.message_question).setOnClickListener(this); - - setTopView(urgent); return view; } @@ -140,4 +139,9 @@ public class MessageComposerFragment extends CallComposerFragment public boolean shouldHide() { return TextUtils.isEmpty(getMessage()); } + + @Override + public void clearComposer() { + customMessage.getText().clear(); + } } diff --git a/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java index 150009495..a23014bf0 100644 --- a/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java +++ b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java @@ -74,7 +74,9 @@ public class ImagePersistTask extends FallibleAsyncTask<Void, Void, Uri> { if (mHeightPercent != 1.0f) { writeClippedBitmap(outputStream); } else { - outputStream.write(mBytes, 0, mBytes.length); + Bitmap bitmap = BitmapFactory.decodeByteArray(mBytes, 0, mBytes.length); + bitmap = CopyAndResizeImageTask.resizeForEnrichedCalling(bitmap); + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream); } } diff --git a/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml b/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml index 75401b14b..a4198fcf9 100644 --- a/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml +++ b/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml @@ -46,6 +46,14 @@ android:background="@android:color/white" android:visibility="gone" /> + <ImageView + android:id="@+id/preview_image_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop" + android:background="#000000" + android:visibility="gone"/> + <!-- Need a background on this view in order for the ripple effect to have a place to draw --> <FrameLayout android:id="@+id/camera_button_container" diff --git a/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml index 518b53ffd..f687f0b5c 100644 --- a/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml +++ b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml @@ -120,13 +120,14 @@ android:visibility="invisible" android:background="@color/compose_and_call_background"> <TextView + android:id="@+id/send_and_call_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:drawableStart="@drawable/quantum_ic_call_white_18" android:drawablePadding="@dimen/send_and_call_drawable_padding" android:textAllCaps="true" - android:text="@string/send_and_call" + android:text="@string/share_and_call" android:textSize="@dimen/send_and_call_text_size" android:fontFamily="sans-serif-medium" android:textColor="@color/background_dialer_white"/> @@ -140,8 +141,8 @@ android:layout_height="wrap_content" android:minHeight="?attr/actionBarSize" android:visibility="invisible" - android:titleTextAppearance="@style/call_composer_toolbar_title_text" - android:subtitleTextAppearance="@style/call_composer_toolbar_subtitle_text" + android:titleTextAppearance="@style/toolbar_title_text" + android:subtitleTextAppearance="@style/toolbar_subtitle_text" android:navigationIcon="@drawable/quantum_ic_close_white_24" android:background="@color/dialer_theme_color"/> </FrameLayout>
\ No newline at end of file diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml index 58893ba50..a4bd4df03 100644 --- a/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml +++ b/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml @@ -27,7 +27,7 @@ android:paddingLeft="@dimen/gallery_item_padding" android:paddingRight="@dimen/gallery_item_padding" android:paddingTop="@dimen/gallery_item_padding" - android:numColumns="3"/> + android:numColumns="@integer/gallery_composer_grid_view_rows"/> <include android:id="@+id/permission_view" diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml index 97f232b3a..577887be9 100644 --- a/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml +++ b/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml @@ -15,43 +15,45 @@ ~ limitations under the License --> <LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="@dimen/call_composer_view_pager_height" - android:orientation="vertical" - android:gravity="bottom" - android:background="@color/background_dialer_white"> + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="@dimen/call_composer_view_pager_height" + android:orientation="vertical" + android:gravity="bottom" + android:background="@color/background_dialer_white"> <TextView - android:id="@+id/message_urgent" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/urgent" - style="@style/message_composer_textview"/> + android:id="@+id/message_urgent" + android:layout_width="match_parent" + android:layout_height="56dp" + android:layout_marginTop="8dp" + android:text="@string/urgent" + style="@style/message_composer_textview"/> <TextView - android:id="@+id/message_chat" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/want_to_chat" - style="@style/message_composer_textview"/> + android:id="@+id/message_chat" + android:layout_width="match_parent" + android:layout_height="56dp" + android:text="@string/want_to_chat" + style="@style/message_composer_textview"/> <TextView - android:id="@+id/message_question" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/quick_question" - style="@style/message_composer_textview"/> + android:id="@+id/message_question" + android:layout_width="match_parent" + android:layout_height="56dp" + android:layout_marginBottom="8dp" + android:text="@string/quick_question" + style="@style/message_composer_textview"/> <View - android:layout_width="match_parent" - android:layout_height="@dimen/message_composer_divider_height" - android:background="@color/call_composer_divider"/> + android:layout_width="match_parent" + android:layout_height="@dimen/message_composer_divider_height" + android:background="@color/call_composer_divider"/> <RelativeLayout - android:orientation="horizontal" - android:layout_width="wrap_content" - android:layout_height="wrap_content"> + android:orientation="horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> <EditText android:id="@+id/custom_message" @@ -59,21 +61,22 @@ android:layout_height="wrap_content" android:padding="@dimen/message_composer_item_padding" android:textSize="@dimen/message_compose_item_text_size" - android:hint="@string/custom_message_hint" + android:hint="@string/message_composer_custom_message_hint" android:textColor="@color/dialer_primary_text_color" android:textColorHint="@color/dialer_edit_text_hint_color" android:background="@color/background_dialer_white" android:textCursorDrawable="@drawable/searchedittext_custom_cursor" - android:layout_toLeftOf="@+id/remaining_characters"/> + android:layout_toStartOf="@+id/remaining_characters" + android:imeOptions="flagNoExtractUi"/> <TextView - android:id="@+id/remaining_characters" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginRight="@dimen/message_composer_item_padding" - android:layout_alignParentRight="true" - android:layout_centerVertical="true" - android:textSize="@dimen/message_compose_remaining_char_text_size" - android:textColor="@color/dialer_edit_text_hint_color"/> + android:id="@+id/remaining_characters" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/message_composer_item_padding" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:textSize="@dimen/message_compose_remaining_char_text_size" + android:textColor="@color/dialer_edit_text_hint_color"/> </RelativeLayout> </LinearLayout>
\ No newline at end of file diff --git a/java/com/android/dialer/callcomposer/res/values-h260dp/values.xml b/java/com/android/dialer/callcomposer/res/values-h260dp/values.xml new file mode 100644 index 000000000..c31f3b015 --- /dev/null +++ b/java/com/android/dialer/callcomposer/res/values-h260dp/values.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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> + <bool name="show_toolbar">true</bool> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/callcomposer/res/values-h480dp/values.xml b/java/com/android/dialer/callcomposer/res/values-h480dp/values.xml new file mode 100644 index 000000000..77b77a553 --- /dev/null +++ b/java/com/android/dialer/callcomposer/res/values-h480dp/values.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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> + <bool name="show_toolbar">false</bool> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/callcomposer/res/values-w360dp/values.xml b/java/com/android/dialer/callcomposer/res/values-w360dp/values.xml new file mode 100644 index 000000000..adff63518 --- /dev/null +++ b/java/com/android/dialer/callcomposer/res/values-w360dp/values.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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> + <integer name="gallery_composer_grid_view_rows">3</integer> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/callcomposer/res/values-w500dp/values.xml b/java/com/android/dialer/callcomposer/res/values-w500dp/values.xml new file mode 100644 index 000000000..3ec2b3513 --- /dev/null +++ b/java/com/android/dialer/callcomposer/res/values-w500dp/values.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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> + <integer name="gallery_composer_grid_view_rows">4</integer> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/callcomposer/res/values/dimens.xml b/java/com/android/dialer/callcomposer/res/values/dimens.xml index 3ebda7a0f..5571170b2 100644 --- a/java/com/android/dialer/callcomposer/res/values/dimens.xml +++ b/java/com/android/dialer/callcomposer/res/values/dimens.xml @@ -17,10 +17,6 @@ <resources> <dimen name="call_composer_view_pager_height">258dp</dimen> - <!-- Toolbar --> - <dimen name="toolbar_title_text_size">16sp</dimen> - <dimen name="toolbar_subtitle_text_size">14sp</dimen> - <!-- Contact bar --> <dimen name="call_composer_contact_photo_border_thickness">2dp</dimen> <dimen name="call_composer_contact_photo_size">116dp</dimen> diff --git a/java/com/android/dialer/callcomposer/res/values/strings.xml b/java/com/android/dialer/callcomposer/res/values/strings.xml index 35a8cf9da..cc7762b64 100644 --- a/java/com/android/dialer/callcomposer/res/values/strings.xml +++ b/java/com/android/dialer/callcomposer/res/values/strings.xml @@ -22,9 +22,11 @@ <!-- A default message to send with a phone call. [CHAR LIMIT=27] --> <string name="quick_question">Quick question…</string> <!-- Hint in a text field to compose a custom message to send with a phone call [CHAR LIMIT=27] --> - <string name="custom_message_hint">Write a custom message</string> - <!-- Text for a button to make a phone call combined with a picture or text message [CHAR LIMIT=26] --> + <string name="message_composer_custom_message_hint">Write a custom message</string> + <!-- Text for a button to make a phone call combined with a text message [CHAR LIMIT=26] --> <string name="send_and_call">Send and call</string> + <!-- Text for a button to make a phone call combined with a picture or other media [CHAR LIMIT=26] --> + <string name="share_and_call">Share and call</string> <!-- Accessibility description for each image in the gallery. For example, "image January 17 2015 1 59 pm". --> <string name="gallery_item_description">image <xliff:g id="date">%1$tB %1$te %1$tY %1$tl %1$tM %1$tp</xliff:g></string> <!-- Accessibility description for each image in the gallery when no date is present. --> diff --git a/java/com/android/dialer/callcomposer/res/values/styles.xml b/java/com/android/dialer/callcomposer/res/values/styles.xml index 891f6397d..29ac4ddaa 100644 --- a/java/com/android/dialer/callcomposer/res/values/styles.xml +++ b/java/com/android/dialer/callcomposer/res/values/styles.xml @@ -36,15 +36,6 @@ <item name="android:textColor">@color/dialer_primary_text_color</item> <item name="android:padding">@dimen/message_composer_item_padding</item> <item name="android:background">@drawable/item_background_material_light</item> - </style> - - <style name="call_composer_toolbar_title_text"> - <item name="android:textSize">@dimen/toolbar_title_text_size</item> - <item name="android:textColor">@color/background_dialer_white</item> - </style> - - <style name="call_composer_toolbar_subtitle_text"> - <item name="android:textSize">@dimen/toolbar_subtitle_text_size</item> - <item name="android:textColor">@color/background_dialer_white</item> + <item name="android:gravity">center_vertical</item> </style> </resources>
\ No newline at end of file diff --git a/java/com/android/dialer/callcomposer/res/values/values.xml b/java/com/android/dialer/callcomposer/res/values/values.xml new file mode 100644 index 000000000..39b8e4071 --- /dev/null +++ b/java/com/android/dialer/callcomposer/res/values/values.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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> + <integer name="gallery_composer_grid_view_rows">2</integer> + <bool name="show_toolbar">false</bool> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/calldetails/AndroidManifest.xml b/java/com/android/dialer/calldetails/AndroidManifest.xml new file mode 100644 index 000000000..b71207ba2 --- /dev/null +++ b/java/com/android/dialer/calldetails/AndroidManifest.xml @@ -0,0 +1,31 @@ +<!-- + ~ 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.calldetails"> + <application> + <activity + android:label="@string/call_details" + android:name="com.android.dialer.calldetails.CallDetailsActivity" + android:theme="@style/Theme.AppCompat.NoActionBar"> + <intent-filter> + <action android:name="android.intent.action.VIEW"/> + <category android:name="android.intent.category.DEFAULT"/> + <data android:mimeType="vnd.android.cursor.item/calls"/> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java new file mode 100644 index 000000000..6070640a0 --- /dev/null +++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java @@ -0,0 +1,130 @@ +/* + * 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.calldetails; + +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.support.annotation.NonNull; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.MenuItem; +import android.widget.Toolbar; +import com.android.dialer.callcomposer.nano.CallComposerContact; +import com.android.dialer.calldetails.nano.CallDetailsEntries; +import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry; +import com.android.dialer.common.Assert; +import com.android.dialer.common.AsyncTaskExecutors; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.protos.ProtoParsers; + +/** Displays the details of a specific call log entry. */ +public class CallDetailsActivity extends AppCompatActivity { + + private static final String EXTRA_CALL_DETAILS_ENTRIES = "call_details_entries"; + private static final String EXTRA_CONTACT = "contact"; + private static final String TASK_DELETE = "task_delete"; + + private CallDetailsEntry[] entries; + + public static Intent newInstance( + Context context, @NonNull CallDetailsEntries details, @NonNull CallComposerContact contact) { + Assert.isNotNull(details); + Assert.isNotNull(contact); + + Intent intent = new Intent(context, CallDetailsActivity.class); + ProtoParsers.put(intent, EXTRA_CONTACT, contact); + ProtoParsers.put(intent, EXTRA_CALL_DETAILS_ENTRIES, details); + return intent; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.call_details_activity); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setActionBar(toolbar); + toolbar.inflateMenu(R.menu.call_details_menu); + toolbar.setNavigationOnClickListener(v -> finish()); + onHandleIntent(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + onHandleIntent(intent); + } + + private void onHandleIntent(Intent intent) { + Bundle arguments = intent.getExtras(); + CallComposerContact contact = + ProtoParsers.getFromInstanceState(arguments, EXTRA_CONTACT, new CallComposerContact()); + entries = + ProtoParsers.getFromInstanceState( + arguments, EXTRA_CALL_DETAILS_ENTRIES, new CallDetailsEntries()) + .entries; + RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setAdapter(new CallDetailsAdapter(this, contact, entries)); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.call_detail_delete_menu_item) { + Logger.get(this).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM); + AsyncTaskExecutors.createAsyncTaskExecutor().submit(TASK_DELETE, new DeleteCallsTask()); + item.setEnabled(false); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** Delete specified calls from the call log. */ + private class DeleteCallsTask extends AsyncTask<Void, Void, Void> { + + private final String callIds; + + DeleteCallsTask() { + StringBuilder callIds = new StringBuilder(); + for (CallDetailsEntry entry : entries) { + if (callIds.length() != 0) { + callIds.append(","); + } + callIds.append(entry.callId); + } + this.callIds = callIds.toString(); + } + + @Override + protected Void doInBackground(Void... params) { + getContentResolver() + .delete(Calls.CONTENT_URI, CallLog.Calls._ID + " IN (" + callIds + ")", null); + return null; + } + + @Override + public void onPostExecute(Void result) { + finish(); + } + } +} diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java new file mode 100644 index 000000000..954583077 --- /dev/null +++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java @@ -0,0 +1,97 @@ +/* + * 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.calldetails; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import com.android.dialer.callcomposer.nano.CallComposerContact; +import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry; +import com.android.dialer.calllogutils.CallTypeHelper; +import com.android.dialer.common.Assert; + +/** Adapter for RecyclerView in {@link CallDetailsActivity}. */ +public class CallDetailsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { + + private static final int HEADER_VIEW_TYPE = 1; + private static final int CALL_ENTRY_VIEW_TYPE = 2; + private static final int FOOTER_VIEW_TYPE = 3; + + private final CallComposerContact contact; + private final CallDetailsEntry[] callDetailsEntries; + private final CallTypeHelper callTypeHelper; + + public CallDetailsAdapter( + Context context, CallComposerContact contact, CallDetailsEntry[] callDetailsEntries) { + this.contact = Assert.isNotNull(contact); + this.callDetailsEntries = Assert.isNotNull(callDetailsEntries); + callTypeHelper = new CallTypeHelper(context.getResources()); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + switch (viewType) { + case HEADER_VIEW_TYPE: + return new CallDetailsHeaderViewHolder( + inflater.inflate(R.layout.contact_container, parent, false)); + case CALL_ENTRY_VIEW_TYPE: + return new CallDetailsEntryViewHolder( + inflater.inflate(R.layout.call_details_entry, parent, false)); + case FOOTER_VIEW_TYPE: + return new CallDetailsFooterViewHolder( + inflater.inflate(R.layout.call_details_footer, parent, false)); + default: + Assert.fail("No ViewHolder available for viewType: " + viewType); + return null; + } + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + if (position == 0) { // Header + ((CallDetailsHeaderViewHolder) holder).updateContactInfo(contact); + } else if (position == getItemCount() - 1) { + ((CallDetailsFooterViewHolder) holder).setPhoneNumber(contact.number); + } else { + CallDetailsEntryViewHolder viewHolder = (CallDetailsEntryViewHolder) holder; + viewHolder.setCallDetails( + contact.number, + callDetailsEntries[position - 1], + callTypeHelper, + position != getItemCount() - 2); + } + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { // Header + return HEADER_VIEW_TYPE; + } else if (position == getItemCount() - 1) { + return FOOTER_VIEW_TYPE; + } else { + return CALL_ENTRY_VIEW_TYPE; + } + } + + @Override + public int getItemCount() { + return callDetailsEntries.length + 2; // Header + footer + } +} diff --git a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java new file mode 100644 index 000000000..b1a70af0c --- /dev/null +++ b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java @@ -0,0 +1,195 @@ +/* + * 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.calldetails; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.provider.CallLog.Calls; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry; +import com.android.dialer.calllogutils.CallEntryFormatter; +import com.android.dialer.calllogutils.CallTypeHelper; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.AppCompatConstants; +import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult; +import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; + +/** ViewHolder for call entries in {@link CallDetailsActivity}. */ +public class CallDetailsEntryViewHolder extends ViewHolder { + + private final ImageView callTypeIcon; + private final TextView callTypeText; + private final TextView callTime; + private final TextView callDuration; + + private final View multimediaImageContainer; + private final View multimediaDetailsContainer; + private final View multimediaDivider; + + private final TextView multimediaDetails; + + private final ImageView multimediaImage; + + // TODO: Display this when location is stored - b/36160042 + @SuppressWarnings("unused") + private final TextView multimediaAttachmentsNumber; + + private final Context context; + + public CallDetailsEntryViewHolder(View container) { + super(container); + context = container.getContext(); + + callTypeIcon = (ImageView) container.findViewById(R.id.call_direction); + callTypeText = (TextView) container.findViewById(R.id.call_type); + callTime = (TextView) container.findViewById(R.id.call_time); + callDuration = (TextView) container.findViewById(R.id.call_duration); + + multimediaImageContainer = container.findViewById(R.id.multimedia_image_container); + multimediaDetailsContainer = container.findViewById(R.id.ec_container); + multimediaDivider = container.findViewById(R.id.divider); + multimediaDetails = (TextView) container.findViewById(R.id.multimedia_details); + multimediaImage = (ImageView) container.findViewById(R.id.multimedia_image); + multimediaAttachmentsNumber = + (TextView) container.findViewById(R.id.multimedia_attachments_number); + } + + void setCallDetails( + String number, + CallDetailsEntry entry, + CallTypeHelper callTypeHelper, + boolean showMultimediaDivider) { + int callType = entry.callType; + boolean isVideoCall = + (entry.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO + && CallUtil.isVideoEnabled(context); + boolean isPulledCall = + (entry.features & Calls.FEATURES_PULLED_EXTERNALLY) == Calls.FEATURES_PULLED_EXTERNALLY; + + Drawable callIcon = getIconForCallType(context.getResources(), callType); + int color = getColorForCallType(context, callType); + callIcon.setColorFilter(color, PorterDuff.Mode.MULTIPLY); + callTime.setTextColor(color); + callTypeIcon.setImageDrawable(callIcon); + + callTypeText.setText(callTypeHelper.getCallTypeText(callType, isVideoCall, isPulledCall)); + callTime.setText(CallEntryFormatter.formatDate(context, entry.date)); + if (CallTypeHelper.isMissedCallType(callType)) { + callDuration.setVisibility(View.GONE); + } else { + callDuration.setVisibility(View.VISIBLE); + callDuration.setText( + CallEntryFormatter.formatDurationAndDataUsage(context, entry.duration, entry.dataUsage)); + } + setMultimediaDetails(number, entry, showMultimediaDivider); + } + + private void setMultimediaDetails(String number, CallDetailsEntry entry, boolean showDivider) { + multimediaDivider.setVisibility(showDivider ? View.VISIBLE : View.GONE); + if (entry.historyResults == null || entry.historyResults.length <= 0) { + LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "no data, hiding UI"); + multimediaDetailsContainer.setVisibility(View.GONE); + } else { + + // TODO: b/36158891 Add room for 2 pieces of enriched call data. It's possible + // to have both call composer data and post call data for a single call. + HistoryResult historyResult = entry.historyResults[0]; + multimediaDetailsContainer.setVisibility(View.VISIBLE); + multimediaDetailsContainer.setOnClickListener( + (v) -> { + DialerUtils.startActivityWithErrorToast(context, IntentUtil.getSendSmsIntent(number)); + }); + multimediaImageContainer.setClipToOutline(true); + + if (!TextUtils.isEmpty(historyResult.imageUri)) { + LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "setting image"); + multimediaImageContainer.setVisibility(View.VISIBLE); + multimediaImage.setImageURI(Uri.parse(historyResult.imageUri)); + multimediaDetails.setText( + isIncoming(historyResult) ? R.string.received_a_photo : R.string.sent_a_photo); + } else { + LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "no image"); + } + + // Set text after image to overwrite the received/sent a photo text + if (!TextUtils.isEmpty(historyResult.text)) { + LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "showing text"); + multimediaDetails.setText( + context.getString(R.string.message_in_quotes, historyResult.text)); + } else { + LogUtil.i("CallDetailsEntryViewHolder.setMultimediaDetails", "no text"); + } + } + } + + private static boolean isIncoming(@NonNull HistoryResult historyResult) { + return historyResult.type == Type.INCOMING_POST_CALL + || historyResult.type == Type.INCOMING_CALL_COMPOSER; + } + + private static Drawable getIconForCallType(Resources resources, int callType) { + switch (callType) { + case AppCompatConstants.CALLS_OUTGOING_TYPE: + return resources.getDrawable(R.drawable.quantum_ic_call_made_white_24); + case AppCompatConstants.CALLS_BLOCKED_TYPE: + return resources.getDrawable(R.drawable.quantum_ic_block_white_24); + case AppCompatConstants.CALLS_INCOMING_TYPE: + case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE: + case AppCompatConstants.CALLS_REJECTED_TYPE: + return resources.getDrawable(R.drawable.quantum_ic_call_received_white_24); + case AppCompatConstants.CALLS_MISSED_TYPE: + default: + // It is possible for users to end up with calls with unknown call types in their + // call history, possibly due to 3rd party call log implementations (e.g. to + // distinguish between rejected and missed calls). Instead of crashing, just + // assume that all unknown call types are missed calls. + return resources.getDrawable(R.drawable.quantum_ic_call_missed_white_24); + } + } + + private static @ColorInt int getColorForCallType(Context context, int callType) { + switch (callType) { + case AppCompatConstants.CALLS_OUTGOING_TYPE: + case AppCompatConstants.CALLS_VOICEMAIL_TYPE: + case AppCompatConstants.CALLS_BLOCKED_TYPE: + case AppCompatConstants.CALLS_INCOMING_TYPE: + case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE: + case AppCompatConstants.CALLS_REJECTED_TYPE: + return ContextCompat.getColor(context, R.color.dialer_secondary_text_color); + case AppCompatConstants.CALLS_MISSED_TYPE: + default: + // It is possible for users to end up with calls with unknown call types in their + // call history, possibly due to 3rd party call log implementations (e.g. to + // distinguish between rejected and missed calls). Instead of crashing, just + // assume that all unknown call types are missed calls. + return ContextCompat.getColor(context, R.color.missed_call); + } + } +} diff --git a/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java new file mode 100644 index 000000000..36662bab9 --- /dev/null +++ b/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.calldetails; + +import android.content.Context; +import android.content.Intent; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.View.OnClickListener; +import com.android.contacts.common.ClipboardUtils; +import com.android.dialer.common.Assert; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; + +/** ViewHolder container for {@link CallDetailsActivity} footer. */ +public class CallDetailsFooterViewHolder extends RecyclerView.ViewHolder + implements OnClickListener { + + private final View copy; + private final View edit; + + private String number; + + public CallDetailsFooterViewHolder(View view) { + super(view); + copy = view.findViewById(R.id.call_detail_action_copy); + edit = view.findViewById(R.id.call_detail_action_edit_before_call); + + copy.setOnClickListener(this); + edit.setOnClickListener(this); + } + + public void setPhoneNumber(String number) { + this.number = number; + } + + @Override + public void onClick(View view) { + Context context = view.getContext(); + if (view == copy) { + Logger.get(context).logImpression(DialerImpression.Type.CALL_DETAILS_COPY_NUMBER); + ClipboardUtils.copyText(context, null, number, true); + } else if (view == edit) { + Logger.get(context).logImpression(DialerImpression.Type.CALL_DETAILS_EDIT_BEFORE_CALL); + Intent dialIntent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(number)); + DialerUtils.startActivityWithErrorToast(context, dialIntent); + } else { + Assert.fail("View on click not implemented: " + view); + } + } +} diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java new file mode 100644 index 000000000..1679c2baf --- /dev/null +++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java @@ -0,0 +1,125 @@ +/* + * 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.calldetails; + +import android.content.Context; +import android.net.Uri; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.callcomposer.nano.CallComposerContact; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.Assert; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.util.DialerUtils; + +/** ViewHolder for Header/Contact in {@link CallDetailsActivity}. */ +public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder + implements OnClickListener { + + private final View callBackButton; + private final TextView nameView; + private final TextView numberView; + private final QuickContactBadge contactPhoto; + private final Context context; + + private CallComposerContact contact; + + CallDetailsHeaderViewHolder(View container) { + super(container); + context = container.getContext(); + callBackButton = container.findViewById(R.id.call_back_button); + nameView = (TextView) container.findViewById(R.id.contact_name); + numberView = (TextView) container.findViewById(R.id.phone_number); + contactPhoto = (QuickContactBadge) container.findViewById(R.id.quick_contact_photo); + callBackButton.setOnClickListener(this); + } + + /** + * Populates the contact info fields based on the current contact information. Copied from {@link + * com.android.contacts.common.dialog.CallSubjectDialog}. + */ + public void updateContactInfo(CallComposerContact contact) { + this.contact = contact; + setPhoto( + contact.photoId, + Uri.parse(contact.photoUri), + Uri.parse(contact.contactUri), + contact.nameOrNumber, + contact.isBusiness); + + nameView.setText(contact.nameOrNumber); + if (!TextUtils.isEmpty(contact.numberLabel) && !TextUtils.isEmpty(contact.displayNumber)) { + numberView.setVisibility(View.VISIBLE); + String secondaryInfo = + context.getString( + com.android.contacts.common.R.string.call_subject_type_and_number, + contact.numberLabel, + contact.displayNumber); + numberView.setText(secondaryInfo); + } else { + numberView.setVisibility(View.GONE); + numberView.setText(null); + } + } + + /** + * Sets the photo on the quick contact galleryIcon. Copied from {@link + * com.android.contacts.common.dialog.CallSubjectDialog}. + */ + private void setPhoto( + long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness) { + contactPhoto.assignContactUri(contactUri); + contactPhoto.setOverlay(null); + + int contactType = + isBusiness ? ContactPhotoManager.TYPE_BUSINESS : ContactPhotoManager.TYPE_DEFAULT; + String lookupKey = contactUri == null ? null : UriUtils.getLookupKeyFromUri(contactUri); + + ContactPhotoManager.DefaultImageRequest request = + new ContactPhotoManager.DefaultImageRequest( + displayName, lookupKey, contactType, true /* isCircular */); + + if (photoId == 0 && photoUri != null) { + contactPhoto.setImageDrawable( + context.getDrawable(R.drawable.product_logo_avatar_anonymous_color_120)); + } else { + ContactPhotoManager.getInstance(context) + .loadThumbnail( + contactPhoto, photoId, false /* darkTheme */, true /* isCircular */, request); + } + } + + @Override + public void onClick(View view) { + if (view == callBackButton) { + Logger.get(view.getContext()).logImpression(DialerImpression.Type.CALL_DETAILS_CALL_BACK); + DialerUtils.startActivityWithErrorToast( + view.getContext(), + new CallIntentBuilder(contact.number, CallInitiationType.Type.CALL_DETAILS).build()); + } else { + Assert.fail("View OnClickListener not implemented: " + view); + } + } +} diff --git a/java/com/android/dialer/calldetails/nano/CallDetailsEntries.java b/java/com/android/dialer/calldetails/nano/CallDetailsEntries.java new file mode 100644 index 000000000..aee8f3652 --- /dev/null +++ b/java/com/android/dialer/calldetails/nano/CallDetailsEntries.java @@ -0,0 +1,440 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! + +package com.android.dialer.calldetails.nano; + +/** This file is autogenerated, but javadoc required. */ +@SuppressWarnings("hiding") +public final class CallDetailsEntries + extends com.google.protobuf.nano.ExtendableMessageNano<CallDetailsEntries> { + + /** This file is autogenerated, but javadoc required. */ + public static final class CallDetailsEntry + extends com.google.protobuf.nano.ExtendableMessageNano<CallDetailsEntry> { + + private static volatile CallDetailsEntry[] _emptyArray; + public static CallDetailsEntry[] emptyArray() { + // Lazily initializes the empty array + if (_emptyArray == null) { + synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { + if (_emptyArray == null) { + _emptyArray = new CallDetailsEntry[0]; + } + } + } + return _emptyArray; + } + + // optional int64 call_id = 1; + public long callId; + + // optional int32 call_type = 2; + public int callType; + + // optional int32 features = 3; + public int features; + + // optional int64 date = 4; + public long date; + + // optional int64 duration = 5; + public long duration; + + // optional int64 data_usage = 6; + public long dataUsage; + + // repeated .com.android.dialer.enrichedcall.historyquery.proto. + // HistoryResult history_results = 7; + public com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult[] historyResults; + + // @@protoc_insertion_point(class_scope:com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry) + + public CallDetailsEntry() { + clear(); + } + + public CallDetailsEntry clear() { + callId = 0L; + callType = 0; + features = 0; + date = 0L; + duration = 0L; + dataUsage = 0L; + historyResults = + com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.emptyArray(); + unknownFieldData = null; + cachedSize = -1; + return this; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof CallDetailsEntry)) { + return false; + } + CallDetailsEntry other = (CallDetailsEntry) o; + if (this.callId != other.callId) { + return false; + } + if (this.callType != other.callType) { + return false; + } + if (this.features != other.features) { + return false; + } + if (this.date != other.date) { + return false; + } + if (this.duration != other.duration) { + return false; + } + if (this.dataUsage != other.dataUsage) { + return false; + } + if (!com.google.protobuf.nano.InternalNano.equals( + this.historyResults, other.historyResults)) { + return false; + } + if (unknownFieldData == null || unknownFieldData.isEmpty()) { + return other.unknownFieldData == null || other.unknownFieldData.isEmpty(); + } else { + return unknownFieldData.equals(other.unknownFieldData); + } + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + getClass().getName().hashCode(); + result = 31 * result + (int) (this.callId ^ (this.callId >>> 32)); + result = 31 * result + this.callType; + result = 31 * result + this.features; + result = 31 * result + (int) (this.date ^ (this.date >>> 32)); + result = 31 * result + (int) (this.duration ^ (this.duration >>> 32)); + result = 31 * result + (int) (this.dataUsage ^ (this.dataUsage >>> 32)); + result = 31 * result + com.google.protobuf.nano.InternalNano.hashCode(this.historyResults); + result = + 31 * result + + (unknownFieldData == null || unknownFieldData.isEmpty() + ? 0 + : unknownFieldData.hashCode()); + return result; + } + + @Override + public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output) + throws java.io.IOException { + if (this.callId != 0L) { + output.writeInt64(1, this.callId); + } + if (this.callType != 0) { + output.writeInt32(2, this.callType); + } + if (this.features != 0) { + output.writeInt32(3, this.features); + } + if (this.date != 0L) { + output.writeInt64(4, this.date); + } + if (this.duration != 0L) { + output.writeInt64(5, this.duration); + } + if (this.dataUsage != 0L) { + output.writeInt64(6, this.dataUsage); + } + if (this.historyResults != null && this.historyResults.length > 0) { + for (int i = 0; i < this.historyResults.length; i++) { + com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult element = + this.historyResults[i]; + if (element != null) { + output.writeMessage(7, element); + } + } + } + super.writeTo(output); + } + + @Override + protected int computeSerializedSize() { + int size = super.computeSerializedSize(); + if (this.callId != 0L) { + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(1, this.callId); + } + if (this.callType != 0) { + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(2, this.callType); + } + if (this.features != 0) { + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(3, this.features); + } + if (this.date != 0L) { + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(4, this.date); + } + if (this.duration != 0L) { + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(5, this.duration); + } + if (this.dataUsage != 0L) { + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(6, this.dataUsage); + } + if (this.historyResults != null && this.historyResults.length > 0) { + for (int i = 0; i < this.historyResults.length; i++) { + com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult element = + this.historyResults[i]; + if (element != null) { + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeMessageSize(7, element); + } + } + } + return size; + } + + @Override + public CallDetailsEntry mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) + throws java.io.IOException { + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + return this; + default: + { + if (!super.storeUnknownField(input, tag)) { + return this; + } + break; + } + case 8: + { + this.callId = input.readInt64(); + break; + } + case 16: + { + this.callType = input.readInt32(); + break; + } + case 24: + { + this.features = input.readInt32(); + break; + } + case 32: + { + this.date = input.readInt64(); + break; + } + case 40: + { + this.duration = input.readInt64(); + break; + } + case 48: + { + this.dataUsage = input.readInt64(); + break; + } + case 58: + { + int arrayLength = + com.google.protobuf.nano.WireFormatNano.getRepeatedFieldArrayLength(input, 58); + int i = this.historyResults == null ? 0 : this.historyResults.length; + com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult[] newArray = + new com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult + [i + arrayLength]; + if (i != 0) { + java.lang.System.arraycopy(this.historyResults, 0, newArray, 0, i); + } + for (; i < newArray.length - 1; i++) { + newArray[i] = + new com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult(); + input.readMessage(newArray[i]); + input.readTag(); + } + // Last one without readTag. + newArray[i] = + new com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult(); + input.readMessage(newArray[i]); + this.historyResults = newArray; + break; + } + } + } + } + + public static CallDetailsEntry parseFrom(byte[] data) + throws com.google.protobuf.nano.InvalidProtocolBufferNanoException { + return com.google.protobuf.nano.MessageNano.mergeFrom(new CallDetailsEntry(), data); + } + + public static CallDetailsEntry parseFrom( + com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { + return new CallDetailsEntry().mergeFrom(input); + } + } + + private static volatile CallDetailsEntries[] _emptyArray; + public static CallDetailsEntries[] emptyArray() { + // Lazily initializes the empty array + if (_emptyArray == null) { + synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { + if (_emptyArray == null) { + _emptyArray = new CallDetailsEntries[0]; + } + } + } + return _emptyArray; + } + + // repeated .com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry entries = 1; + public com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry[] entries; + + // @@protoc_insertion_point(class_scope:com.android.dialer.calldetails.CallDetailsEntries) + + public CallDetailsEntries() { + clear(); + } + + public CallDetailsEntries clear() { + entries = com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry.emptyArray(); + unknownFieldData = null; + cachedSize = -1; + return this; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof CallDetailsEntries)) { + return false; + } + CallDetailsEntries other = (CallDetailsEntries) o; + if (!com.google.protobuf.nano.InternalNano.equals(this.entries, other.entries)) { + return false; + } + if (unknownFieldData == null || unknownFieldData.isEmpty()) { + return other.unknownFieldData == null || other.unknownFieldData.isEmpty(); + } else { + return unknownFieldData.equals(other.unknownFieldData); + } + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + getClass().getName().hashCode(); + result = 31 * result + com.google.protobuf.nano.InternalNano.hashCode(this.entries); + result = + 31 * result + + (unknownFieldData == null || unknownFieldData.isEmpty() + ? 0 + : unknownFieldData.hashCode()); + return result; + } + + @Override + public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output) + throws java.io.IOException { + if (this.entries != null && this.entries.length > 0) { + for (int i = 0; i < this.entries.length; i++) { + com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry element = + this.entries[i]; + if (element != null) { + output.writeMessage(1, element); + } + } + } + super.writeTo(output); + } + + @Override + protected int computeSerializedSize() { + int size = super.computeSerializedSize(); + if (this.entries != null && this.entries.length > 0) { + for (int i = 0; i < this.entries.length; i++) { + com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry element = + this.entries[i]; + if (element != null) { + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeMessageSize(1, element); + } + } + } + return size; + } + + @Override + public CallDetailsEntries mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) + throws java.io.IOException { + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + return this; + default: + { + if (!super.storeUnknownField(input, tag)) { + return this; + } + break; + } + case 10: + { + int arrayLength = + com.google.protobuf.nano.WireFormatNano.getRepeatedFieldArrayLength(input, 10); + int i = this.entries == null ? 0 : this.entries.length; + com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry[] newArray = + new com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry + [i + arrayLength]; + if (i != 0) { + java.lang.System.arraycopy(this.entries, 0, newArray, 0, i); + } + for (; i < newArray.length - 1; i++) { + newArray[i] = + new com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry(); + input.readMessage(newArray[i]); + input.readTag(); + } + // Last one without readTag. + newArray[i] = + new com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry(); + input.readMessage(newArray[i]); + this.entries = newArray; + break; + } + } + } + } + + public static CallDetailsEntries parseFrom(byte[] data) + throws com.google.protobuf.nano.InvalidProtocolBufferNanoException { + return com.google.protobuf.nano.MessageNano.mergeFrom(new CallDetailsEntries(), data); + } + + public static CallDetailsEntries parseFrom( + com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { + return new CallDetailsEntries().mergeFrom(input); + } +} diff --git a/java/com/android/dialer/calldetails/res/drawable/multimedia_image_background.xml b/java/com/android/dialer/calldetails/res/drawable/multimedia_image_background.xml new file mode 100644 index 000000000..421bdbfee --- /dev/null +++ b/java/com/android/dialer/calldetails/res/drawable/multimedia_image_background.xml @@ -0,0 +1,20 @@ +<?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" + android:shape="rectangle"> + <corners android:radius="2dp"/> +</shape> diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_activity.xml b/java/com/android/dialer/calldetails/res/layout/call_details_activity.xml new file mode 100644 index 000000000..038a8745e --- /dev/null +++ b/java/com/android/dialer/calldetails/res/layout/call_details_activity.xml @@ -0,0 +1,37 @@ +<?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 + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:background="@color/dialer_theme_color" + android:elevation="4dp" + android:titleTextAppearance="@style/toolbar_title_text" + android:title="@string/call_details" + android:navigationIcon="@drawable/quantum_ic_arrow_back_white_24"/> + + <android.support.v7.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/background_dialer_white"/> +</LinearLayout>
\ No newline at end of file diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml new file mode 100644 index 000000000..7f8bb8087 --- /dev/null +++ b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml @@ -0,0 +1,73 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="@dimen/call_entry_padding"> + + <ImageView + android:id="@+id/call_direction" + android:layout_width="@dimen/call_entry_icon_size" + android:layout_height="@dimen/call_entry_icon_size" + android:layout_marginStart="@dimen/call_entry_padding" + android:layout_marginEnd="@dimen/call_entry_left_margin"/> + + <TextView + android:id="@+id/call_type" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toEndOf="@+id/call_direction" + style="@style/PrimaryText"/> + + <TextView + android:id="@+id/call_time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toEndOf="@id/call_direction" + android:layout_below="@+id/call_type" + android:layout_marginBottom="@dimen/call_entry_bottom_padding" + style="@style/SecondaryText"/> + + <TextView + android:id="@+id/call_duration" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_marginEnd="@dimen/call_entry_padding" + style="@style/PrimaryText"/> + + <include + layout="@layout/ec_data_container" + android:id="@+id/ec_container" + android:layout_height="@dimen/ec_container_height" + android:layout_width="match_parent" + android:layout_marginStart="@dimen/ec_text_left_margin" + android:layout_below="@+id/call_time" + android:visibility="gone"/> + + <View + android:id="@+id/divider" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_below="@id/ec_container" + android:layout_marginTop="@dimen/ec_divider_top_bottom_margin" + android:layout_marginBottom="@dimen/ec_divider_top_bottom_margin" + android:layout_marginStart="@dimen/ec_text_left_margin" + android:background="#12000000" + android:visibility="gone"/> +</RelativeLayout>
\ No newline at end of file diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_footer.xml b/java/com/android/dialer/calldetails/res/layout/call_details_footer.xml new file mode 100644 index 000000000..885cb0989 --- /dev/null +++ b/java/com/android/dialer/calldetails/res/layout/call_details_footer.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_marginTop="@dimen/ec_divider_top_bottom_margin" + android:layout_marginBottom="@dimen/ec_divider_top_bottom_margin" + android:background="#12000000"/> + + <TextView + android:id="@+id/call_detail_action_copy" + style="@style/CallDetailsActionItemStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawableStart="@drawable/quantum_ic_content_copy_grey600_24" + android:text="@string/call_details_copy_number"/> + + <TextView + android:id="@+id/call_detail_action_edit_before_call" + style="@style/CallDetailsActionItemStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawableStart="@drawable/quantum_ic_edit_grey600_24" + android:text="@string/call_details_edit_number"/> +</LinearLayout> diff --git a/java/com/android/dialer/calldetails/res/layout/contact_container.xml b/java/com/android/dialer/calldetails/res/layout/contact_container.xml new file mode 100644 index 000000000..95fe189b2 --- /dev/null +++ b/java/com/android/dialer/calldetails/res/layout/contact_container.xml @@ -0,0 +1,60 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:padding="@dimen/contact_container_padding"> + + <QuickContactBadge + android:id="@+id/quick_contact_photo" + android:layout_width="@dimen/call_details_contact_photo_size" + android:layout_height="@dimen/call_details_contact_photo_size" + android:focusable="true"/> + + <TextView + android:id="@+id/contact_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/text_bottom_margin" + android:layout_marginStart="@dimen/photo_text_margin" + android:layout_toEndOf="@+id/quick_contact_photo" + android:layout_toStartOf="@+id/call_back_button" + style="@style/PrimaryText"/> + + <TextView + android:id="@+id/phone_number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/photo_text_margin" + android:layout_toEndOf="@+id/quick_contact_photo" + android:layout_toStartOf="@+id/call_back_button" + android:layout_below="@+id/contact_name" + style="@style/SecondaryText"/> + + <ImageView + android:id="@+id/call_back_button" + android:layout_width="@dimen/call_back_button_size" + android:layout_height="@dimen/call_back_button_size" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/description_call_log_call_action" + android:src="@drawable/quantum_ic_call_white_24" + android:tint="@color/secondary_text_color"/> +</RelativeLayout>
\ No newline at end of file diff --git a/java/com/android/dialer/calldetails/res/layout/ec_data_container.xml b/java/com/android/dialer/calldetails/res/layout/ec_data_container.xml new file mode 100644 index 000000000..5ad7912fa --- /dev/null +++ b/java/com/android/dialer/calldetails/res/layout/ec_data_container.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="@dimen/ec_container_height"> + + <TextView + android:id="@+id/multimedia_details" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:maxLines="2" + style="@style/SecondaryText"/> + + <FrameLayout + android:id="@+id/multimedia_image_container" + android:layout_width="@dimen/ec_photo_size" + android:layout_height="@dimen/ec_photo_size" + android:layout_alignParentEnd="true" + android:layout_marginEnd="@dimen/call_entry_padding" + android:layout_centerVertical="true" + android:background="@drawable/multimedia_image_background" + android:outlineProvider="background" + android:visibility="gone"> + + <ImageView + android:id="@+id/multimedia_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop"/> + + <com.android.incallui.autoresizetext.AutoResizeTextView + android:id="@+id/multimedia_attachments_number" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="4dp" + android:gravity="center" + android:textColor="@color/background_dialer_white" + android:textSize="100sp" + android:background="#80000000" + android:visibility="gone"/> + </FrameLayout> +</RelativeLayout>
\ No newline at end of file diff --git a/java/com/android/dialer/calldetails/res/menu/call_details_menu.xml b/java/com/android/dialer/calldetails/res/menu/call_details_menu.xml new file mode 100644 index 000000000..c2d1032da --- /dev/null +++ b/java/com/android/dialer/calldetails/res/menu/call_details_menu.xml @@ -0,0 +1,23 @@ +<?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 + --> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/call_detail_delete_menu_item" + android:icon="@drawable/quantum_ic_delete_white_24" + android:title="@string/delete" + android:showAsAction="ifRoom"/> +</menu>
\ No newline at end of file diff --git a/java/com/android/dialer/calldetails/res/values/dimens.xml b/java/com/android/dialer/calldetails/res/values/dimens.xml new file mode 100644 index 000000000..b1a8f1c8e --- /dev/null +++ b/java/com/android/dialer/calldetails/res/values/dimens.xml @@ -0,0 +1,40 @@ +<?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="text_bottom_margin">2dp</dimen> + <dimen name="call_details_primary_text_size">16sp</dimen> + <dimen name="call_details_secondary_text_size">14sp</dimen> + + <!-- contact container --> + <dimen name="contact_container_padding">16dp</dimen> + <dimen name="call_details_contact_photo_size">40dp</dimen> + <dimen name="photo_text_margin">16dp</dimen> + <dimen name="call_back_button_size">24dp</dimen> + + <!-- call entry container --> + <dimen name="call_entry_icon_size">24dp</dimen> + <dimen name="call_entry_padding">16dp</dimen> + <dimen name="call_entry_bottom_padding">14dp</dimen> + <dimen name="call_entry_left_margin">32dp</dimen> + + <!-- EC container --> + <dimen name="call_details_ec_text_size">12sp</dimen> + <dimen name="ec_container_height">48dp</dimen> + <dimen name="ec_text_left_margin">72dp</dimen> + <dimen name="ec_photo_size">40dp</dimen> + <dimen name="ec_divider_top_bottom_margin">8dp</dimen> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/calldetails/res/values/strings.xml b/java/com/android/dialer/calldetails/res/values/strings.xml new file mode 100644 index 000000000..8a7cc4cfc --- /dev/null +++ b/java/com/android/dialer/calldetails/res/values/strings.xml @@ -0,0 +1,42 @@ +<?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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Title bar for call detail screen --> + <string name="call_details">Call details</string> + + <!-- Menu item in call details used to remove a call or voicemail from the call log. --> + <string name="delete">Delete</string> + + <!-- Option displayed in context menu to copy long pressed phone number. [CHAR LIMIT=48] --> + <string name="call_details_copy_number">Copy number</string> + + <!-- Label for action to edit a number before calling it. [CHAR LIMIT=48] --> + <string name="call_details_edit_number">Edit number before call</string> + + <!-- String describing the phone icon on a call log list item. When tapped, it will place a + call to the number represented by that call log entry. [CHAR LIMIT=NONE]--> + <string name="description_call_log_call_action">Call</string> + + <!-- String shown when the call details show a image that was sent --> + <string name="sent_a_photo">Sent a photo</string> + + <!-- String shown when the call details show a image that was received --> + <string name="received_a_photo">Received a photo</string> + + <!-- Messages shown to the user are wrapped in quotes, e.g. the user would see "Some text" --> + <string name="message_in_quotes">\"<xliff:g id="message">%1$s</xliff:g>\"</string> +</resources> diff --git a/java/com/android/dialer/calldetails/res/values/styles.xml b/java/com/android/dialer/calldetails/res/values/styles.xml new file mode 100644 index 000000000..4fffe1afb --- /dev/null +++ b/java/com/android/dialer/calldetails/res/values/styles.xml @@ -0,0 +1,48 @@ +<?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> + <style name="PrimaryText"> + <item name="android:textColor">#DE000000</item> + <item name="android:textSize">@dimen/call_details_primary_text_size</item> + <item name="android:maxLines">1</item> + </style> + + <style name="SecondaryText"> + <item name="android:textColor">#8A000000</item> + <item name="android:textSize">@dimen/call_details_secondary_text_size</item> + <item name="android:maxLines">1</item> + </style> + + <style name="ECText"> + <item name="android:textColor">#8A000000</item> + <item name="android:textSize">@dimen/call_details_ec_text_size</item> + <item name="android:maxLines">1</item> + </style> + + <style name="CallDetailsActionItemStyle"> + <item name="android:foreground">?android:attr/selectableItemBackground</item> + <item name="android:clickable">true</item> + <item name="android:drawablePadding">28dp</item> + <item name="android:gravity">center_vertical</item> + <item name="android:paddingStart">28dp</item> + <item name="android:paddingEnd">28dp</item> + <item name="android:paddingTop">16dp</item> + <item name="android:paddingBottom">16dp</item> + <item name="android:textColor">#8A000000</item> + <item name="android:textSize">14sp</item> + </style> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/callintent/nano/CallInitiationType.java b/java/com/android/dialer/callintent/nano/CallInitiationType.java index 4badd6e57..1dddb6ce8 100644 --- a/java/com/android/dialer/callintent/nano/CallInitiationType.java +++ b/java/com/android/dialer/callintent/nano/CallInitiationType.java @@ -11,17 +11,19 @@ * 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 + * limitations under the License. */ // Generated by the protocol buffer compiler. DO NOT EDIT! package com.android.dialer.callintent.nano; +/** This file is autogenerated, but javadoc required. */ @SuppressWarnings("hiding") -public final class CallInitiationType extends - com.google.protobuf.nano.ExtendableMessageNano<CallInitiationType> { +public final class CallInitiationType + extends com.google.protobuf.nano.ExtendableMessageNano<CallInitiationType> { + /** This file is autogenerated, but javadoc required. */ // enum Type public interface Type { public static final int UNKNOWN_INITIATION = 0; @@ -44,11 +46,11 @@ public final class CallInitiationType extends } private static volatile CallInitiationType[] _emptyArray; + public static CallInitiationType[] emptyArray() { // Lazily initializes the empty array if (_emptyArray == null) { - synchronized ( - com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { + synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { if (_emptyArray == null) { _emptyArray = new CallInitiationType[0]; } @@ -70,20 +72,20 @@ public final class CallInitiationType extends } @Override - public CallInitiationType mergeFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) + public CallInitiationType mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { while (true) { int tag = input.readTag(); switch (tag) { case 0: return this; - default: { - if (!super.storeUnknownField(input, tag)) { - return this; + default: + { + if (!super.storeUnknownField(input, tag)) { + return this; + } + break; } - break; - } } } } @@ -94,8 +96,7 @@ public final class CallInitiationType extends } public static CallInitiationType parseFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) - throws java.io.IOException { + com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { return new CallInitiationType().mergeFrom(input); } } diff --git a/java/com/android/dialer/calllogutils/AndroidManifest.xml b/java/com/android/dialer/calllogutils/AndroidManifest.xml new file mode 100644 index 000000000..228865a38 --- /dev/null +++ b/java/com/android/dialer/calllogutils/AndroidManifest.xml @@ -0,0 +1,16 @@ +<!-- + ~ 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 package="com.android.dialer.calllogutils"/>
\ No newline at end of file diff --git a/java/com/android/dialer/calllogutils/CallEntryFormatter.java b/java/com/android/dialer/calllogutils/CallEntryFormatter.java new file mode 100644 index 000000000..bd6d53f48 --- /dev/null +++ b/java/com/android/dialer/calllogutils/CallEntryFormatter.java @@ -0,0 +1,113 @@ +/* + * 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.calllogutils; + +import android.content.Context; +import android.icu.lang.UCharacter; +import android.icu.text.BreakIterator; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.text.format.DateUtils; +import android.text.format.Formatter; +import com.android.dialer.util.DialerUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** Utility class for formatting data and data usage in call log entries. */ +public class CallEntryFormatter { + + /** + * Formats the provided date into a value suitable for display in the current locale. + * + * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016 + * may 25,20:02". + * + * <p>For pre-N devices, the returned value may not start with a capital if the local convention + * is to not capitalize day names. On N+ devices, the returned value is always capitalized. + */ + public static CharSequence formatDate(Context context, long callDateMillis) { + CharSequence dateValue = + DateUtils.formatDateRange( + context, + callDateMillis /* startDate */, + callDateMillis /* endDate */, + DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_WEEKDAY + | DateUtils.FORMAT_SHOW_YEAR); + + // We want the beginning of the date string to be capitalized, even if the word at the beginning + // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba” + // (not capitalized). To handle this issue we apply title casing to the start of the sentence so + // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02". + // + // The ICU library was not available in Android until N, so we can only do this in N+ devices. + // Pre-N devices will still see incorrect capitalization in some languages. + if (VERSION.SDK_INT < VERSION_CODES.N) { + return dateValue; + } + + // Using the ICU library is safer than just applying toUpperCase() on the first letter of the + // word because in some languages, there can be multiple starting characters which should be + // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be + // capitalized together. + + // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized like the + // month ("May") are not lower-cased as part of the conversion. + return UCharacter.toTitleCase( + Locale.getDefault(), + dateValue.toString(), + BreakIterator.getSentenceInstance(), + UCharacter.TITLECASE_NO_LOWERCASE); + } + + private static CharSequence formatDuration(Context context, long elapsedSeconds) { + long minutes = 0; + long seconds = 0; + + if (elapsedSeconds >= 60) { + minutes = elapsedSeconds / 60; + elapsedSeconds -= minutes * 60; + seconds = elapsedSeconds; + return context.getString(R.string.call_details_duration_format, minutes, seconds); + } else { + seconds = elapsedSeconds; + return context.getString(R.string.call_details_short_duration_format, seconds); + } + } + + /** + * Formats a string containing the call duration and the data usage (if specified). + * + * @param elapsedSeconds Total elapsed seconds. + * @param dataUsage Data usage in bytes, or null if not specified. + * @return String containing call duration and data usage. + */ + public static CharSequence formatDurationAndDataUsage( + Context context, long elapsedSeconds, Long dataUsage) { + CharSequence duration = formatDuration(context, elapsedSeconds); + List<CharSequence> durationItems = new ArrayList<>(); + if (dataUsage != null) { + durationItems.add(duration); + durationItems.add(Formatter.formatShortFileSize(context, dataUsage)); + return DialerUtils.join(durationItems); + } else { + return duration; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallTypeHelper.java b/java/com/android/dialer/calllogutils/CallTypeHelper.java index f3c27a1ac..d3b5b67d7 100644 --- a/java/com/android/dialer/app/calllog/CallTypeHelper.java +++ b/java/com/android/dialer/calllogutils/CallTypeHelper.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package com.android.dialer.app.calllog; +package com.android.dialer.calllogutils; import android.content.res.Resources; -import com.android.dialer.app.R; import com.android.dialer.compat.AppCompatConstants; /** Helper class to perform operations related to call types. */ diff --git a/java/com/android/dialer/app/calllog/CallTypeIconsView.java b/java/com/android/dialer/calllogutils/CallTypeIconsView.java index cd5c5460c..61208bc9a 100644 --- a/java/com/android/dialer/app/calllog/CallTypeIconsView.java +++ b/java/com/android/dialer/calllogutils/CallTypeIconsView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.dialer.app.calllog; +package com.android.dialer.calllogutils; import android.content.Context; import android.graphics.Bitmap; @@ -26,7 +26,6 @@ import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import com.android.contacts.common.util.BitmapUtil; -import com.android.dialer.app.R; import com.android.dialer.compat.AppCompatConstants; import java.util.ArrayList; import java.util.List; @@ -41,6 +40,7 @@ public class CallTypeIconsView extends View { private static Resources sResources; private List<Integer> mCallTypes = new ArrayList<>(3); private boolean mShowVideo = false; + private boolean mShowHd = false; private int mWidth; private int mHeight; @@ -94,6 +94,15 @@ public class CallTypeIconsView extends View { return mShowVideo; } + public void setShowHd(boolean showHd) { + mShowHd = showHd; + if (showHd) { + mWidth += sResources.hdCall.getIntrinsicWidth(); + mHeight = Math.max(mHeight, sResources.hdCall.getIntrinsicHeight()); + invalidate(); + } + } + public int getCount() { return mCallTypes.size(); } @@ -147,6 +156,13 @@ public class CallTypeIconsView extends View { drawable.setBounds(left, 0, right, sResources.videoCall.getIntrinsicHeight()); drawable.draw(canvas); } + // If showing HD call icon, draw it scaled appropriately. + if (mShowHd) { + final Drawable drawable = sResources.hdCall; + final int right = left + sResources.hdCall.getIntrinsicWidth(); + drawable.setBounds(left, 0, right, sResources.hdCall.getIntrinsicHeight()); + drawable.draw(canvas); + } } private static class Resources { @@ -166,9 +182,12 @@ public class CallTypeIconsView extends View { // Drawable representing a blocked call. public final Drawable blocked; - // Drawable repesenting a video call. + // Drawable repesenting a video call. public final Drawable videoCall; + // Drawable represeting a hd call. + public final Drawable hdCall; + /** The margin to use for icons. */ public final int iconMargin; @@ -204,6 +223,10 @@ public class CallTypeIconsView extends View { videoCall.setColorFilter( r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY); + hdCall = getScaledBitmap(context, R.drawable.quantum_ic_hd_white_24); + hdCall.setColorFilter( + r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY); + iconMargin = r.getDimensionPixelSize(R.dimen.call_log_icon_margin); } diff --git a/java/com/android/dialer/app/calllog/PhoneAccountUtils.java b/java/com/android/dialer/calllogutils/PhoneAccountUtils.java index c6d94d341..c639893ef 100644 --- a/java/com/android/dialer/app/calllog/PhoneAccountUtils.java +++ b/java/com/android/dialer/calllogutils/PhoneAccountUtils.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.dialer.app.calllog; +package com.android.dialer.calllogutils; import android.content.ComponentName; import android.content.Context; diff --git a/java/com/android/dialer/app/PhoneCallDetails.java b/java/com/android/dialer/calllogutils/PhoneCallDetails.java index 436f68eec..ba05a87e2 100644 --- a/java/com/android/dialer/app/PhoneCallDetails.java +++ b/java/com/android/dialer/calllogutils/PhoneCallDetails.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.dialer.app; +package com.android.dialer.calllogutils; import android.content.Context; import android.content.res.Resources; @@ -27,7 +27,6 @@ import android.text.TextUtils; import com.android.contacts.common.ContactsUtils.UserType; import com.android.contacts.common.preference.ContactsPreferences; import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.dialer.app.calllog.PhoneNumberDisplayUtil; import com.android.dialer.phonenumbercache.ContactInfo; /** The details of a phone call to be shown in the UI. */ diff --git a/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java b/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java index 410d4cc37..9bebfacac 100644 --- a/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java +++ b/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.dialer.app.calllog; +package com.android.dialer.calllogutils; import android.content.Context; import android.provider.CallLog.Calls; @@ -22,15 +22,13 @@ import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; import com.android.contacts.common.compat.PhoneNumberUtilsCompat; -import com.android.dialer.app.R; import com.android.dialer.phonenumberutil.PhoneNumberHelper; /** Helper for formatting and managing the display of phone numbers. */ public class PhoneNumberDisplayUtil { /** Returns the string to display for the given phone number if there is no matching contact. */ - /* package */ - static CharSequence getDisplayName( + public static CharSequence getDisplayName( Context context, CharSequence number, int presentation, boolean isVoicemail) { if (presentation == Calls.PRESENTATION_UNKNOWN) { return context.getResources().getString(R.string.unknown); @@ -42,7 +40,7 @@ public class PhoneNumberDisplayUtil { return context.getResources().getString(R.string.payphone); } if (isVoicemail) { - return context.getResources().getString(R.string.voicemail); + return context.getResources().getString(R.string.voicemail_string); } if (PhoneNumberHelper.isLegacyUnknownNumbers(number)) { return context.getResources().getString(R.string.unknown); diff --git a/java/com/android/dialer/calllogutils/res/values/colors.xml b/java/com/android/dialer/calllogutils/res/values/colors.xml new file mode 100644 index 000000000..dc4ec2493 --- /dev/null +++ b/java/com/android/dialer/calllogutils/res/values/colors.xml @@ -0,0 +1,24 @@ +<?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 for missed call icons. --> + <color name="missed_call">#ff2e58</color> + <!-- Color for answered or outgoing call icons. --> + <color name="answered_call">#00c853</color> + <!-- Color for blocked call icons. --> + <color name="blocked_call">@color/dialer_secondary_text_color</color> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/calllogutils/res/values/dimens.xml b/java/com/android/dialer/calllogutils/res/values/dimens.xml new file mode 100644 index 000000000..0935ac188 --- /dev/null +++ b/java/com/android/dialer/calllogutils/res/values/dimens.xml @@ -0,0 +1,19 @@ +<?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="call_type_icon_size">12dp</dimen> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/calllogutils/res/values/strings.xml b/java/com/android/dialer/calllogutils/res/values/strings.xml new file mode 100644 index 000000000..6a6f10113 --- /dev/null +++ b/java/com/android/dialer/calllogutils/res/values/strings.xml @@ -0,0 +1,90 @@ +<?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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Title for incoming call type. [CHAR LIMIT=40] --> + <string name="type_incoming">Incoming call</string> + + <!-- Title for incoming call which was transferred to another device. [CHAR LIMIT=60] --> + <string name="type_incoming_pulled">Incoming call transferred to another device</string> + + <!-- Title for outgoing call type. [CHAR LIMIT=40] --> + <string name="type_outgoing">Outgoing call</string> + + <!-- Title for outgoing call which was transferred to another device. [CHAR LIMIT=60] --> + <string name="type_outgoing_pulled">Outgoing call transferred to another device</string> + + <!-- Title for missed call type. [CHAR LIMIT=40] --> + <string name="type_missed">Missed call</string> + + <!-- Title for incoming video call in call details screen [CHAR LIMIT=60] --> + <string name="type_incoming_video">Incoming video call</string> + + <!-- Title for incoming video call in call details screen which was transferred to another device. + [CHAR LIMIT=60] --> + <string name="type_incoming_video_pulled">Incoming video call transferred to another device</string> + + <!-- Title for outgoing video call in call details screen [CHAR LIMIT=60] --> + <string name="type_outgoing_video">Outgoing video call</string> + + <!-- Title for outgoing video call in call details screen which was transferred to another device. + [CHAR LIMIT=60] --> + <string name="type_outgoing_video_pulled">Outgoing video call transferred to another device</string> + + <!-- Title for missed video call in call details screen [CHAR LIMIT=60] --> + <string name="type_missed_video">Missed video call</string> + + <!-- Title for voicemail details screen --> + <string name="type_voicemail">Voicemail</string> + + <!-- Title for rejected call type. [CHAR LIMIT=40] --> + <string name="type_rejected">Declined call</string> + + <!-- Title for blocked call type. [CHAR LIMIT=40] --> + <string name="type_blocked">Blocked call</string> + + <!-- Title for "answered elsewhere" call type. This will happen if a call was ringing + simultaneously on multiple devices, and the user answered it on a device other than the + current device. [CHAR LIMIT=60] --> + <string name="type_answered_elsewhere">Call answered on another device</string> + + <!-- String describing the phone account the call was made on or to. This string will be used + in description_incoming_missed_call, description_incoming_answered_call, and + description_outgoing_call. + Note: AccessibilityServices uses this attribute to announce what the view represents. + [CHAR LIMIT=NONE] --> + <string name="description_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">^1</xliff:g></string> + + <!-- String describing the secondary line number the call was received via. + Note: AccessibilityServices use this attribute to announce what the view represents. + [CHAR LIMIT=NONE]--> + <string name="description_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string> + + <!-- String describing the PhoneAccount and via number that a call was received on, if both are + visible. + Note: AccessibilityServices use this attribute to announce what the view represents. + [CHAR LIMIT=NONE]--> + <string name="description_via_number_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g>, via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g></string> + + <!-- String used for displaying calls to the voicemail number in the call log --> + <string name="voicemail_string">Voicemail</string> + + <!-- A nicely formatted call duration displayed when viewing call details. For example "42 min 28 sec" --> + <string name="call_details_duration_format"><xliff:g example="42" id="minutes">%s</xliff:g> min <xliff:g example="28" id="seconds">%s</xliff:g> sec</string> + + <!-- A nicely formatted call duration displayed when viewing call details for duration less than 1 minute. For example "28 sec" --> + <string name="call_details_short_duration_format"><xliff:g example="28" id="seconds">%s</xliff:g> sec</string> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/common/Assert.java b/java/com/android/dialer/common/Assert.java index 00b4f2595..943e1ddcf 100644 --- a/java/com/android/dialer/common/Assert.java +++ b/java/com/android/dialer/common/Assert.java @@ -19,6 +19,7 @@ package com.android.dialer.common; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import javax.annotation.CheckReturnValue; /** Assertions which will result in program termination unless disabled by flags. */ public class Assert { @@ -33,7 +34,9 @@ public class Assert { * Called when a truly exceptional case occurs. * * @throws AssertionError + * @deprecated Use throw Assert.create*FailException() instead. */ + @Deprecated public static void fail() { throw new AssertionError("Fail"); } @@ -43,11 +46,38 @@ public class Assert { * * @param reason the optional reason to supply as the exception message * @throws AssertionError + * @deprecated Use throw Assert.create*FailException() instead. */ + @Deprecated public static void fail(String reason) { throw new AssertionError(reason); } + @CheckReturnValue + public static AssertionError createAssertionFailException(String msg) { + return new AssertionError(msg); + } + + @CheckReturnValue + public static UnsupportedOperationException createUnsupportedOperationFailException() { + return new UnsupportedOperationException(); + } + + @CheckReturnValue + public static UnsupportedOperationException createUnsupportedOperationFailException(String msg) { + return new UnsupportedOperationException(msg); + } + + @CheckReturnValue + public static IllegalStateException createIllegalStateFailException() { + return new IllegalStateException(); + } + + @CheckReturnValue + public static IllegalStateException createIllegalStateFailException(String msg) { + return new IllegalStateException(msg); + } + /** * Ensures the truth of an expression involving one or more parameters to the calling method. * diff --git a/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java b/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java deleted file mode 100644 index f9d7cea90..000000000 --- a/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2016 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.common; - -import android.support.annotation.Nullable; -import javax.annotation.Generated; - - - final class AutoValue_FallibleAsyncTask_FallibleTaskResult<ResultT> extends FallibleAsyncTask.FallibleTaskResult<ResultT> { - - private final Throwable throwable; - private final ResultT result; - - AutoValue_FallibleAsyncTask_FallibleTaskResult( - @Nullable Throwable throwable, - @Nullable ResultT result) { - this.throwable = throwable; - this.result = result; - } - - @Nullable - @Override - public Throwable getThrowable() { - return throwable; - } - - @Nullable - @Override - public ResultT getResult() { - return result; - } - - @Override - public String toString() { - return "FallibleTaskResult{" - + "throwable=" + throwable + ", " - + "result=" + result - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof FallibleAsyncTask.FallibleTaskResult) { - FallibleAsyncTask.FallibleTaskResult<?> that = (FallibleAsyncTask.FallibleTaskResult<?>) o; - return ((this.throwable == null) ? (that.getThrowable() == null) : this.throwable.equals(that.getThrowable())) - && ((this.result == null) ? (that.getResult() == null) : this.result.equals(that.getResult())); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= (throwable == null) ? 0 : this.throwable.hashCode(); - h *= 1000003; - h ^= (result == null) ? 0 : this.result.hashCode(); - return h; - } - -} - diff --git a/java/com/android/dialer/common/FallibleAsyncTask.java b/java/com/android/dialer/common/FallibleAsyncTask.java index fbdbda75f..f3abace1a 100644 --- a/java/com/android/dialer/common/FallibleAsyncTask.java +++ b/java/com/android/dialer/common/FallibleAsyncTask.java @@ -20,7 +20,7 @@ import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.android.dialer.common.FallibleAsyncTask.FallibleTaskResult; - +import com.google.auto.value.AutoValue; /** * A task that runs work in the background, passing Throwables from {@link @@ -52,8 +52,8 @@ public abstract class FallibleAsyncTask<ParamsT, ProgressT, ResultT> * * @param <ResultT> the type of the result of the background computation */ - - protected abstract static class FallibleTaskResult<ResultT> { + @AutoValue + public abstract static class FallibleTaskResult<ResultT> { /** Creates an instance of FallibleTaskResult for the given throwable. */ private static <ResultT> FallibleTaskResult<ResultT> createFailureResult(@NonNull Throwable t) { diff --git a/java/com/android/dialer/common/PerAccountSharedPreferences.java b/java/com/android/dialer/common/PerAccountSharedPreferences.java new file mode 100644 index 000000000..0ed1b03a5 --- /dev/null +++ b/java/com/android/dialer/common/PerAccountSharedPreferences.java @@ -0,0 +1,146 @@ +/* + * 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.common; + +import android.content.Context; +import android.content.SharedPreferences; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import java.util.Set; + +/** + * Class that helps us store dialer preferences that are phone account dependent. This is necessary + * for cases such as settings that are phone account dependent e.g endless vm. The logic is to + * essentially store the shared preference by appending the phone account id to the key. + */ +public class PerAccountSharedPreferences { + private final String sharedPrefsKeyPrefix; + private final SharedPreferences preferences; + private final PhoneAccountHandle phoneAccountHandle; + + public PerAccountSharedPreferences( + Context context, PhoneAccountHandle handle, SharedPreferences prefs) { + preferences = prefs; + phoneAccountHandle = handle; + sharedPrefsKeyPrefix = "phone_account_dependent_"; + } + + /** + * Not to be used, currently only used by {@VisualVoicemailPreferences} for legacy reasons. + */ + protected PerAccountSharedPreferences( + Context context, PhoneAccountHandle handle, SharedPreferences prefs, String prefix) { + Assert.checkArgument(prefix.equals("visual_voicemail_")); + preferences = prefs; + phoneAccountHandle = handle; + sharedPrefsKeyPrefix = prefix; + } + + public class Editor { + + private final SharedPreferences.Editor editor; + + private Editor() { + editor = preferences.edit(); + } + + public void apply() { + editor.apply(); + } + + public Editor putBoolean(String key, boolean value) { + editor.putBoolean(getKey(key), value); + return this; + } + + public Editor putFloat(String key, float value) { + editor.putFloat(getKey(key), value); + return this; + } + + public Editor putInt(String key, int value) { + editor.putInt(getKey(key), value); + return this; + } + + public Editor putLong(String key, long value) { + editor.putLong(getKey(key), value); + return this; + } + + public Editor putString(String key, String value) { + editor.putString(getKey(key), value); + return this; + } + + public Editor putStringSet(String key, Set<String> value) { + editor.putStringSet(getKey(key), value); + return this; + } + } + + public Editor edit() { + return new Editor(); + } + + public boolean getBoolean(String key, boolean defValue) { + return getValue(key, defValue); + } + + public float getFloat(String key, float defValue) { + return getValue(key, defValue); + } + + public int getInt(String key, int defValue) { + return getValue(key, defValue); + } + + public long getLong(String key, long defValue) { + return getValue(key, defValue); + } + + public String getString(String key, String defValue) { + return getValue(key, defValue); + } + + @Nullable + public String getString(String key) { + return getValue(key, null); + } + + public Set<String> getStringSet(String key, Set<String> defValue) { + return getValue(key, defValue); + } + + public boolean contains(String key) { + return preferences.contains(getKey(key)); + } + + private <T> T getValue(String key, T defValue) { + if (!contains(key)) { + return defValue; + } + Object object = preferences.getAll().get(getKey(key)); + if (object == null) { + return defValue; + } + return (T) object; + } + + private String getKey(String key) { + return sharedPrefsKeyPrefix + key + "_" + phoneAccountHandle.getId(); + } +} diff --git a/java/com/android/dialer/common/proguard.flags b/java/com/android/dialer/common/proguard.flags new file mode 100644 index 000000000..4b6b84671 --- /dev/null +++ b/java/com/android/dialer/common/proguard.flags @@ -0,0 +1,4 @@ +-assumenosideeffects class com.android.dialer.common.LogUtil { + public static void v(...); + public static void d(...); +} diff --git a/java/com/android/dialer/common/res/values/config.xml b/java/com/android/dialer/common/res/values/config.xml new file mode 100644 index 000000000..c4df279ba --- /dev/null +++ b/java/com/android/dialer/common/res/values/config.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="spring_hd_codec">false</bool> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/constants/Constants.java b/java/com/android/dialer/constants/Constants.java index 77773018a..d92c0bcfc 100644 --- a/java/com/android/dialer/constants/Constants.java +++ b/java/com/android/dialer/constants/Constants.java @@ -19,7 +19,6 @@ package com.android.dialer.constants; import android.support.annotation.NonNull; import com.android.dialer.common.Assert; import com.android.dialer.proguard.UsedByReflection; -import com.android.dialer.constants.ConstantsImpl; /** * Utility to access constants that are different across build variants (Google Dialer, AOSP, @@ -29,11 +28,22 @@ import com.android.dialer.constants.ConstantsImpl; */ @UsedByReflection(value = "Constants.java") public abstract class Constants { - private static Constants instance = new ConstantsImpl(); + private static Constants instance; private static boolean didInitializeInstance; @NonNull public static synchronized Constants get() { + if (!didInitializeInstance) { + didInitializeInstance = true; + try { + Class<?> clazz = Class.forName(Constants.class.getName() + "Impl"); + instance = (Constants) clazz.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + Assert.fail( + "Unable to create an instance of ConstantsImpl. To fix this error include one of the " + + "constants modules (googledialer, aosp etc...) in your target."); + } + } return instance; } diff --git a/java/com/android/dialer/database/CallLogQueryHandler.java b/java/com/android/dialer/database/CallLogQueryHandler.java index ffca69f40..1f6bd5fb3 100644 --- a/java/com/android/dialer/database/CallLogQueryHandler.java +++ b/java/com/android/dialer/database/CallLogQueryHandler.java @@ -33,6 +33,7 @@ import android.os.Message; import android.provider.CallLog.Calls; import android.provider.VoicemailContract.Status; import android.provider.VoicemailContract.Voicemails; +import android.support.v4.os.BuildCompat; import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler; import com.android.dialer.common.LogUtil; import com.android.dialer.compat.AppCompatConstants; @@ -40,6 +41,7 @@ import com.android.dialer.compat.SdkVersionOverride; import com.android.dialer.phonenumbercache.CallLogQuery; import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.PermissionsUtil; +import com.android.voicemail.VoicemailComponent; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; @@ -126,13 +128,23 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { public void fetchVoicemailUnreadCount() { if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) { // Only count voicemails that have not been read and have not been deleted. + StringBuilder where = + new StringBuilder(Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0 "); + List<String> selectionArgs = new ArrayList<>(); + + if (BuildCompat.isAtLeastO()) { + VoicemailComponent.get(mContext) + .getVoicemailClient() + .appendOmtpVoicemailSelectionClause(mContext, where, selectionArgs); + } + startQuery( QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN, null, Voicemails.CONTENT_URI, new String[] {Voicemails._ID}, - Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0", - null, + where.toString(), + selectionArgs.toArray(new String[selectionArgs.size()]), null); } } @@ -168,6 +180,12 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { selectionArgs.add(Long.toString(newerThan)); } + if (callType == Calls.VOICEMAIL_TYPE) { + VoicemailComponent.get(mContext) + .getVoicemailClient() + .appendOmtpVoicemailSelectionClause(mContext, where, selectionArgs); + } + final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit; final String selection = where.length() > 0 ? where.toString() : null; Uri uri = diff --git a/java/com/android/dialer/debug/bindings/stub/DebugBindings.java b/java/com/android/dialer/debug/bindings/stub/DebugBindings.java new file mode 100644 index 000000000..7df38341d --- /dev/null +++ b/java/com/android/dialer/debug/bindings/stub/DebugBindings.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 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.debug.bindings; + +import android.content.Context; + +/** Hooks into the debug module. */ +public class DebugBindings { + + public static void registerConnectionService(Context context) {} + + public static void addNewIncomingCall(Context context, String phoneNumber) {} +} diff --git a/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java b/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java deleted file mode 100644 index 14299f92c..000000000 --- a/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2016 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.enrichedcall; - -import javax.annotation.Generated; - -@Generated("com.google.auto.value.processor.AutoValueProcessor") - final class AutoValue_EnrichedCallCapabilities extends EnrichedCallCapabilities { - - private final boolean supportsCallComposer; - private final boolean supportsPostCall; - - AutoValue_EnrichedCallCapabilities( - boolean supportsCallComposer, - boolean supportsPostCall) { - this.supportsCallComposer = supportsCallComposer; - this.supportsPostCall = supportsPostCall; - } - - @Override - public boolean supportsCallComposer() { - return supportsCallComposer; - } - - @Override - public boolean supportsPostCall() { - return supportsPostCall; - } - - @Override - public String toString() { - return "EnrichedCallCapabilities{" - + "supportsCallComposer=" + supportsCallComposer + ", " - + "supportsPostCall=" + supportsPostCall + ", " - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof EnrichedCallCapabilities) { - EnrichedCallCapabilities that = (EnrichedCallCapabilities) o; - return (this.supportsCallComposer == that.supportsCallComposer()) - && (this.supportsPostCall == that.supportsPostCall()); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= this.supportsCallComposer ? 1231 : 1237; - h *= 1000003; - h ^= this.supportsPostCall ? 1231 : 1237; - return h; - } - -} - diff --git a/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java b/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java deleted file mode 100644 index edfefc479..000000000 --- a/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2016 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.enrichedcall; - -import android.net.Uri; -import android.support.annotation.Nullable; -import javax.annotation.Generated; - -@Generated("com.google.auto.value.processor.AutoValueProcessor") - final class AutoValue_OutgoingCallComposerData extends OutgoingCallComposerData { - - private final String subject; - private final Uri imageUri; - private final String imageContentType; - - private AutoValue_OutgoingCallComposerData( - @Nullable String subject, - @Nullable Uri imageUri, - @Nullable String imageContentType) { - this.subject = subject; - this.imageUri = imageUri; - this.imageContentType = imageContentType; - } - - @Nullable - @Override - public String getSubject() { - return subject; - } - - @Nullable - @Override - public Uri getImageUri() { - return imageUri; - } - - @Nullable - @Override - public String getImageContentType() { - return imageContentType; - } - - @Override - public String toString() { - return "OutgoingCallComposerData{" - + "subject=" + subject + ", " - + "imageUri=" + imageUri + ", " - + "imageContentType=" + imageContentType - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof OutgoingCallComposerData) { - OutgoingCallComposerData that = (OutgoingCallComposerData) o; - return ((this.subject == null) ? (that.getSubject() == null) : this.subject.equals(that.getSubject())) - && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri())) - && ((this.imageContentType == null) ? (that.getImageContentType() == null) : this.imageContentType.equals(that.getImageContentType())); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= (subject == null) ? 0 : this.subject.hashCode(); - h *= 1000003; - h ^= (imageUri == null) ? 0 : this.imageUri.hashCode(); - h *= 1000003; - h ^= (imageContentType == null) ? 0 : this.imageContentType.hashCode(); - return h; - } - - static final class Builder extends OutgoingCallComposerData.Builder { - private String subject; - private Uri imageUri; - private String imageContentType; - Builder() { - } - private Builder(OutgoingCallComposerData source) { - this.subject = source.getSubject(); - this.imageUri = source.getImageUri(); - this.imageContentType = source.getImageContentType(); - } - @Override - public OutgoingCallComposerData.Builder setSubject(@Nullable String subject) { - this.subject = subject; - return this; - } - @Override - OutgoingCallComposerData.Builder setImageUri(@Nullable Uri imageUri) { - this.imageUri = imageUri; - return this; - } - @Override - OutgoingCallComposerData.Builder setImageContentType(@Nullable String imageContentType) { - this.imageContentType = imageContentType; - return this; - } - @Override - OutgoingCallComposerData autoBuild() { - return new AutoValue_OutgoingCallComposerData( - this.subject, - this.imageUri, - this.imageContentType); - } - } - -} diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java b/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java index b7d780950..c3c78c9c8 100644 --- a/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java +++ b/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java @@ -16,21 +16,24 @@ package com.android.dialer.enrichedcall; - +import com.google.auto.value.AutoValue; /** Value type holding enriched call capabilities. */ - +@AutoValue public abstract class EnrichedCallCapabilities { public static final EnrichedCallCapabilities NO_CAPABILITIES = - EnrichedCallCapabilities.create(false, false); + EnrichedCallCapabilities.create(false, false, false); public static EnrichedCallCapabilities create( - boolean supportsCallComposer, boolean supportsPostCall) { - return new AutoValue_EnrichedCallCapabilities(supportsCallComposer, supportsPostCall); + boolean supportsCallComposer, boolean supportsPostCall, boolean supportsVideoCall) { + return new AutoValue_EnrichedCallCapabilities( + supportsCallComposer, supportsPostCall, supportsVideoCall); } public abstract boolean supportsCallComposer(); public abstract boolean supportsPostCall(); + + public abstract boolean supportsVideoShare(); } diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java b/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java new file mode 100644 index 000000000..5291e292f --- /dev/null +++ b/java/com/android/dialer/enrichedcall/EnrichedCallComponent.java @@ -0,0 +1,48 @@ +/* + * 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.enrichedcall; + +import android.content.Context; +import android.support.annotation.NonNull; +import dagger.Subcomponent; +import com.android.dialer.enrichedcall.stub.EnrichedCallManagerStub; + +/** Subcomponent that can be used to access the enriched call implementation. */ +public class EnrichedCallComponent { + private static EnrichedCallComponent instance; + private EnrichedCallManager enrichedCallManager; + + @NonNull + public EnrichedCallManager getEnrichedCallManager() { + if (enrichedCallManager == null) { + enrichedCallManager = new EnrichedCallManagerStub(); + } + return enrichedCallManager; + } + + public static EnrichedCallComponent get(Context context) { + if (instance == null) { + instance = new EnrichedCallComponent(); + } + return instance; + } + + /** Used to refer to the root application component. */ + public interface HasComponent { + EnrichedCallComponent enrichedCallComponent(); + } +} diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java index 6af8c409a..a36b2cc0d 100644 --- a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java +++ b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java @@ -16,38 +16,25 @@ package com.android.dialer.enrichedcall; -import android.app.Application; import android.support.annotation.IntDef; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.android.dialer.common.Assert; +import android.support.annotation.WorkerThread; +import com.android.dialer.calldetails.nano.CallDetailsEntries; +import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry; +import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult; +import com.android.dialer.enrichedcall.videoshare.VideoShareListener; import com.android.dialer.multimedia.MultimediaData; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Map; /** Performs all enriched calling logic. */ public interface EnrichedCallManager { - /** Factory for {@link EnrichedCallManager}. */ - interface Factory { - EnrichedCallManager getEnrichedCallManager(); - } - - /** Accessor for {@link EnrichedCallManager}. */ - class Accessor { - - /** - * @throws IllegalArgumentException if application does not implement {@link - * EnrichedCallManager.Factory} - */ - @NonNull - public static EnrichedCallManager getInstance(@NonNull Application application) { - Assert.isNotNull(application); - - return ((EnrichedCallManager.Factory) application).getEnrichedCallManager(); - } - } + int POST_CALL_NOTE_MAX_CHAR = 60; /** Receives updates when enriched call capabilities are ready. */ interface CapabilitiesListener { @@ -148,6 +135,15 @@ public interface EnrichedCallManager { void endCallComposerSession(long sessionId); /** + * Sends a post call note to the given number. + * + * @throws IllegalArgumentException if message is longer than {@link #POST_CALL_NOTE_MAX_CHAR} + * characters + */ + @MainThread + void sendPostCallNote(@NonNull String number, @NonNull String message); + + /** * Called once the capabilities are available for a corresponding call to {@link * #requestCapabilities(String)}. * @@ -162,8 +158,8 @@ public interface EnrichedCallManager { interface StateChangedListener { /** - * Callback fired when state changes. Listeners should call {@link #getSession(String)} to - * retrieve the new state. + * Callback fired when state changes. Listeners should call {@link #getSession(long)} or {@link + * #getSession(String, String)} to retrieve the new state. */ void onEnrichedCallStateChanged(); } @@ -177,10 +173,10 @@ public interface EnrichedCallManager { @MainThread void registerStateChangedListener(@NonNull StateChangedListener listener); - /** Returns the {@link Session} for the given number, or {@code null} if no session exists. */ + /** Returns the {@link Session} for the given unique call id, falling back to the number. */ @MainThread @Nullable - Session getSession(@NonNull String number); + Session getSession(@NonNull String uniqueCallId, @NonNull String number); /** Returns the {@link Session} for the given sessionId, or {@code null} if no session exists. */ @MainThread @@ -188,6 +184,18 @@ public interface EnrichedCallManager { Session getSession(long sessionId); /** + * Returns a mapping of enriched call data for all of the given {@link CallDetailsEntries}. + * + * <p>The mapping is created by finding the HistoryResults whose timestamps occurred during or + * close after a CallDetailsEntry. A CallDetailsEntry can have multiple HistoryResults in the + * event that both a CallComposer message and PostCall message were sent for the same call. + */ + @WorkerThread + @NonNull + Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData( + @NonNull String number, @NonNull CallDetailsEntries entries); + + /** * Unregisters the given {@link StateChangedListener}. * * <p>As a result of this method, the listener will not receive updates when the state of enriched @@ -222,4 +230,77 @@ public interface EnrichedCallManager { */ @MainThread void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData); + + /** + * Called when post call data arrives for the given session. + * + * @throws IllegalStateException if there's no session for the given id + */ + @MainThread + void onIncomingPostCallData(long sessionId, @NonNull MultimediaData multimediaData); + + /** + * Registers the given {@link VideoShareListener}. + * + * <p>As a result of this method, the listener will receive updates when any video share state + * changes. + */ + @MainThread + void registerVideoShareListener(@NonNull VideoShareListener listener); + + /** + * Unregisters the given {@link VideoShareListener}. + * + * <p>As a result of this method, the listener will not receive updates when any video share state + * changes. + */ + @MainThread + void unregisterVideoShareListener(@NonNull VideoShareListener listener); + + /** Called when an incoming video share invite is received. */ + @MainThread + void onIncomingVideoShareInvite(long sessionId, @NonNull String number); + + /** + * Starts a video share session with the given remote number. + * + * @param number the remote number in any format + * @return the id for the started session, or {@link Session#NO_SESSION_ID} if the session fails + */ + @MainThread + long startVideoShareSession(@NonNull String number); + + /** + * Accepts a video share session invite. + * + * @param sessionId the session to accept + * @return whether or not accepting the session succeeded + */ + @MainThread + boolean acceptVideoShareSession(long sessionId); + + /** + * Retrieve the session id for an incoming video share invite. + * + * @param number the remote number in any format + * @return the id for the session invite, or {@link Session#NO_SESSION_ID} if there is no invite + */ + @MainThread + long getVideoShareInviteSessionId(@NonNull String number); + + /** + * Ends the given video share session. + * + * @param sessionId the id of the session to end + */ + @MainThread + void endVideoShareSession(long sessionId); + + /** + * Returns the {@link VideoShareSession} for the given sessionId, or {@code null} if no session + * exists. + */ + @MainThread + @Nullable + VideoShareSession getVideoShareSession(long sessionId); } diff --git a/java/com/android/dialer/enrichedcall/FuzzyPhoneNumberMatcher.java b/java/com/android/dialer/enrichedcall/FuzzyPhoneNumberMatcher.java new file mode 100644 index 000000000..f589f94a6 --- /dev/null +++ b/java/com/android/dialer/enrichedcall/FuzzyPhoneNumberMatcher.java @@ -0,0 +1,20 @@ +package com.android.dialer.enrichedcall; + +import android.support.annotation.NonNull; +import com.android.dialer.common.Assert; + +/** Utility for comparing phone numbers. */ +public class FuzzyPhoneNumberMatcher { + + /** Returns {@code true} if the given numbers can be interpreted to be the same. */ + public static boolean matches(@NonNull String a, @NonNull String b) { + String aNormalized = Assert.isNotNull(a).replaceAll("[^0-9]", ""); + String bNormalized = Assert.isNotNull(b).replaceAll("[^0-9]", ""); + if (aNormalized.length() < 7 || bNormalized.length() < 7) { + return false; + } + String aMatchable = aNormalized.substring(aNormalized.length() - 7); + String bMatchable = bNormalized.substring(bNormalized.length() - 7); + return aMatchable.equals(bMatchable); + } +} diff --git a/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java b/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java index a8ee49d4e..56145ddd4 100644 --- a/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java +++ b/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java @@ -20,7 +20,7 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.android.dialer.common.Assert; - +import com.google.auto.value.AutoValue; /** * Value type holding references to all data that could be provided for the call composer. @@ -29,19 +29,19 @@ import com.android.dialer.common.Assert; * * <pre> * OutgoingCallComposerData.builder.build(); // throws exception, no data set - * OutgoingCallComposerData - * .setSubject(subject) + * OutgoingCallComposerData.builder + * .setText(subject) * .build(); // Success - * OutgoingCallComposerData + * OutgoingCallComposerData.builder * .setImageData(uri, contentType) * .build(); // Success - * OutgoingCallComposerData - * .setSubject(subject) + * OutgoingCallComposerData.builder + * .setText(subject) * .setImageData(uri, contentType) * .build(); // Success * </pre> */ - +@AutoValue public abstract class OutgoingCallComposerData { public static Builder builder() { @@ -62,7 +62,7 @@ public abstract class OutgoingCallComposerData { public abstract String getImageContentType(); /** Builds instances of {@link OutgoingCallComposerData}. */ - + @AutoValue.Builder public abstract static class Builder { public abstract Builder setSubject(String subject); diff --git a/java/com/android/dialer/enrichedcall/Session.java b/java/com/android/dialer/enrichedcall/Session.java index b0439fae9..b3f291438 100644 --- a/java/com/android/dialer/enrichedcall/Session.java +++ b/java/com/android/dialer/enrichedcall/Session.java @@ -17,6 +17,7 @@ package com.android.dialer.enrichedcall; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.android.dialer.enrichedcall.EnrichedCallManager.State; import com.android.dialer.multimedia.MultimediaData; @@ -38,6 +39,12 @@ public interface Session { */ long getSessionId(); + /** Returns the id of the dialer call associated with this session, or null if there isn't one. */ + @Nullable + String getUniqueDialerCallId(); + + void setUniqueDialerCallId(@NonNull String id); + /** Returns the number associated with the remote end of this session. */ @NonNull String getRemoteNumber(); diff --git a/java/com/android/dialer/enrichedcall/VideoShareSession.java b/java/com/android/dialer/enrichedcall/VideoShareSession.java new file mode 100644 index 000000000..07bc4ed09 --- /dev/null +++ b/java/com/android/dialer/enrichedcall/VideoShareSession.java @@ -0,0 +1,20 @@ +/* + * 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.enrichedcall; + +/** Holds state information and data about video share sessions. */ +public interface VideoShareSession {} diff --git a/java/com/android/dialer/enrichedcall/historyquery/HistoryQuery.java b/java/com/android/dialer/enrichedcall/historyquery/HistoryQuery.java new file mode 100644 index 000000000..b7593cebb --- /dev/null +++ b/java/com/android/dialer/enrichedcall/historyquery/HistoryQuery.java @@ -0,0 +1,31 @@ +package com.android.dialer.enrichedcall.historyquery; + +import android.support.annotation.NonNull; +import com.android.dialer.common.LogUtil; +import com.google.auto.value.AutoValue; + +/** + * Data object representing the pieces of information required to query for historical enriched call + * data. + */ +@AutoValue +public abstract class HistoryQuery { + + @NonNull + public static HistoryQuery create(@NonNull String number, long callStartTime, long callEndTime) { + return new AutoValue_HistoryQuery(number, callStartTime, callEndTime); + } + + public abstract String getNumber(); + + public abstract long getCallStartTimestamp(); + + public abstract long getCallEndTimestamp(); + + @Override + public String toString() { + return String.format( + "HistoryQuery{number: %s, callStartTimestamp: %d, callEndTimestamp: %d}", + LogUtil.sanitizePhoneNumber(getNumber()), getCallStartTimestamp(), getCallEndTimestamp()); + } +} diff --git a/java/com/android/dialer/enrichedcall/historyquery/nano/HistoryResult.java b/java/com/android/dialer/enrichedcall/historyquery/nano/HistoryResult.java new file mode 100644 index 000000000..2fdc2da50 --- /dev/null +++ b/java/com/android/dialer/enrichedcall/historyquery/nano/HistoryResult.java @@ -0,0 +1,203 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! + +package com.android.dialer.enrichedcall.historyquery.proto.nano; + +/** This file is autogenerated, but javadoc required. */ +@SuppressWarnings("hiding") +public final class HistoryResult + extends com.google.protobuf.nano.ExtendableMessageNano<HistoryResult> { + + /** This file is autogenerated, but javadoc required. */ + // enum Type + public interface Type { + public static final int INCOMING_CALL_COMPOSER = 1; + public static final int OUTGOING_CALL_COMPOSER = 2; + public static final int INCOMING_POST_CALL = 3; + public static final int OUTGOING_POST_CALL = 4; + } + + private static volatile HistoryResult[] _emptyArray; + + public static HistoryResult[] emptyArray() { + // Lazily initializes the empty array + if (_emptyArray == null) { + synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { + if (_emptyArray == null) { + _emptyArray = new HistoryResult[0]; + } + } + } + return _emptyArray; + } + + // optional .com.android.dialer.enrichedcall.historyquery.proto.HistoryResult.Type type = 1; + public int type; + + // optional string text = 2; + public java.lang.String text; + + // optional string image_uri = 4; + public java.lang.String imageUri; + + // optional string image_content_type = 5; + public java.lang.String imageContentType; + + // optional int64 timestamp = 7; + public long timestamp; + + // @@protoc_insertion_point(class_scope:com.android.dialer.enrichedcall.historyquery.proto.HistoryResult) + + public HistoryResult() { + clear(); + } + + public HistoryResult clear() { + type = + com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type + .INCOMING_CALL_COMPOSER; + text = ""; + imageUri = ""; + imageContentType = ""; + timestamp = 0L; + unknownFieldData = null; + cachedSize = -1; + return this; + } + + @Override + public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output) + throws java.io.IOException { + if (this.type + != com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type + .INCOMING_CALL_COMPOSER) { + output.writeInt32(1, this.type); + } + if (this.text != null && !this.text.equals("")) { + output.writeString(2, this.text); + } + if (this.imageUri != null && !this.imageUri.equals("")) { + output.writeString(4, this.imageUri); + } + if (this.imageContentType != null && !this.imageContentType.equals("")) { + output.writeString(5, this.imageContentType); + } + if (this.timestamp != 0L) { + output.writeInt64(7, this.timestamp); + } + super.writeTo(output); + } + + @Override + protected int computeSerializedSize() { + int size = super.computeSerializedSize(); + if (this.type + != com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type + .INCOMING_CALL_COMPOSER) { + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(1, this.type); + } + if (this.text != null && !this.text.equals("")) { + size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(2, this.text); + } + if (this.imageUri != null && !this.imageUri.equals("")) { + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(4, this.imageUri); + } + if (this.imageContentType != null && !this.imageContentType.equals("")) { + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize( + 5, this.imageContentType); + } + if (this.timestamp != 0L) { + size += + com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt64Size(7, this.timestamp); + } + return size; + } + + @Override + public HistoryResult mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) + throws java.io.IOException { + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + return this; + default: + { + if (!super.storeUnknownField(input, tag)) { + return this; + } + break; + } + case 8: + { + int initialPos = input.getPosition(); + int value = input.readInt32(); + switch (value) { + case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type + .INCOMING_CALL_COMPOSER: + case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type + .OUTGOING_CALL_COMPOSER: + case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type + .INCOMING_POST_CALL: + case com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult.Type + .OUTGOING_POST_CALL: + this.type = value; + break; + default: + input.rewindToPosition(initialPos); + storeUnknownField(input, tag); + break; + } + break; + } + case 18: + { + this.text = input.readString(); + break; + } + case 34: + { + this.imageUri = input.readString(); + break; + } + case 42: + { + this.imageContentType = input.readString(); + break; + } + case 56: + { + this.timestamp = input.readInt64(); + break; + } + } + } + } + + public static HistoryResult parseFrom(byte[] data) + throws com.google.protobuf.nano.InvalidProtocolBufferNanoException { + return com.google.protobuf.nano.MessageNano.mergeFrom(new HistoryResult(), data); + } + + public static HistoryResult parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) + throws java.io.IOException { + return new HistoryResult().mergeFrom(input); + } +} diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java b/java/com/android/dialer/enrichedcall/stub/EnrichedCallManagerStub.java index db9a799d3..01d1f2aac 100644 --- a/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java +++ b/java/com/android/dialer/enrichedcall/stub/EnrichedCallManagerStub.java @@ -14,11 +14,24 @@ * limitations under the License */ -package com.android.dialer.enrichedcall; +package com.android.dialer.enrichedcall.stub; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.util.ArrayMap; +import com.android.dialer.calldetails.nano.CallDetailsEntries; +import com.android.dialer.calldetails.nano.CallDetailsEntries.CallDetailsEntry; +import com.android.dialer.common.Assert; +import com.android.dialer.enrichedcall.EnrichedCallCapabilities; +import com.android.dialer.enrichedcall.EnrichedCallManager; +import com.android.dialer.enrichedcall.Session; +import com.android.dialer.enrichedcall.VideoShareSession; +import com.android.dialer.enrichedcall.historyquery.proto.nano.HistoryResult; +import com.android.dialer.enrichedcall.videoshare.VideoShareListener; import com.android.dialer.multimedia.MultimediaData; +import java.util.List; +import java.util.Map; /** Stub implementation of {@link EnrichedCallManager}. */ public final class EnrichedCallManagerStub implements EnrichedCallManager { @@ -52,6 +65,9 @@ public final class EnrichedCallManagerStub implements EnrichedCallManager { public void endCallComposerSession(long sessionId) {} @Override + public void sendPostCallNote(@NonNull String number, @NonNull String message) {} + + @Override public void onCapabilitiesReceived( @NonNull String number, @NonNull EnrichedCallCapabilities capabilities) {} @@ -60,7 +76,7 @@ public final class EnrichedCallManagerStub implements EnrichedCallManager { @Nullable @Override - public Session getSession(@NonNull String number) { + public Session getSession(@NonNull String uniqueCallId, @NonNull String number) { return null; } @@ -70,6 +86,15 @@ public final class EnrichedCallManagerStub implements EnrichedCallManager { return null; } + @NonNull + @Override + @WorkerThread + public Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData( + @NonNull String number, @NonNull CallDetailsEntries entries) { + Assert.isWorkerThread(); + return new ArrayMap<>(); + } + @Override public void unregisterStateChangedListener(@NonNull StateChangedListener listener) {} @@ -81,4 +106,40 @@ public final class EnrichedCallManagerStub implements EnrichedCallManager { @Override public void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData) {} + + @Override + public void onIncomingPostCallData(long sessionId, @NonNull MultimediaData multimediaData) {} + + @Override + public void registerVideoShareListener(@NonNull VideoShareListener listener) {} + + @Override + public void unregisterVideoShareListener(@NonNull VideoShareListener listener) {} + + @Override + public void onIncomingVideoShareInvite(long sessionId, @NonNull String number) {} + + @Override + public long startVideoShareSession(String number) { + return Session.NO_SESSION_ID; + } + + @Override + public boolean acceptVideoShareSession(long sessionId) { + return false; + } + + @Override + public long getVideoShareInviteSessionId(@NonNull String number) { + return Session.NO_SESSION_ID; + } + + @Override + public void endVideoShareSession(long sessionId) {} + + @Nullable + @Override + public VideoShareSession getVideoShareSession(long sessionId) { + return null; + } } diff --git a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java b/java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java index 39c55d040..0ec72111e 100644 --- a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java +++ b/java/com/android/dialer/enrichedcall/stub/StubEnrichedCallModule.java @@ -14,8 +14,9 @@ * limitations under the License */ -package com.android.dialer.enrichedcall; +package com.android.dialer.enrichedcall.stub; +import com.android.dialer.enrichedcall.EnrichedCallManager; import dagger.Module; import dagger.Provides; import javax.inject.Singleton; @@ -29,4 +30,6 @@ public class StubEnrichedCallModule { static EnrichedCallManager provideEnrichedCallManager() { return new EnrichedCallManagerStub(); } + + private StubEnrichedCallModule() {} } diff --git a/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java b/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java new file mode 100644 index 000000000..bcc387a3f --- /dev/null +++ b/java/com/android/dialer/enrichedcall/videoshare/VideoShareListener.java @@ -0,0 +1,14 @@ +package com.android.dialer.enrichedcall.videoshare; + +import android.support.annotation.MainThread; + +/** Receives updates when video share status has changed. */ +public interface VideoShareListener { + + /** + * Callback fired when video share has changed (service connected / disconnected, video share + * invite received or canceled, or when a session changes). + */ + @MainThread + void onVideoShareChanged(); +} diff --git a/java/com/android/dialer/inject/ApplicationModule.java b/java/com/android/dialer/inject/ContextModule.java index 99e5296ea..aa83f0105 100644 --- a/java/com/android/dialer/inject/ApplicationModule.java +++ b/java/com/android/dialer/inject/ContextModule.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -16,24 +16,24 @@ package com.android.dialer.inject; -import android.app.Application; +import android.content.Context; import android.support.annotation.NonNull; import com.android.dialer.common.Assert; import dagger.Module; import dagger.Provides; -/** Provides the singleton application object. */ +/** Provides the singleton context object. */ @Module -public final class ApplicationModule { +public final class ContextModule { - @NonNull private final Application application; + @NonNull private final Context context; - public ApplicationModule(@NonNull Application application) { - this.application = Assert.isNotNull(application); + public ContextModule(@NonNull Context context) { + this.context = Assert.isNotNull(context); } @Provides - Application provideApplication() { - return application; + Context provideContext() { + return context; } } diff --git a/java/com/android/dialer/inject/DialerAppComponent.java b/java/com/android/dialer/inject/HasRootComponent.java index 9832ce804..0802b806a 100644 --- a/java/com/android/dialer/inject/DialerAppComponent.java +++ b/java/com/android/dialer/inject/HasRootComponent.java @@ -16,14 +16,10 @@ package com.android.dialer.inject; -import com.android.dialer.enrichedcall.EnrichedCallManager; -import com.android.dialer.enrichedcall.StubEnrichedCallModule; -import dagger.Component; -import javax.inject.Singleton; - -/** Core application-wide {@link Component} for the open source dialer app. */ -@Singleton -@Component(modules = {ApplicationModule.class, StubEnrichedCallModule.class}) -public interface DialerAppComponent { - EnrichedCallManager enrichedCallManager(); +/** + * Used by packages to access the root component from the Application without creating a dependency + * cycle. + */ +public interface HasRootComponent { + Object component(); } diff --git a/java/com/android/dialer/interactions/PhoneNumberInteraction.java b/java/com/android/dialer/interactions/PhoneNumberInteraction.java index f36e5319c..b797629dc 100644 --- a/java/com/android/dialer/interactions/PhoneNumberInteraction.java +++ b/java/com/android/dialer/interactions/PhoneNumberInteraction.java @@ -81,8 +81,10 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> { private static final String TAG = PhoneNumberInteraction.class.getSimpleName(); /** The identifier for a permissions request if one is generated. */ public static final int REQUEST_READ_CONTACTS = 1; + public static final int REQUEST_CALL_PHONE = 2; - private static final String[] PHONE_NUMBER_PROJECTION = + @VisibleForTesting + public static final String[] PHONE_NUMBER_PROJECTION = new String[] { Phone._ID, Phone.NUMBER, @@ -191,13 +193,14 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> { * numbers have been queried for. The activity must implement {@link InteractionErrorListener} * and {@link DisambigDialogDismissedListener}. * @param isVideoCall {@code true} if the call is a video call, {@code false} otherwise. + * @return true if the necessary permissions were found to start the interaction, false otherwise */ - public static void startInteractionForPhoneCall( + public static boolean startInteractionForPhoneCall( TransactionSafeActivity activity, Uri uri, boolean isVideoCall, CallSpecificAppData callSpecificAppData) { - new PhoneNumberInteraction( + return new PhoneNumberInteraction( activity, ContactDisplayUtils.INTERACTION_CALL, isVideoCall, callSpecificAppData) .startInteraction(uri); } @@ -211,11 +214,19 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> { * Initiates the interaction to result in either a phone call or sms message for a contact. * * @param uri Contact Uri + * @return true if the necessary permissions were found to start the interaction, false otherwise */ - private void startInteraction(Uri uri) { - // It's possible for a shortcut to have been created, and then Contacts 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. + private boolean startInteraction(Uri uri) { + // 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. + if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.CALL_PHONE) + != PackageManager.PERMISSION_GRANTED) { + LogUtil.i("PhoneNumberInteraction.startInteraction", "No phone permissions"); + ActivityCompat.requestPermissions( + (Activity) mContext, new String[] {Manifest.permission.CALL_PHONE}, REQUEST_CALL_PHONE); + return false; + } if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { LogUtil.i("PhoneNumberInteraction.startInteraction", "No contact permissions"); @@ -223,7 +234,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> { (Activity) mContext, new String[] {Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS); - return; + return false; } if (mLoader != null) { @@ -249,6 +260,7 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> { mContext, queryUri, PHONE_NUMBER_PROJECTION, PHONE_NUMBER_SELECTION, null, null); mLoader.registerListener(0, this); mLoader.startLoading(); + return true; } @Override @@ -457,8 +469,8 @@ public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> { * will be chosen to make a call or initiate an sms message. * * <p>It is recommended to use {@link #startInteractionForPhoneCall(TransactionSafeActivity, Uri, - * boolean, int)} instead of directly using this class, as those methods handle one or multiple - * data cases appropriately. + * boolean, CallSpecificAppData)} instead of directly using this class, as those methods handle + * one or multiple data cases appropriately. * * <p>This fragment may only be attached to activities which implement {@link * DisambigDialogDismissedListener}. diff --git a/java/com/android/dialer/logging/nano/ContactLookupResult.java b/java/com/android/dialer/logging/nano/ContactLookupResult.java index 8960560fb..93f5f0135 100644 --- a/java/com/android/dialer/logging/nano/ContactLookupResult.java +++ b/java/com/android/dialer/logging/nano/ContactLookupResult.java @@ -11,17 +11,19 @@ * 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 + * limitations under the License. */ // Generated by the protocol buffer compiler. DO NOT EDIT! package com.android.dialer.logging.nano; +/** This file is autogenerated, but javadoc required. */ @SuppressWarnings("hiding") -public final class ContactLookupResult extends - com.google.protobuf.nano.ExtendableMessageNano<ContactLookupResult> { +public final class ContactLookupResult + extends com.google.protobuf.nano.ExtendableMessageNano<ContactLookupResult> { + /** This file is autogenerated, but javadoc required. */ // enum Type public interface Type { public static final int UNKNOWN_LOOKUP_RESULT_TYPE = 0; @@ -34,11 +36,11 @@ public final class ContactLookupResult extends } private static volatile ContactLookupResult[] _emptyArray; + public static ContactLookupResult[] emptyArray() { // Lazily initializes the empty array if (_emptyArray == null) { - synchronized ( - com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { + synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { if (_emptyArray == null) { _emptyArray = new ContactLookupResult[0]; } @@ -60,20 +62,20 @@ public final class ContactLookupResult extends } @Override - public ContactLookupResult mergeFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) + public ContactLookupResult mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { while (true) { int tag = input.readTag(); switch (tag) { case 0: return this; - default: { - if (!super.storeUnknownField(input, tag)) { - return this; + default: + { + if (!super.storeUnknownField(input, tag)) { + return this; + } + break; } - break; - } } } } @@ -84,8 +86,7 @@ public final class ContactLookupResult extends } public static ContactLookupResult parseFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) - throws java.io.IOException { + com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { return new ContactLookupResult().mergeFrom(input); } } diff --git a/java/com/android/dialer/logging/nano/ContactSource.java b/java/com/android/dialer/logging/nano/ContactSource.java index 35d8b8ca1..dbe40cd53 100644 --- a/java/com/android/dialer/logging/nano/ContactSource.java +++ b/java/com/android/dialer/logging/nano/ContactSource.java @@ -11,17 +11,19 @@ * 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 + * limitations under the License. */ // Generated by the protocol buffer compiler. DO NOT EDIT! package com.android.dialer.logging.nano; +/** This file is autogenerated, but javadoc required. */ @SuppressWarnings("hiding") -public final class ContactSource extends - com.google.protobuf.nano.ExtendableMessageNano<ContactSource> { +public final class ContactSource + extends com.google.protobuf.nano.ExtendableMessageNano<ContactSource> { + /** This file is autogenerated, but javadoc required. */ // enum Type public interface Type { public static final int UNKNOWN_SOURCE_TYPE = 0; @@ -33,11 +35,11 @@ public final class ContactSource extends } private static volatile ContactSource[] _emptyArray; + public static ContactSource[] emptyArray() { // Lazily initializes the empty array if (_emptyArray == null) { - synchronized ( - com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { + synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { if (_emptyArray == null) { _emptyArray = new ContactSource[0]; } @@ -59,20 +61,20 @@ public final class ContactSource extends } @Override - public ContactSource mergeFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) + public ContactSource mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { while (true) { int tag = input.readTag(); switch (tag) { case 0: return this; - default: { - if (!super.storeUnknownField(input, tag)) { - return this; + default: + { + if (!super.storeUnknownField(input, tag)) { + return this; + } + break; } - break; - } } } } @@ -82,8 +84,7 @@ public final class ContactSource extends return com.google.protobuf.nano.MessageNano.mergeFrom(new ContactSource(), data); } - public static ContactSource parseFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) + public static ContactSource parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { return new ContactSource().mergeFrom(input); } diff --git a/java/com/android/dialer/logging/nano/DialerImpression.java b/java/com/android/dialer/logging/nano/DialerImpression.java index 6bb56751f..80a006b55 100644 --- a/java/com/android/dialer/logging/nano/DialerImpression.java +++ b/java/com/android/dialer/logging/nano/DialerImpression.java @@ -14,12 +14,16 @@ * limitations under the License. */ +// Generated by the protocol buffer compiler. DO NOT EDIT! + package com.android.dialer.logging.nano; +/** This file is autogenerated, but javadoc required. */ @SuppressWarnings("hiding") -public final class DialerImpression extends - com.google.protobuf.nano.ExtendableMessageNano<DialerImpression> { +public final class DialerImpression + extends com.google.protobuf.nano.ExtendableMessageNano<DialerImpression> { + /** This file is autogenerated, but javadoc required. */ // enum Type public interface Type { public static final int UNKNOWN_AOSP_EVENT_TYPE = 1000; @@ -33,7 +37,8 @@ public final class DialerImpression extends public static final int DIALOG_ACTION_CONFIRM_NUMBER_NOT_SPAM = 1008; public static final int REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER = 1009; public static final int DIALOG_ACTION_CONFIRM_NUMBER_SPAM_INDIRECTLY_VIA_BLOCK_NUMBER = 1010; - public static final int REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = 1011; + public static final int + REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = 1011; public static final int USER_ACTION_BLOCKED_NUMBER = 1012; public static final int USER_ACTION_UNBLOCKED_NUMBER = 1013; public static final int SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER = 1014; @@ -41,7 +46,8 @@ public final class DialerImpression extends public static final int SPAM_AFTER_CALL_NOTIFICATION_SHOW_NON_SPAM_DIALOG = 1016; public static final int SPAM_AFTER_CALL_NOTIFICATION_ADD_TO_CONTACTS = 1019; public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM = 1020; - public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_NOT_SPAM_AND_BLOCKED = 1021; + public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_NOT_SPAM_AND_BLOCKED = + 1021; public static final int SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM = 1022; public static final int SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_SPAM_DIALOG = 1024; public static final int SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_NON_SPAM_DIALOG = 1025; @@ -89,7 +95,8 @@ public final class DialerImpression extends public static final int STORAGE_PERMISSION_DENIED = 1073; public static final int CAMERA_PERMISSION_DENIED = 1078; public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_ACTIVITY = 1079; - public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION = 1080; + public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION = + 1080; public static final int BACKUP_ON_BACKUP = 1081; public static final int BACKUP_ON_FULL_BACKUP = 1082; public static final int BACKUP_ON_BACKUP_DISABLED = 1083; @@ -116,15 +123,43 @@ public final class DialerImpression extends public static final int BACKUP_ON_RESTORE_VM_DUPLICATE_NOT_RESTORING = 1104; public static final int CALL_LOG_SHARE_AND_CALL = 1105; public static final int CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL = 1106; - public static final int CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY = 1107; + public static final int CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY = + 1107; + public static final int POST_CALL_PROMPT_USER_TO_SEND_MESSAGE_CLICKED = 1108; + public static final int POST_CALL_PROMPT_USER_TO_SEND_MESSAGE = 1109; + public static final int POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE = 1110; + public static final int POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE_CLICKED = 1111; + public static final int IN_CALL_SCREEN_TURN_ON_MUTE = 1112; + public static final int IN_CALL_SCREEN_TURN_OFF_MUTE = 1113; + public static final int IN_CALL_SCREEN_SWAP_CAMERA = 1114; + public static final int IN_CALL_SCREEN_TURN_ON_VIDEO = 1115; + public static final int IN_CALL_SCREEN_TURN_OFF_VIDEO = 1116; + public static final int VIDEO_CALL_WITH_INCOMING_VOICE_CALL = 1117; + public static final int VIDEO_CALL_WITH_INCOMING_VIDEO_CALL = 1118; + public static final int VOICE_CALL_WITH_INCOMING_VOICE_CALL = 1119; + public static final int VOICE_CALL_WITH_INCOMING_VIDEO_CALL = 1120; + public static final int CALL_DETAILS_COPY_NUMBER = 1121; + public static final int CALL_DETAILS_EDIT_BEFORE_CALL = 1122; + public static final int CALL_DETAILS_CALL_BACK = 1123; + public static final int VVM_USER_DISMISSED_VM_ALMOST_FULL_PROMO = 1124; + public static final int VVM_USER_DISMISSED_VM_FULL_PROMO = 1125; + public static final int VVM_USER_ENABLED_ARCHIVE_FROM_VM_ALMOST_FULL_PROMO = 1126; + public static final int VVM_USER_ENABLED_ARCHIVE_FROM_VM_FULL_PROMO = 1127; + public static final int VVM_USER_SHOWN_VM_ALMOST_FULL_PROMO = 1128; + public static final int VVM_USER_SHOWN_VM_FULL_PROMO = 1129; + public static final int VVM_USER_SHOWN_VM_ALMOST_FULL_ERROR_MESSAGE = 1130; + public static final int VVM_USER_SHOWN_VM_FULL_ERROR_MESSAGE = 1131; + public static final int VVM_USER_TURNED_ARCHIVE_ON_FROM_SETTINGS = 1132; + public static final int VVM_USER_TURNED_ARCHIVE_OFF_FROM_SETTINGS = 1133; + public static final int VVM_ARCHIVE_AUTO_DELETED_VM_FROM_SERVER = 1134; + public static final int VVM_ARCHIVE_AUTO_DELETE_TURNED_OFF = 1135; } private static volatile DialerImpression[] _emptyArray; public static DialerImpression[] emptyArray() { // Lazily initializes the empty array if (_emptyArray == null) { - synchronized ( - com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { + synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { if (_emptyArray == null) { _emptyArray = new DialerImpression[0]; } @@ -146,20 +181,20 @@ public final class DialerImpression extends } @Override - public DialerImpression mergeFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) + public DialerImpression mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { while (true) { int tag = input.readTag(); switch (tag) { case 0: return this; - default: { - if (!super.storeUnknownField(input, tag)) { - return this; + default: + { + if (!super.storeUnknownField(input, tag)) { + return this; + } + break; } - break; - } } } } @@ -169,10 +204,8 @@ public final class DialerImpression extends return com.google.protobuf.nano.MessageNano.mergeFrom(new DialerImpression(), data); } - public static DialerImpression parseFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) + public static DialerImpression parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { return new DialerImpression().mergeFrom(input); } } - diff --git a/java/com/android/dialer/logging/nano/InteractionEvent.java b/java/com/android/dialer/logging/nano/InteractionEvent.java index 8d9430be9..7ca95fa45 100644 --- a/java/com/android/dialer/logging/nano/InteractionEvent.java +++ b/java/com/android/dialer/logging/nano/InteractionEvent.java @@ -23,8 +23,8 @@ package com.android.dialer.logging.nano; public final class InteractionEvent extends com.google.protobuf.nano.ExtendableMessageNano<InteractionEvent> { - // enum Type /** This file is autogenerated, but javadoc required. */ + // enum Type public interface Type { public static final int UNKNOWN = 0; public static final int CALL_BLOCKED = 15; diff --git a/java/com/android/dialer/logging/nano/ReportingLocation.java b/java/com/android/dialer/logging/nano/ReportingLocation.java index 1f05ce414..08ee04e7e 100644 --- a/java/com/android/dialer/logging/nano/ReportingLocation.java +++ b/java/com/android/dialer/logging/nano/ReportingLocation.java @@ -11,17 +11,19 @@ * 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 + * limitations under the License. */ // Generated by the protocol buffer compiler. DO NOT EDIT! package com.android.dialer.logging.nano; +/** This file is autogenerated, but javadoc required. */ @SuppressWarnings("hiding") -public final class ReportingLocation extends - com.google.protobuf.nano.ExtendableMessageNano<ReportingLocation> { +public final class ReportingLocation + extends com.google.protobuf.nano.ExtendableMessageNano<ReportingLocation> { + /** This file is autogenerated, but javadoc required. */ // enum Type public interface Type { public static final int UNKNOWN_REPORTING_LOCATION = 0; @@ -30,11 +32,11 @@ public final class ReportingLocation extends } private static volatile ReportingLocation[] _emptyArray; + public static ReportingLocation[] emptyArray() { // Lazily initializes the empty array if (_emptyArray == null) { - synchronized ( - com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { + synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) { if (_emptyArray == null) { _emptyArray = new ReportingLocation[0]; } @@ -56,20 +58,20 @@ public final class ReportingLocation extends } @Override - public ReportingLocation mergeFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) + public ReportingLocation mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { while (true) { int tag = input.readTag(); switch (tag) { case 0: return this; - default: { - if (!super.storeUnknownField(input, tag)) { - return this; + default: + { + if (!super.storeUnknownField(input, tag)) { + return this; + } + break; } - break; - } } } } @@ -79,8 +81,7 @@ public final class ReportingLocation extends return com.google.protobuf.nano.MessageNano.mergeFrom(new ReportingLocation(), data); } - public static ReportingLocation parseFrom( - com.google.protobuf.nano.CodedInputByteBufferNano input) + public static ReportingLocation parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException { return new ReportingLocation().mergeFrom(input); } diff --git a/java/com/android/dialer/logging/nano/ScreenEvent.java b/java/com/android/dialer/logging/nano/ScreenEvent.java index be4e5eb9e..bd5b817e1 100644 --- a/java/com/android/dialer/logging/nano/ScreenEvent.java +++ b/java/com/android/dialer/logging/nano/ScreenEvent.java @@ -22,8 +22,8 @@ package com.android.dialer.logging.nano; @SuppressWarnings("hiding") public final class ScreenEvent extends com.google.protobuf.nano.ExtendableMessageNano<ScreenEvent> { - // enum Type /** This file is autogenerated, but javadoc required. */ + // enum Type public interface Type { public static final int UNKNOWN = 0; public static final int DIALPAD = 1; diff --git a/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java b/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java deleted file mode 100644 index cc6815094..000000000 --- a/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java +++ /dev/null @@ -1,165 +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.multimedia; - -import android.location.Location; -import android.net.Uri; -import android.support.annotation.Nullable; -import javax.annotation.Generated; - -@Generated("com.google.auto.value.processor.AutoValueProcessor") - final class AutoValue_MultimediaData extends MultimediaData { - - private final String subject; - private final Location location; - private final Uri imageUri; - private final String imageContentType; - private final boolean important; - - private AutoValue_MultimediaData( - @Nullable String subject, - @Nullable Location location, - @Nullable Uri imageUri, - @Nullable String imageContentType, - boolean important) { - this.subject = subject; - this.location = location; - this.imageUri = imageUri; - this.imageContentType = imageContentType; - this.important = important; - } - - @Nullable - @Override - public String getSubject() { - return subject; - } - - @Nullable - @Override - public Location getLocation() { - return location; - } - - @Nullable - @Override - public Uri getImageUri() { - return imageUri; - } - - @Nullable - @Override - public String getImageContentType() { - return imageContentType; - } - - @Override - public boolean isImportant() { - return important; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof MultimediaData) { - MultimediaData that = (MultimediaData) o; - return ((this.subject == null) ? (that.getSubject() == null) : this.subject.equals(that.getSubject())) - && ((this.location == null) ? (that.getLocation() == null) : this.location.equals(that.getLocation())) - && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri())) - && ((this.imageContentType == null) ? (that.getImageContentType() == null) : this.imageContentType.equals(that.getImageContentType())) - && (this.important == that.isImportant()); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= (subject == null) ? 0 : this.subject.hashCode(); - h *= 1000003; - h ^= (location == null) ? 0 : this.location.hashCode(); - h *= 1000003; - h ^= (imageUri == null) ? 0 : this.imageUri.hashCode(); - h *= 1000003; - h ^= (imageContentType == null) ? 0 : this.imageContentType.hashCode(); - h *= 1000003; - h ^= this.important ? 1231 : 1237; - return h; - } - - static final class Builder extends MultimediaData.Builder { - private String subject; - private Location location; - private Uri imageUri; - private String imageContentType; - private Boolean important; - Builder() { - } - private Builder(MultimediaData source) { - this.subject = source.getSubject(); - this.location = source.getLocation(); - this.imageUri = source.getImageUri(); - this.imageContentType = source.getImageContentType(); - this.important = source.isImportant(); - } - @Override - public MultimediaData.Builder setSubject(@Nullable String subject) { - this.subject = subject; - return this; - } - @Override - public MultimediaData.Builder setLocation(@Nullable Location location) { - this.location = location; - return this; - } - @Override - MultimediaData.Builder setImageUri(@Nullable Uri imageUri) { - this.imageUri = imageUri; - return this; - } - @Override - MultimediaData.Builder setImageContentType(@Nullable String imageContentType) { - this.imageContentType = imageContentType; - return this; - } - @Override - public MultimediaData.Builder setImportant(boolean important) { - this.important = important; - return this; - } - @Override - public MultimediaData build() { - String missing = ""; - if (this.important == null) { - missing += " important"; - } - if (!missing.isEmpty()) { - throw new IllegalStateException("Missing required properties:" + missing); - } - return new AutoValue_MultimediaData( - this.subject, - this.location, - this.imageUri, - this.imageContentType, - this.important); - } - } - -} diff --git a/java/com/android/dialer/multimedia/MultimediaData.java b/java/com/android/dialer/multimedia/MultimediaData.java index ebd41a918..22bb7641c 100644 --- a/java/com/android/dialer/multimedia/MultimediaData.java +++ b/java/com/android/dialer/multimedia/MultimediaData.java @@ -21,10 +21,10 @@ import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.android.dialer.common.LogUtil; +import com.google.auto.value.AutoValue; - -/** Holds the data associated with an enriched call session. */ - +/** Holds data associated with a call. */ +@AutoValue public abstract class MultimediaData { public static final MultimediaData EMPTY = builder().build(); @@ -34,32 +34,33 @@ public abstract class MultimediaData { return new AutoValue_MultimediaData.Builder().setImportant(false); } - /** Returns the call composer subject if set, or null if this isn't a call composer session. */ + /** + * Returns the text part of this data. + * + * <p>This field is used for both the call composer session and the post call note. + */ @Nullable - public abstract String getSubject(); + public abstract String getText(); - /** Returns the call composer location if set, or null if this isn't a call composer session. */ + /** Returns the location part of this data. */ @Nullable public abstract Location getLocation(); - /** Returns {@code true} if this session contains image data. */ + /** Returns {@code true} if this object contains image data. */ public boolean hasImageData() { // imageUri and content are always either both null or nonnull return getImageUri() != null && getImageContentType() != null; } - /** Returns the call composer photo if set, or null if this isn't a call composer session. */ + /** Returns the image uri part of this object's image. */ @Nullable public abstract Uri getImageUri(); - /** - * Returns the content type of the image, either image/png or image/jpeg, if set, or null if this - * isn't a call composer session. - */ + /** Returns the content type part of this object's image, either image/png or image/jpeg. */ @Nullable public abstract String getImageContentType(); - /** Returns {@code true} if this is a call composer session that's marked as important. */ + /** Returns {@code true} if this data is marked as important. */ public abstract boolean isImportant(); /** Returns the string form of this MultimediaData with no PII. */ @@ -68,7 +69,7 @@ public abstract class MultimediaData { return String.format( "MultimediaData{subject: %s, location: %s, imageUrl: %s, imageContentType: %s, " + "important: %b}", - LogUtil.sanitizePii(getSubject()), + LogUtil.sanitizePii(getText()), LogUtil.sanitizePii(getLocation()), LogUtil.sanitizePii(getImageUri()), getImageContentType(), @@ -76,10 +77,10 @@ public abstract class MultimediaData { } /** Creates instances of {@link MultimediaData}. */ - + @AutoValue.Builder public abstract static class Builder { - public abstract Builder setSubject(@NonNull String subject); + public abstract Builder setText(@NonNull String subject); public abstract Builder setLocation(@NonNull Location location); diff --git a/java/com/android/dialer/notification/AndroidManifest.xml b/java/com/android/dialer/notification/AndroidManifest.xml new file mode 100644 index 000000000..c5484f263 --- /dev/null +++ b/java/com/android/dialer/notification/AndroidManifest.xml @@ -0,0 +1,21 @@ +<!-- + ~ 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 + package="com.android.dialer.notification" + xmlns:android="http://schemas.android.com/apk/res/android"> + <uses-sdk android:minSdkVersion="23" /> +</manifest> diff --git a/java/com/android/dialer/notification/GroupedNotificationUtil.java b/java/com/android/dialer/notification/GroupedNotificationUtil.java new file mode 100644 index 000000000..63ea51739 --- /dev/null +++ b/java/com/android/dialer/notification/GroupedNotificationUtil.java @@ -0,0 +1,66 @@ +/* + * 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.notification; + +import android.app.NotificationManager; +import android.service.notification.StatusBarNotification; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import java.util.Objects; + +/** Utilities for dealing with grouped notifications */ +public final class GroupedNotificationUtil { + + /** + * Remove notification(s) that were added as part of a group. Will ensure that if this is the last + * notification in the group the summary will be removed. + * + * @param tag String tag as included in {@link NotificationManager#notify(String, int, + * android.app.Notification)}. If null will remove all notifications under id + * @param id notification id as included with {@link NotificationManager#notify(String, int, + * android.app.Notification)}. + * @param summaryTag String tag of the summary notification + */ + public static void removeNotification( + @NonNull NotificationManager notificationManager, + @Nullable String tag, + int id, + @NonNull String summaryTag) { + if (tag == null) { + // Clear all missed call notifications + for (StatusBarNotification notification : notificationManager.getActiveNotifications()) { + if (notification.getId() == id) { + notificationManager.cancel(notification.getTag(), id); + } + } + } else { + notificationManager.cancel(tag, id); + + // See if other non-summary missed call notifications exist, and if not then clear the summary + boolean clearSummary = true; + for (StatusBarNotification notification : notificationManager.getActiveNotifications()) { + if (notification.getId() == id && !Objects.equals(summaryTag, notification.getTag())) { + clearSummary = false; + break; + } + } + if (clearSummary) { + notificationManager.cancel(summaryTag, id); + } + } + } +} diff --git a/java/com/android/dialer/notification/NotificationChannelManager.java b/java/com/android/dialer/notification/NotificationChannelManager.java new file mode 100644 index 000000000..9ff57321e --- /dev/null +++ b/java/com/android/dialer/notification/NotificationChannelManager.java @@ -0,0 +1,232 @@ +/* + * 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.notification; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; +import android.content.Context; +import android.media.AudioAttributes; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringDef; +import android.support.v4.os.BuildCompat; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telephony.TelephonyManager; +import com.android.contacts.common.compat.TelephonyManagerCompat; +import com.android.dialer.buildtype.BuildType; +import com.android.dialer.common.LogUtil; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Contains info on how to create {@link NotificationChannel NotificationChannels} */ +public class NotificationChannelManager { + + private static NotificationChannelManager instance; + + public static NotificationChannelManager getInstance() { + if (instance == null) { + instance = new NotificationChannelManager(); + } + return instance; + } + + /** + * Set the channel of notification appropriately. Will create the channel if it does not already + * exist. Safe to call pre-O (will no-op). + * + * <p>phoneAccount should only be null if channelName is {@link Channel#MISC}. + */ + public static void applyChannel( + @NonNull Notification.Builder notification, + @NonNull Context context, + @Channel String channelName, + @Nullable PhoneAccountHandle phoneAccount) { + if (phoneAccount == null) { + if (!Channel.MISC.equals(channelName)) { + IllegalArgumentException exception = + new IllegalArgumentException( + "Phone account handle must not be null unless on Channel.MISC"); + if (BuildType.get() >= BuildType.RELEASE) { + LogUtil.e("NotificationChannelManager.applyChannel", null, exception); + } else { + throw exception; + } + } + } + + if (BuildCompat.isAtLeastO()) { + NotificationChannel channel = + NotificationChannelManager.getInstance().getChannel(context, channelName, phoneAccount); + notification.setChannel(channel.getId()); + } + } + + /** The base Channel IDs for {@link NotificationChannel} */ + @Retention(RetentionPolicy.SOURCE) + @StringDef({ + Channel.INCOMING_CALL, + Channel.ONGOING_CALL, + Channel.MISSED_CALL, + Channel.VOICEMAIL, + Channel.EXTERNAL_CALL, + Channel.MISC + }) + public @interface Channel { + String INCOMING_CALL = "incomingCall"; + String ONGOING_CALL = "ongoingCall"; + String MISSED_CALL = "missedCall"; + String VOICEMAIL = "voicemail"; + String EXTERNAL_CALL = "externalCall"; + String MISC = "miscellaneous"; + } + + private NotificationChannelManager() {} + + private NotificationChannel getChannel( + @NonNull Context context, + @Channel String channelName, + @Nullable PhoneAccountHandle phoneAccount) { + String channelId = channelNameToId(channelName, phoneAccount); + NotificationChannel channel = getNotificationManager(context).getNotificationChannel(channelId); + if (channel == null) { + channel = createChannel(context, channelName, phoneAccount); + } + return channel; + } + + private static String channelNameToId( + @Channel String name, @Nullable PhoneAccountHandle phoneAccountHandle) { + if (phoneAccountHandle == null) { + return name; + } else { + return name + ":" + phoneAccountHandle.getId(); + } + } + + private NotificationChannel createChannel( + Context context, + @Channel String channelName, + @Nullable PhoneAccountHandle phoneAccountHandle) { + String channelId = channelNameToId(channelName, phoneAccountHandle); + + if (phoneAccountHandle != null) { + PhoneAccount account = getTelecomManager(context).getPhoneAccount(phoneAccountHandle); + NotificationChannelGroup group = + new NotificationChannelGroup( + phoneAccountHandle.getId(), + (account == null) ? phoneAccountHandle.getId() : account.getLabel().toString()); + getNotificationManager(context) + .createNotificationChannelGroup(group); // No-op if already exists + } else if (!Channel.MISC.equals(channelName)) { + LogUtil.w( + "NotificationChannelManager.createChannel", + "Null PhoneAccountHandle with channel " + channelName); + } + + Uri silentRingtone = Uri.parse(""); + + CharSequence name; + int importance; + boolean canShowBadge; + boolean lights; + boolean vibration; + Uri sound; + switch (channelName) { + case Channel.INCOMING_CALL: + name = context.getText(R.string.notification_channel_incoming_call); + importance = NotificationManager.IMPORTANCE_MAX; + canShowBadge = false; + lights = true; + vibration = false; + sound = silentRingtone; + break; + case Channel.MISSED_CALL: + name = context.getText(R.string.notification_channel_missed_call); + importance = NotificationManager.IMPORTANCE_DEFAULT; + canShowBadge = true; + lights = true; + vibration = true; + sound = silentRingtone; + break; + case Channel.ONGOING_CALL: + name = context.getText(R.string.notification_channel_ongoing_call); + importance = NotificationManager.IMPORTANCE_DEFAULT; + canShowBadge = false; + lights = false; + vibration = false; + sound = null; + break; + case Channel.VOICEMAIL: + name = context.getText(R.string.notification_channel_voicemail); + importance = NotificationManager.IMPORTANCE_DEFAULT; + canShowBadge = true; + lights = true; + vibration = true; + sound = + TelephonyManagerCompat.getVoicemailRingtoneUri( + getTelephonyManager(context), phoneAccountHandle); + break; + case Channel.EXTERNAL_CALL: + name = context.getText(R.string.notification_channel_external_call); + importance = NotificationManager.IMPORTANCE_HIGH; + canShowBadge = false; + lights = true; + vibration = true; + sound = null; + break; + case Channel.MISC: + name = context.getText(R.string.notification_channel_misc); + importance = NotificationManager.IMPORTANCE_DEFAULT; + canShowBadge = false; + lights = true; + vibration = true; + sound = null; + break; + default: + throw new IllegalArgumentException("Unknown channel: " + channelName); + } + + NotificationChannel channel = new NotificationChannel(channelId, name, importance); + channel.setShowBadge(canShowBadge); + if (sound != null) { + channel.setSound( + sound, + new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build()); + } + channel.enableLights(lights); + channel.enableVibration(vibration); + getNotificationManager(context).createNotificationChannel(channel); + return channel; + } + + private static NotificationManager getNotificationManager(@NonNull Context context) { + return context.getSystemService(NotificationManager.class); + } + + private static TelephonyManager getTelephonyManager(@NonNull Context context) { + return context.getSystemService(TelephonyManager.class); + } + + private static TelecomManager getTelecomManager(@NonNull Context context) { + return context.getSystemService(TelecomManager.class); + } +} diff --git a/java/com/android/dialer/notification/res/values/ids.xml b/java/com/android/dialer/notification/res/values/ids.xml new file mode 100644 index 000000000..6bdb489a7 --- /dev/null +++ b/java/com/android/dialer/notification/res/values/ids.xml @@ -0,0 +1,27 @@ +<?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> + <item name="notification_incoming_call" type="id"/> + <item name="notification_ongoing_call" type="id"/> + <item name="notification_missed_call" type="id"/> + <item name="notification_voicemail" type="id"/> + <item name="notification_external_call" type="id"/> + <item name="notification_call_blocking_disabled_by_emergency_call" type="id"/> + <item name="notification_spam_call" type="id"/> + <item name="notification_feedback" type="id"/> +</resources> diff --git a/java/com/android/dialer/notification/res/values/strings.xml b/java/com/android/dialer/notification/res/values/strings.xml new file mode 100644 index 000000000..2fc4962c6 --- /dev/null +++ b/java/com/android/dialer/notification/res/values/strings.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> + <string name="notification_channel_incoming_call">Incoming calls</string> + <string name="notification_channel_ongoing_call">Ongoing calls</string> + <string name="notification_channel_missed_call">Missed calls</string> + <string name="notification_channel_voicemail">Voicemails</string> + <string name="notification_channel_external_call">External calls</string> + <string name="notification_channel_misc">Miscellaneous</string> +</resources> diff --git a/java/com/android/dialer/oem/AndroidManifest.xml b/java/com/android/dialer/oem/AndroidManifest.xml new file mode 100644 index 000000000..e161a6d14 --- /dev/null +++ b/java/com/android/dialer/oem/AndroidManifest.xml @@ -0,0 +1,3 @@ +<manifest + package="com.android.dialer.oem"> +</manifest>
\ No newline at end of file diff --git a/java/com/android/dialer/oem/MotorolaHiddenMenuKeySequence.java b/java/com/android/dialer/oem/MotorolaHiddenMenuKeySequence.java new file mode 100644 index 000000000..18f621e01 --- /dev/null +++ b/java/com/android/dialer/oem/MotorolaHiddenMenuKeySequence.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * This file is derived in part from code issued under the following license. + * + * 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.oem; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import com.android.dialer.common.LogUtil; +import java.util.regex.Pattern; + +/** + * Util class to handle special char sequence and launch corresponding intent based the sequence. + */ +public class MotorolaHiddenMenuKeySequence { + private static final String EXTRA_HIDDEN_MENU_CODE = "HiddenMenuCode"; + private static MotorolaHiddenMenuKeySequence instance = null; + + private static String[] hiddenKeySequenceArray = null; + private static String[] hiddenKeySequenceIntentArray = null; + private static String[] hiddenKeyPatternArray = null; + private static String[] hiddenKeyPatternIntentArray = null; + private static boolean featureHiddenMenuEnabled = false; + + /** + * Handle input char sequence. + * + * @param context context + * @param input input sequence + * @return true if the input matches any pattern + */ + static boolean handleCharSequence(Context context, String input) { + getInstance(context); + if (!featureHiddenMenuEnabled) { + return false; + } + return handleKeySequence(context, input) || handleKeyPattern(context, input); + } + + /** + * Public interface to return the Singleton instance + * + * @param context the Context + * @return the MotorolaHiddenMenuKeySequence singleton instance + */ + private static synchronized MotorolaHiddenMenuKeySequence getInstance(Context context) { + if (null == instance) { + instance = new MotorolaHiddenMenuKeySequence(context); + } + return instance; + } + + private MotorolaHiddenMenuKeySequence(Context context) { + featureHiddenMenuEnabled = + context.getResources().getBoolean(R.bool.motorola_feature_hidden_menu); + // In case we do have a SPN from resource we need to match from service; otherwise we are + // free to go + if (featureHiddenMenuEnabled) { + + hiddenKeySequenceArray = + context.getResources().getStringArray(R.array.motorola_hidden_menu_key_sequence); + hiddenKeySequenceIntentArray = + context.getResources().getStringArray(R.array.motorola_hidden_menu_key_sequence_intents); + hiddenKeyPatternArray = + context.getResources().getStringArray(R.array.motorola_hidden_menu_key_pattern); + hiddenKeyPatternIntentArray = + context.getResources().getStringArray(R.array.motorola_hidden_menu_key_pattern_intents); + + if (hiddenKeySequenceArray.length != hiddenKeySequenceIntentArray.length + || hiddenKeyPatternArray.length != hiddenKeyPatternIntentArray.length + || (hiddenKeySequenceArray.length == 0 && hiddenKeyPatternArray.length == 0)) { + LogUtil.e( + "MotorolaHiddenMenuKeySequence", + "the key sequence array is not matching, turn off feature." + + "key sequence: %d != %d, key pattern %d != %d", + hiddenKeySequenceArray.length, + hiddenKeySequenceIntentArray.length, + hiddenKeyPatternArray.length, + hiddenKeyPatternIntentArray.length); + featureHiddenMenuEnabled = false; + } + } + } + + private static boolean handleKeyPattern(Context context, String input) { + int len = input.length(); + if (len <= 3 || hiddenKeyPatternArray == null || hiddenKeyPatternIntentArray == null) { + return false; + } + + for (int i = 0; i < hiddenKeyPatternArray.length; i++) { + if ((Pattern.compile(hiddenKeyPatternArray[i])).matcher(input).matches()) { + return sendIntent(context, input, hiddenKeyPatternIntentArray[i]); + } + } + return false; + } + + private static boolean handleKeySequence(Context context, String input) { + int len = input.length(); + if (len <= 3 || hiddenKeySequenceArray == null || hiddenKeySequenceIntentArray == null) { + return false; + } + + for (int i = 0; i < hiddenKeySequenceArray.length; i++) { + if (hiddenKeySequenceArray[i].equals(input)) { + return sendIntent(context, input, hiddenKeySequenceIntentArray[i]); + } + } + return false; + } + + private static boolean sendIntent( + final Context context, final String input, final String action) { + LogUtil.d("MotorolaHiddenMenuKeySequence.sendIntent", "input: %s", input); + try { + Intent intent = new Intent(action); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(EXTRA_HIDDEN_MENU_CODE, input); + + ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0); + + if (resolveInfo != null + && resolveInfo.activityInfo != null + && resolveInfo.activityInfo.enabled) { + context.startActivity(intent); + return true; + } else { + LogUtil.w("MotorolaHiddenMenuKeySequence.sendIntent", "not able to resolve the intent"); + } + } catch (ActivityNotFoundException e) { + LogUtil.e( + "MotorolaHiddenMenuKeySequence.sendIntent", "handleHiddenMenu Key Pattern Exception", e); + } + return false; + } +} diff --git a/java/com/android/dialer/oem/MotorolaUtils.java b/java/com/android/dialer/oem/MotorolaUtils.java new file mode 100644 index 000000000..29bf0b23d --- /dev/null +++ b/java/com/android/dialer/oem/MotorolaUtils.java @@ -0,0 +1,51 @@ +package com.android.dialer.oem; + +import android.content.Context; +import com.android.dialer.common.ConfigProviderBindings; + +/** Util class for Motorola OEM devices. */ +public class MotorolaUtils { + + private static final String CONFIG_HD_CODEC_BLINKING_ICON_WHEN_CONNECTING_CALL_ENABLED = + "hd_codec_blinking_icon_when_connecting_enabled"; + private static final String CONFIG_HD_CODEC_SHOW_ICON_IN_CALL_LOG_ENABLED = + "hd_codec_show_icon_in_call_log_enabled"; + + // This is used to check if a Motorola device supports HD voice call feature, which comes from + // system feature setting. + private static final String HD_CALL_FEATRURE = "com.motorola.software.sprint.hd_call"; + + // Feature flag indicates it's a HD call, currently this is only used by Motorola system build. + // TODO(b/35359461): Upstream and move it to android.provider.CallLog. + private static final int FEATURES_HD_CALL = 0x10000000; + + public static boolean shouldBlinkHdIconWhenConnectingCall(Context context) { + return ConfigProviderBindings.get(context) + .getBoolean(CONFIG_HD_CODEC_BLINKING_ICON_WHEN_CONNECTING_CALL_ENABLED, true) + && isSupportingSprintHdCodec(context); + } + + public static boolean shouldShowHdIconInCallLog(Context context, int features) { + return ConfigProviderBindings.get(context) + .getBoolean(CONFIG_HD_CODEC_SHOW_ICON_IN_CALL_LOG_ENABLED, true) + && isSupportingSprintHdCodec(context) + && (features & FEATURES_HD_CALL) == FEATURES_HD_CALL; + } + + /** + * Handle special char sequence entered in dialpad. This may launch special intent based on input. + * + * @param context context + * @param input input string + * @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. + return MotorolaHiddenMenuKeySequence.handleCharSequence(context, input); + } + + private static boolean isSupportingSprintHdCodec(Context context) { + return context.getPackageManager().hasSystemFeature(HD_CALL_FEATRURE) + && context.getResources().getBoolean(R.bool.motorola_sprint_hd_codec); + } +} diff --git a/java/com/android/dialer/oem/res/values/motorola_config.xml b/java/com/android/dialer/oem/res/values/motorola_config.xml new file mode 100644 index 000000000..f875d573d --- /dev/null +++ b/java/com/android/dialer/oem/res/values/motorola_config.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Flag to control if HD codec is supported by Sprint. --> + <bool name="motorola_sprint_hd_codec">false</bool> + + <!-- Hidden menu configuration for Motorola. --> + <!-- Flag to control if the Hidden Menu sequence will be supported by Sprint. --> + <bool name="motorola_feature_hidden_menu">false</bool> + + <!-- This defines the specific key seuquence that will be catched in the SpecialCharSequenceMgr + such as, ##OMADM# --> + <string-array name="motorola_hidden_menu_key_sequence"> + <item>##66236#</item> <!--##OMADM#--> + <item>##2539#</item> <!--##AKEY#--> + <item>##786#</item> <!--##RTN#--> + <item>##72786#</item> <!--##SCRTN#--> + <item>##3282#</item> <!--##DATA#--> + <item>##33284#</item> <!--##DEBUG#--> + <item>##3424#</item> <!--##DIAG#--> + <item>##564#</item> <!--##LOG#--> + <item>##4567257#</item> <!--##GLMSCLR#--> + <item>##873283#</item> <!--##UPDATE#--> + <item>##6343#</item> <!--##MEID#--> + <item>##27263#</item> <!--##BRAND#--> + <item>##258#</item> <!--##BLV#--> + <item>##8422#</item> <!--##UICC#--> + <item>##4382#</item> <!--CMAS/WEA--> + </string-array> + + <string name="motorola_hidden_menu_intent">com.motorola.intent.action.LAUNCH_HIDDEN_MENU</string> + + <!-- This defines the intents that will be send out when the key quence is matched, this must be + in the same order with he KeySequence array. --> + <string-array name="motorola_hidden_menu_key_sequence_intents"> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>com.motorola.android.intent.action.omadm.sprint.hfa</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + <item>@string/motorola_hidden_menu_intent</item> + </string-array> + + <!-- This defines the specific key patterns that will be catched in the SpecialCharSequenceMgr + such as, ##[0-9]{3,7}# --> + <string-array name="motorola_hidden_menu_key_pattern"> + <!--##MSL#, here MSL is 6 digits SPC code, ##OTKSL#, OTKSL is also digits code --> + <item>##[0-9]{6}#</item> + </string-array> + + <!-- This defines the intents that will be send out when the key quence is matched, this must be + in the same order with he KeyPattern array. --> + <string-array name="motorola_hidden_menu_key_pattern_intents"> + <item>@string/motorola_hidden_menu_intent</item> + </string-array> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/p13n/inference/P13nRanking.java b/java/com/android/dialer/p13n/inference/P13nRanking.java index 6bfc0352a..0682e85db 100644 --- a/java/com/android/dialer/p13n/inference/P13nRanking.java +++ b/java/com/android/dialer/p13n/inference/P13nRanking.java @@ -22,6 +22,7 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.android.dialer.common.Assert; +import com.android.dialer.common.ConfigProviderBindings; import com.android.dialer.p13n.inference.protocol.P13nRanker; import com.android.dialer.p13n.inference.protocol.P13nRankerFactory; import java.util.List; @@ -38,37 +39,51 @@ public final class P13nRanking { public static P13nRanker get(@NonNull Context context) { Assert.isNotNull(context); Assert.isMainThread(); + if (ranker != null) { return ranker; } + if (!ConfigProviderBindings.get(context).getBoolean("p13n_ranker_should_enable", false)) { + setToIdentityRanker(); + return ranker; + } + Context application = context.getApplicationContext(); if (application instanceof P13nRankerFactory) { ranker = ((P13nRankerFactory) application).newP13nRanker(); } if (ranker == null) { - ranker = - new P13nRanker() { - @Override - public void refresh(@Nullable P13nRefreshCompleteListener listener) {} - - @Override - public List<String> rankList(List<String> phoneNumbers) { - return phoneNumbers; - } - - @NonNull - @Override - public Cursor rankCursor( - @NonNull Cursor phoneQueryResults, int phoneNumberColumnIndex) { - return phoneQueryResults; - } - }; + setToIdentityRanker(); } return ranker; } + private static void setToIdentityRanker() { + ranker = + new P13nRanker() { + @Override + public void refresh(@Nullable P13nRefreshCompleteListener listener) {} + + @Override + public List<String> rankList(List<String> phoneNumbers) { + return phoneNumbers; + } + + @NonNull + @Override + public Cursor rankCursor(@NonNull Cursor phoneQueryResults, int queryLength) { + return phoneQueryResults; + } + + @Override + public boolean shouldShowEmptyListForNullQuery() { + return true; + } + }; + } + public static void setForTesting(@NonNull P13nRanker ranker) { P13nRanking.ranker = ranker; } diff --git a/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java b/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java index 9a859a6db..41f1de49d 100644 --- a/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java +++ b/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java @@ -41,13 +41,15 @@ public interface P13nRanker { * input cursor is closed or invalid, or if any other error occurs in the ranking process. * * @param phoneQueryResults cursor of results of a Dialer search query - * @param phoneNumberColumnIndex column index of the phone number in the cursor data + * @param queryLength length of the search query that resulted in the cursor data, if below 0, + * assumes no length is specified, thus applies the default behavior which is same as when + * queryLength is greater than zero. * @return new cursor of data reordered by ranking (or reference to input cursor if order * unchanged) */ @NonNull @MainThread - Cursor rankCursor(@NonNull Cursor phoneQueryResults, int phoneNumberColumnIndex); + Cursor rankCursor(@NonNull Cursor phoneQueryResults, int queryLength); /** * Refreshes ranking cache (pulls fresh contextual features, pre-caches inference results, etc.). @@ -61,6 +63,10 @@ public interface P13nRanker { @MainThread void refresh(@Nullable P13nRefreshCompleteListener listener); + /** Decides if results should be displayed for no-query search. */ + @MainThread + boolean shouldShowEmptyListForNullQuery(); + /** * Callback class for when ranking refresh has completed. * diff --git a/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java index 03b77b91c..f443d56fb 100644 --- a/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java +++ b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java @@ -18,7 +18,9 @@ package com.android.dialer.phonenumbercache; import android.content.Context; import android.net.Uri; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; import java.io.InputStream; public interface CachedNumberLookupService { @@ -35,6 +37,7 @@ public interface CachedNumberLookupService { * found in the cache, {@link ContactInfo#EMPTY} if the phone number was not found in the * cache, and null if there was an error when querying the cache. */ + @WorkerThread CachedContactInfo lookupCachedContactFromNumber(Context context, String number); void addContact(Context context, CachedContactInfo info); @@ -64,6 +67,7 @@ public interface CachedNumberLookupService { int SOURCE_TYPE_PROFILE = 4; int SOURCE_TYPE_CNAP = 5; + @NonNull ContactInfo getContactInfo(); void setSource(int sourceType, String name, long directoryId); diff --git a/java/com/android/dialer/postcall/AndroidManifest.xml b/java/com/android/dialer/postcall/AndroidManifest.xml new file mode 100644 index 000000000..2bf07bca2 --- /dev/null +++ b/java/com/android/dialer/postcall/AndroidManifest.xml @@ -0,0 +1,28 @@ +<!-- + ~ Copyright (C) 2016 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.callcomposer"> + + <application> + <activity + android:name="com.android.dialer.postcall.PostCallActivity" + android:exported="false" + android:theme="@style/Theme.AppCompat.NoActionBar" + android:windowSoftInputMode="adjustResize" + android:screenOrientation="portrait"/> + </application> +</manifest> diff --git a/java/com/android/dialer/postcall/PostCall.java b/java/com/android/dialer/postcall/PostCall.java new file mode 100644 index 000000000..cfe7c867b --- /dev/null +++ b/java/com/android/dialer/postcall/PostCall.java @@ -0,0 +1,182 @@ +/* + * 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.postcall; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.design.widget.BaseTransientBottomBar.BaseCallback; +import android.support.design.widget.Snackbar; +import android.view.View; +import android.view.View.OnClickListener; +import com.android.dialer.buildtype.BuildType; +import com.android.dialer.common.Assert; +import com.android.dialer.common.ConfigProvider; +import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; + +/** Helper class to handle all post call actions. */ +public class PostCall { + + private static final String KEY_POST_CALL_CALL_CONNECT_TIME = "post_call_call_connect_time"; + private static final String KEY_POST_CALL_CALL_DISCONNECT_TIME = "post_call_call_disconnect_time"; + private static final String KEY_POST_CALL_CALL_NUMBER = "post_call_call_number"; + private static final String KEY_POST_CALL_MESSAGE_SENT = "post_call_message_sent"; + + public static void promptUserForMessageIfNecessary(Activity activity, View rootView) { + if (isEnabled(activity)) { + if (shouldPromptUserToViewSentMessage(activity)) { + promptUserToViewSentMessage(activity, rootView); + } else if (shouldPromptUserToSendMessage(activity)) { + promptUserToSendMessage(activity, rootView); + } + } + } + + private static void promptUserToSendMessage(Activity activity, View rootView) { + LogUtil.i("PostCall.promptUserToSendMessage", "returned from call, showing post call SnackBar"); + String message = activity.getString(R.string.post_call_message); + String addMessage = activity.getString(R.string.post_call_add_message); + OnClickListener onClickListener = + v -> { + Logger.get(activity) + .logImpression(DialerImpression.Type.POST_CALL_PROMPT_USER_TO_SEND_MESSAGE_CLICKED); + activity.startActivity(PostCallActivity.newIntent(activity, getPhoneNumber(activity))); + }; + + Snackbar.make(rootView, message, Snackbar.LENGTH_INDEFINITE) + .setAction(addMessage, onClickListener) + .setActionTextColor( + activity.getResources().getColor(R.color.dialer_snackbar_action_text_color)) + .show(); + Logger.get(activity).logImpression(DialerImpression.Type.POST_CALL_PROMPT_USER_TO_SEND_MESSAGE); + PreferenceManager.getDefaultSharedPreferences(activity) + .edit() + .remove(KEY_POST_CALL_CALL_DISCONNECT_TIME) + .apply(); + } + + private static void promptUserToViewSentMessage(Activity activity, View rootView) { + LogUtil.i( + "PostCall.promptUserToViewSentMessage", + "returned from sending a post call message, message sent."); + String message = activity.getString(R.string.post_call_message_sent); + String addMessage = activity.getString(R.string.view); + OnClickListener onClickListener = + v -> { + Logger.get(activity) + .logImpression( + DialerImpression.Type.POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE_CLICKED); + Intent intent = IntentUtil.getSendSmsIntent(getPhoneNumber(activity)); + DialerUtils.startActivityWithErrorToast(activity, intent); + }; + + Snackbar.make(rootView, message, Snackbar.LENGTH_INDEFINITE) + .setAction(addMessage, onClickListener) + .setActionTextColor( + activity.getResources().getColor(R.color.dialer_snackbar_action_text_color)) + .addCallback( + new BaseCallback<Snackbar>() { + @Override + public void onDismissed(Snackbar snackbar, int i) { + super.onDismissed(snackbar, i); + clear(snackbar.getContext()); + } + }) + .show(); + Logger.get(activity) + .logImpression(DialerImpression.Type.POST_CALL_PROMPT_USER_TO_VIEW_SENT_MESSAGE); + PreferenceManager.getDefaultSharedPreferences(activity) + .edit() + .remove(KEY_POST_CALL_MESSAGE_SENT) + .apply(); + } + + public static void onCallDisconnected(Context context, String number, long callConnectedMillis) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putLong(KEY_POST_CALL_CALL_CONNECT_TIME, callConnectedMillis) + .putLong(KEY_POST_CALL_CALL_DISCONNECT_TIME, System.currentTimeMillis()) + .putString(KEY_POST_CALL_CALL_NUMBER, number) + .apply(); + } + + public static void onMessageSent(Context context, String number) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putString(KEY_POST_CALL_CALL_NUMBER, number) + .putBoolean(KEY_POST_CALL_MESSAGE_SENT, true) + .apply(); + } + + private static void clear(Context context) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .remove(KEY_POST_CALL_CALL_DISCONNECT_TIME) + .remove(KEY_POST_CALL_CALL_NUMBER) + .remove(KEY_POST_CALL_MESSAGE_SENT) + .remove(KEY_POST_CALL_CALL_CONNECT_TIME) + .apply(); + } + + private static boolean shouldPromptUserToSendMessage(Context context) { + SharedPreferences manager = PreferenceManager.getDefaultSharedPreferences(context); + long disconnectTimeMillis = manager.getLong(KEY_POST_CALL_CALL_DISCONNECT_TIME, -1); + long connectTimeMillis = manager.getLong(KEY_POST_CALL_CALL_CONNECT_TIME, -1); + + long timeSinceDisconnect = System.currentTimeMillis() - disconnectTimeMillis; + long callDurationMillis = disconnectTimeMillis - connectTimeMillis; + + ConfigProvider binding = ConfigProviderBindings.get(context); + return disconnectTimeMillis != -1 + && connectTimeMillis != -1 + && binding.getLong("postcall_last_call_threshold", 30_000) > timeSinceDisconnect + && binding.getLong("postcall_call_duration_threshold", 60_000) > callDurationMillis; + } + + private static boolean shouldPromptUserToViewSentMessage(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(KEY_POST_CALL_MESSAGE_SENT, false); + } + + private static String getPhoneNumber(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getString(KEY_POST_CALL_CALL_NUMBER, null); + } + + private static boolean isEnabled(Context context) { + @BuildType.Type int type = BuildType.get(); + switch (type) { + case BuildType.BUGFOOD: + case BuildType.DOGFOOD: + case BuildType.FISHFOOD: + case BuildType.TEST: + return ConfigProviderBindings.get(context).getBoolean("enable_post_call", true); + case BuildType.RELEASE: + return ConfigProviderBindings.get(context).getBoolean("enable_post_call_prod", true); + default: + Assert.fail(); + return false; + } + } +} diff --git a/java/com/android/dialer/postcall/PostCallActivity.java b/java/com/android/dialer/postcall/PostCallActivity.java new file mode 100644 index 000000000..8da03dcd1 --- /dev/null +++ b/java/com/android/dialer/postcall/PostCallActivity.java @@ -0,0 +1,151 @@ +/* + * 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.postcall; + +import android.Manifest.permission; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; +import android.telephony.SmsManager; +import android.widget.Toolbar; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.enrichedcall.EnrichedCallCapabilities; +import com.android.dialer.enrichedcall.EnrichedCallComponent; +import com.android.dialer.enrichedcall.EnrichedCallManager; +import com.android.dialer.util.PermissionsUtil; +import com.android.dialer.widget.MessageFragment; + +/** Activity used to send post call messages after a phone call. */ +public class PostCallActivity extends AppCompatActivity implements MessageFragment.Listener { + + public static final String KEY_PHONE_NUMBER = "phone_number"; + public static final String KEY_MESSAGE = "message"; + private static final int REQUEST_CODE_SEND_SMS = 1; + + private boolean useRcs; + + public static Intent newIntent(@NonNull Context context, @NonNull String number) { + Intent intent = new Intent(Assert.isNotNull(context), PostCallActivity.class); + intent.putExtra(KEY_PHONE_NUMBER, Assert.isNotNull(number)); + return intent; + } + + @Override + protected void onCreate(@Nullable Bundle bundle) { + super.onCreate(bundle); + setContentView(R.layout.post_call_activity); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + toolbar.setTitle(getString(R.string.post_call_message)); + toolbar.setNavigationOnClickListener(v -> finish()); + + useRcs = canUseRcs(getIntent().getStringExtra(KEY_PHONE_NUMBER)); + LogUtil.i("PostCallActivity.onCreate", "useRcs: %b", useRcs); + + int postCallCharLimit = + useRcs + ? getResources().getInteger(R.integer.post_call_char_limit) + : MessageFragment.NO_CHAR_LIMIT; + String[] messages = + new String[] { + getString(R.string.post_call_message_1), + getString(R.string.post_call_message_2), + getString(R.string.post_call_message_3) + }; + MessageFragment fragment = + MessageFragment.builder() + .setCharLimit(postCallCharLimit) + .showSendIcon() + .setMessages(messages) + .build(); + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.message_container, fragment) + .commit(); + } + + private boolean canUseRcs(@NonNull String number) { + EnrichedCallCapabilities capabilities = + getEnrichedCallManager().getCapabilities(Assert.isNotNull(number)); + LogUtil.i( + "PostCallActivity.canUseRcs", + "number: %s, capabilities: %s", + LogUtil.sanitizePhoneNumber(number), + capabilities); + return capabilities != null && capabilities.supportsPostCall(); + } + + @Override + public void onMessageFragmentSendMessage(@NonNull String message) { + String number = Assert.isNotNull(getIntent().getStringExtra(KEY_PHONE_NUMBER)); + getIntent().putExtra(KEY_MESSAGE, message); + + if (useRcs) { + LogUtil.i("PostCallActivity.onMessageFragmentSendMessage", "sending post call Rcs."); + getEnrichedCallManager().sendPostCallNote(number, message); + PostCall.onMessageSent(this, number); + finish(); + } else if (PermissionsUtil.hasPermission(this, permission.SEND_SMS)) { + LogUtil.i("PostCallActivity.sendMessage", "Sending post call SMS."); + SmsManager smsManager = SmsManager.getDefault(); + smsManager.sendMultipartTextMessage( + number, null, smsManager.divideMessage(message), null, null); + PostCall.onMessageSent(this, number); + finish(); + } else if (PermissionsUtil.isFirstRequest(this, permission.SEND_SMS) + || shouldShowRequestPermissionRationale(permission.SEND_SMS)) { + LogUtil.i("PostCallActivity.sendMessage", "Requesting SMS_SEND permission."); + requestPermissions(new String[] {permission.SEND_SMS}, REQUEST_CODE_SEND_SMS); + } else { + LogUtil.i( + "PostCallActivity.sendMessage", "Permission permanently denied, sending to settings."); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData(Uri.parse("package:" + this.getPackageName())); + startActivity(intent); + } + } + + @Override + public void onMessageFragmentAfterTextChange(String message) {} + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (permissions.length > 0 && permissions[0].equals(permission.SEND_SMS)) { + PermissionsUtil.permissionRequested(this, permissions[0]); + } + if (requestCode == REQUEST_CODE_SEND_SMS + && grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + onMessageFragmentSendMessage(getIntent().getStringExtra(KEY_MESSAGE)); + } + } + + @NonNull + private EnrichedCallManager getEnrichedCallManager() { + return EnrichedCallComponent.get(this).getEnrichedCallManager(); + } +} diff --git a/java/com/android/dialer/postcall/res/layout/post_call_activity.xml b/java/com/android/dialer/postcall/res/layout/post_call_activity.xml new file mode 100644 index 000000000..6ea8126c5 --- /dev/null +++ b/java/com/android/dialer/postcall/res/layout/post_call_activity.xml @@ -0,0 +1,38 @@ +<?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" + android:background="@color/background_dialer_white" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:id="@+id/message_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:background="@color/background_dialer_white"/> + + <Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?attr/actionBarSize" + android:titleTextAppearance="@style/toolbar_title_text" + android:subtitleTextAppearance="@style/toolbar_subtitle_text" + android:navigationIcon="@drawable/quantum_ic_close_white_24" + android:background="@color/dialer_theme_color"/> +</RelativeLayout>
\ No newline at end of file diff --git a/java/com/android/dialer/postcall/res/values/strings.xml b/java/com/android/dialer/postcall/res/values/strings.xml new file mode 100644 index 000000000..d5e085a05 --- /dev/null +++ b/java/com/android/dialer/postcall/res/values/strings.xml @@ -0,0 +1,31 @@ +<?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> + <!-- Shown as a message that notifies asks the user if they want to send a post call message --> + <string name="post_call_message">Say why you called</string> + <!-- Premade message to be sent as a text/RCS message --> + <string name="post_call_message_1">This is urgent. Call me back.</string> + <!-- Premade message to be sent as a text/RCS message --> + <string name="post_call_message_2">Call me back when you have some time.</string> + <!-- Premade message to be sent as a text/RCS message --> + <string name="post_call_message_3">Not urgent, we can chat later.</string> + <!-- Asks the user if they want to send a post call message --> + <string name="post_call_add_message">Add message</string> + <!-- Shown to let the user know that their message was sent. --> + <string name="post_call_message_sent">Message sent</string> + <string name="view">View</string> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/postcall/res/values/values.xml b/java/com/android/dialer/postcall/res/values/values.xml new file mode 100644 index 000000000..64fe9f6c8 --- /dev/null +++ b/java/com/android/dialer/postcall/res/values/values.xml @@ -0,0 +1,19 @@ +<?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> + <integer name="post_call_char_limit">60</integer> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/proguard/proguard.flags b/java/com/android/dialer/proguard/proguard.flags new file mode 100644 index 000000000..0f684a0b3 --- /dev/null +++ b/java/com/android/dialer/proguard/proguard.flags @@ -0,0 +1,6 @@ +# Keep the annotation, classes, methods, and fields marked as UsedByReflection +-keep class com.android.dialer.proguard.UsedByReflection +-keep @com.android.dialer.proguard.UsedByReflection class * +-keepclassmembers class * { + @com.android.dialer.proguard.UsedByReflection *; +} diff --git a/java/com/android/dialer/proguard/proguard_base.flags b/java/com/android/dialer/proguard/proguard_base.flags new file mode 100644 index 000000000..7b5794ec7 --- /dev/null +++ b/java/com/android/dialer/proguard/proguard_base.flags @@ -0,0 +1,74 @@ +# 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 +# no downsides. You probably do not want to make changes to this file. + +# The presence of both of these attributes causes dalvik and other jvms to print +# stack traces on uncaught exceptions, which is necessary to get useful crash +# reports. +-keepattributes SourceFile,LineNumberTable + +# Preverification was introduced in Java 6 to enable faster classloading, but +# dex doesn't use the java .class format, so it has no benefit and can cause +# problems. +-dontpreverify + +# Skipping analysis of some classes may make proguard strip something that's +# needed. +-dontskipnonpubliclibraryclasses + +# Case-insensitive filesystems can't handle when a.class and A.class exist in +# the same directory. +-dontusemixedcaseclassnames + +# This prevents the names of native methods from being obfuscated and prevents +# UnsatisfiedLinkErrors. +-keepclasseswithmembernames class * { + native <methods>; +} + +# hackbod discourages the use of enums on android, but if you use them, they +# should work. Allow instantiation via reflection by keeping the values method. +-keepclassmembers enum * { + public static **[] values(); +} + +# Parcel reflectively accesses this field. +-keepclassmembers class * implements android.os.Parcelable { + public static *** CREATOR; +} + +# These methods are needed to ensure that serialization behaves as expected when +# classes are obfuscated, shrunk, and/or optimized. +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +# Don't warn about Guava. Any Guava-using app will fail the proguard stage without this dontwarn, +# and since Guava is so widely used, we include it here in the base. +-dontwarn com.google.common.** + +# Don't warn about Error Prone annotations (e.g. @CompileTimeConstant) +-dontwarn com.google.errorprone.annotations.** + +# Based on http://ag/718466: android.app.Notification.setLatestEventInfo() was +# removed in MNC, but is still referenced (safely) by the NotificationCompat +# code. +-dontwarn android.app.Notification + +# Silence notes about dynamically referenced classes from AOSP support +# libraries. +-dontnote android.graphics.Insets + +# AOSP support library: ICU references to gender and plurals messages. +-dontnote libcore.icu.ICU +-keep class libcore.icu.ICU { *** get(...);} + +# AOSP support library: Handle classes that use reflection. +-dontnote android.support.v4.app.NotificationCompatJellybean diff --git a/java/com/android/dialer/proguard/proguard_release.flags b/java/com/android/dialer/proguard/proguard_release.flags new file mode 100644 index 000000000..1c845cfa3 --- /dev/null +++ b/java/com/android/dialer/proguard/proguard_release.flags @@ -0,0 +1,24 @@ +# Copied from http://google3/java/com/google/android/apps/common/proguard/release.flags + +# Used for building release binaries. Obfuscates, optimizes, and shrinks. + +# By default, proguard leaves all classes in their original package, which +# needlessly repeats com.google.android.apps.etc. +-repackageclasses "" + +# Allows proguard to make private and protected methods and fields public as +# part of optimization. This lets proguard inline trivial getter/setter methods. +-allowaccessmodification + +# The source file attribute must be present in order to print stack traces, but +# we rename it in order to avoid leaking the pre-obfuscation class name. +-renamesourcefileattribute PG + +# This allows proguard to strip isLoggable() blocks containing only debug log +# code from release builds. +-assumenosideeffects class android.util.Log { + static *** i(...); + static *** d(...); + static *** v(...); + static *** isLoggable(...); +} diff --git a/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java b/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java deleted file mode 100644 index ef995c816..000000000 --- a/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java +++ /dev/null @@ -1,161 +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.shortcuts; - -import android.support.annotation.NonNull; -import javax.annotation.Generated; - -@Generated("com.google.auto.value.processor.AutoValueProcessor") - final class AutoValue_DialerShortcut extends DialerShortcut { - - private final long contactId; - private final String lookupKey; - private final String displayName; - private final int rank; - - private AutoValue_DialerShortcut( - long contactId, - String lookupKey, - String displayName, - int rank) { - this.contactId = contactId; - this.lookupKey = lookupKey; - this.displayName = displayName; - this.rank = rank; - } - - @Override - long getContactId() { - return contactId; - } - - @NonNull - @Override - String getLookupKey() { - return lookupKey; - } - - @NonNull - @Override - String getDisplayName() { - return displayName; - } - - @Override - int getRank() { - return rank; - } - - @Override - public String toString() { - return "DialerShortcut{" - + "contactId=" + contactId + ", " - + "lookupKey=" + lookupKey + ", " - + "displayName=" + displayName + ", " - + "rank=" + rank - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof DialerShortcut) { - DialerShortcut that = (DialerShortcut) o; - return (this.contactId == that.getContactId()) - && (this.lookupKey.equals(that.getLookupKey())) - && (this.displayName.equals(that.getDisplayName())) - && (this.rank == that.getRank()); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= (this.contactId >>> 32) ^ this.contactId; - h *= 1000003; - h ^= this.lookupKey.hashCode(); - h *= 1000003; - h ^= this.displayName.hashCode(); - h *= 1000003; - h ^= this.rank; - return h; - } - - static final class Builder extends DialerShortcut.Builder { - private Long contactId; - private String lookupKey; - private String displayName; - private Integer rank; - Builder() { - } - private Builder(DialerShortcut source) { - this.contactId = source.getContactId(); - this.lookupKey = source.getLookupKey(); - this.displayName = source.getDisplayName(); - this.rank = source.getRank(); - } - @Override - DialerShortcut.Builder setContactId(long contactId) { - this.contactId = contactId; - return this; - } - @Override - DialerShortcut.Builder setLookupKey(String lookupKey) { - this.lookupKey = lookupKey; - return this; - } - @Override - DialerShortcut.Builder setDisplayName(String displayName) { - this.displayName = displayName; - return this; - } - @Override - DialerShortcut.Builder setRank(int rank) { - this.rank = rank; - return this; - } - @Override - DialerShortcut build() { - String missing = ""; - if (this.contactId == null) { - missing += " contactId"; - } - if (this.lookupKey == null) { - missing += " lookupKey"; - } - if (this.displayName == null) { - missing += " displayName"; - } - if (this.rank == null) { - missing += " rank"; - } - if (!missing.isEmpty()) { - throw new IllegalStateException("Missing required properties:" + missing); - } - return new AutoValue_DialerShortcut( - this.contactId, - this.lookupKey, - this.displayName, - this.rank); - } - } - -}
\ No newline at end of file diff --git a/java/com/android/dialer/shortcuts/CallContactActivity.java b/java/com/android/dialer/shortcuts/CallContactActivity.java index 1e9a01b39..40bf97b87 100644 --- a/java/com/android/dialer/shortcuts/CallContactActivity.java +++ b/java/com/android/dialer/shortcuts/CallContactActivity.java @@ -56,11 +56,20 @@ public class CallContactActivity extends TransactionSafeActivity } } + /** + * Attempt to make a call, finishing the activity if the required permissions are already granted. + * If the required permissions are not already granted, the activity is not finished so that the + * user can choose to grant or deny them. + */ private void makeCall() { CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); callSpecificAppData.callInitiationType = CallInitiationType.Type.LAUNCHER_SHORTCUT; - PhoneNumberInteraction.startInteractionForPhoneCall( - this, contactUri, false /* isVideoCall */, callSpecificAppData); + boolean interactionStarted = + PhoneNumberInteraction.startInteractionForPhoneCall( + this, contactUri, false /* isVideoCall */, callSpecificAppData); + if (interactionStarted) { + finish(); + } } @Override @@ -115,6 +124,7 @@ public class CallContactActivity extends TransactionSafeActivity int requestCode, String[] permissions, int[] grantResults) { switch (requestCode) { case PhoneNumberInteraction.REQUEST_READ_CONTACTS: + case PhoneNumberInteraction.REQUEST_CALL_PHONE: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -122,8 +132,8 @@ public class CallContactActivity extends TransactionSafeActivity } else { Toast.makeText(this, R.string.dialer_shortcut_no_permissions, Toast.LENGTH_SHORT) .show(); + finish(); } - finish(); break; } default: diff --git a/java/com/android/dialer/shortcuts/DialerShortcut.java b/java/com/android/dialer/shortcuts/DialerShortcut.java index f2fb3301a..a8d4204fe 100644 --- a/java/com/android/dialer/shortcuts/DialerShortcut.java +++ b/java/com/android/dialer/shortcuts/DialerShortcut.java @@ -22,7 +22,7 @@ import android.net.Uri; import android.os.Build.VERSION_CODES; import android.provider.ContactsContract.Contacts; import android.support.annotation.NonNull; - +import com.google.auto.value.AutoValue; /** * Convenience data structure. @@ -31,7 +31,7 @@ import android.support.annotation.NonNull; * convenience methods for doing things like constructing labels. */ @TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1 - +@AutoValue abstract class DialerShortcut { /** Marker value indicates that shortcut has no setRank. Used by pinned shortcuts. */ @@ -160,7 +160,7 @@ abstract class DialerShortcut { return new AutoValue_DialerShortcut.Builder().setRank(NO_RANK); } - + @AutoValue.Builder abstract static class Builder { /** diff --git a/java/com/android/dialer/shortcuts/res/values/strings.xml b/java/com/android/dialer/shortcuts/res/values/strings.xml index 1e2c87f12..5f14a8100 100644 --- a/java/com/android/dialer/shortcuts/res/values/strings.xml +++ b/java/com/android/dialer/shortcuts/res/values/strings.xml @@ -30,8 +30,8 @@ be found or doesn't have any phone numbers. [CHAR LIMIT=70] --> <string name="dialer_shortcut_contact_not_found_or_has_no_number">Contact no longer available.</string> - <!-- Error message to display when a tapping a shortcut fails because contact permissions are - missing. [CHAR LIMIT=70] --> - <string name="dialer_shortcut_no_permissions">Cannot call without contact permissions.</string> + <!-- Error message to display when a tapping a shortcut fails because permissions are missing. + [CHAR LIMIT=70] --> + <string name="dialer_shortcut_no_permissions">Cannot call without permissions.</string> </resources> diff --git a/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml b/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml index 5e8f58d1f..49149e3e1 100644 --- a/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml +++ b/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml @@ -24,8 +24,6 @@ <intent android:action="android.intent.action.INSERT" - android:data="content://com.android.contacts/contacts" - android:targetPackage="com.google.android.contacts" - android:targetClass="com.android.contacts.activities.CompactContactEditorActivity"/> + android:data="content://com.android.contacts/contacts"/> </shortcut> </shortcuts> diff --git a/java/com/android/dialer/simulator/SimulatorComponent.java b/java/com/android/dialer/simulator/SimulatorComponent.java new file mode 100644 index 000000000..a16592e34 --- /dev/null +++ b/java/com/android/dialer/simulator/SimulatorComponent.java @@ -0,0 +1,46 @@ +/* + * 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.simulator; + +import android.content.Context; +import dagger.Subcomponent; +import com.android.dialer.simulator.impl.SimulatorImpl; + +/** Subcomponent that can be used to access the simulator implementation. */ +public class SimulatorComponent { + private static SimulatorComponent instance; + private Simulator simulator; + + public Simulator getSimulator() { + if (simulator == null) { + simulator = new SimulatorImpl(); + } + return simulator; + } + + public static SimulatorComponent get(Context context) { + if (instance == null) { + instance = new SimulatorComponent(); + } + return instance; + } + + /** Used to refer to the root application component. */ + public interface HasComponent { + SimulatorComponent simulatorComponent(); + } +} diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java deleted file mode 100644 index 591819819..000000000 --- a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java +++ /dev/null @@ -1,160 +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.simulator.impl; - -import android.support.annotation.NonNull; -import javax.annotation.Generated; - -@Generated("com.google.auto.value.processor.AutoValueProcessor") - final class AutoValue_SimulatorCallLog_CallEntry extends SimulatorCallLog.CallEntry { - - private final String number; - private final int type; - private final int presentation; - private final long timeMillis; - - private AutoValue_SimulatorCallLog_CallEntry( - String number, - int type, - int presentation, - long timeMillis) { - this.number = number; - this.type = type; - this.presentation = presentation; - this.timeMillis = timeMillis; - } - - @NonNull - @Override - String getNumber() { - return number; - } - - @Override - int getType() { - return type; - } - - @Override - int getPresentation() { - return presentation; - } - - @Override - long getTimeMillis() { - return timeMillis; - } - - @Override - public String toString() { - return "CallEntry{" - + "number=" + number + ", " - + "type=" + type + ", " - + "presentation=" + presentation + ", " - + "timeMillis=" + timeMillis - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof SimulatorCallLog.CallEntry) { - SimulatorCallLog.CallEntry that = (SimulatorCallLog.CallEntry) o; - return (this.number.equals(that.getNumber())) - && (this.type == that.getType()) - && (this.presentation == that.getPresentation()) - && (this.timeMillis == that.getTimeMillis()); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= this.number.hashCode(); - h *= 1000003; - h ^= this.type; - h *= 1000003; - h ^= this.presentation; - h *= 1000003; - h ^= (this.timeMillis >>> 32) ^ this.timeMillis; - return h; - } - - static final class Builder extends SimulatorCallLog.CallEntry.Builder { - private String number; - private Integer type; - private Integer presentation; - private Long timeMillis; - Builder() { - } - private Builder(SimulatorCallLog.CallEntry source) { - this.number = source.getNumber(); - this.type = source.getType(); - this.presentation = source.getPresentation(); - this.timeMillis = source.getTimeMillis(); - } - @Override - SimulatorCallLog.CallEntry.Builder setNumber(String number) { - this.number = number; - return this; - } - @Override - SimulatorCallLog.CallEntry.Builder setType(int type) { - this.type = type; - return this; - } - @Override - SimulatorCallLog.CallEntry.Builder setPresentation(int presentation) { - this.presentation = presentation; - return this; - } - @Override - SimulatorCallLog.CallEntry.Builder setTimeMillis(long timeMillis) { - this.timeMillis = timeMillis; - return this; - } - @Override - SimulatorCallLog.CallEntry build() { - String missing = ""; - if (this.number == null) { - missing += " number"; - } - if (this.type == null) { - missing += " type"; - } - if (this.presentation == null) { - missing += " presentation"; - } - if (this.timeMillis == null) { - missing += " timeMillis"; - } - if (!missing.isEmpty()) { - throw new IllegalStateException("Missing required properties:" + missing); - } - return new AutoValue_SimulatorCallLog_CallEntry( - this.number, - this.type, - this.presentation, - this.timeMillis); - } - } - -} diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java deleted file mode 100644 index 00295f359..000000000 --- a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java +++ /dev/null @@ -1,231 +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.simulator.impl; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import java.io.ByteArrayOutputStream; -import java.util.List; -import javax.annotation.Generated; - -@Generated("com.google.auto.value.processor.AutoValueProcessor") - final class AutoValue_SimulatorContacts_Contact extends SimulatorContacts.Contact { - - private final String accountType; - private final String accountName; - private final String name; - private final boolean isStarred; - private final ByteArrayOutputStream photoStream; - private final List<SimulatorContacts.PhoneNumber> phoneNumbers; - private final List<SimulatorContacts.Email> emails; - - private AutoValue_SimulatorContacts_Contact( - String accountType, - String accountName, - @Nullable String name, - boolean isStarred, - @Nullable ByteArrayOutputStream photoStream, - List<SimulatorContacts.PhoneNumber> phoneNumbers, - List<SimulatorContacts.Email> emails) { - this.accountType = accountType; - this.accountName = accountName; - this.name = name; - this.isStarred = isStarred; - this.photoStream = photoStream; - this.phoneNumbers = phoneNumbers; - this.emails = emails; - } - - @NonNull - @Override - String getAccountType() { - return accountType; - } - - @NonNull - @Override - String getAccountName() { - return accountName; - } - - @Nullable - @Override - String getName() { - return name; - } - - @Override - boolean getIsStarred() { - return isStarred; - } - - @Nullable - @Override - ByteArrayOutputStream getPhotoStream() { - return photoStream; - } - - @NonNull - @Override - List<SimulatorContacts.PhoneNumber> getPhoneNumbers() { - return phoneNumbers; - } - - @NonNull - @Override - List<SimulatorContacts.Email> getEmails() { - return emails; - } - - @Override - public String toString() { - return "Contact{" - + "accountType=" + accountType + ", " - + "accountName=" + accountName + ", " - + "name=" + name + ", " - + "isStarred=" + isStarred + ", " - + "photoStream=" + photoStream + ", " - + "phoneNumbers=" + phoneNumbers + ", " - + "emails=" + emails - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof SimulatorContacts.Contact) { - SimulatorContacts.Contact that = (SimulatorContacts.Contact) o; - return (this.accountType.equals(that.getAccountType())) - && (this.accountName.equals(that.getAccountName())) - && ((this.name == null) ? (that.getName() == null) : this.name.equals(that.getName())) - && (this.isStarred == that.getIsStarred()) - && ((this.photoStream == null) ? (that.getPhotoStream() == null) : this.photoStream.equals(that.getPhotoStream())) - && (this.phoneNumbers.equals(that.getPhoneNumbers())) - && (this.emails.equals(that.getEmails())); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= this.accountType.hashCode(); - h *= 1000003; - h ^= this.accountName.hashCode(); - h *= 1000003; - h ^= (name == null) ? 0 : this.name.hashCode(); - h *= 1000003; - h ^= this.isStarred ? 1231 : 1237; - h *= 1000003; - h ^= (photoStream == null) ? 0 : this.photoStream.hashCode(); - h *= 1000003; - h ^= this.phoneNumbers.hashCode(); - h *= 1000003; - h ^= this.emails.hashCode(); - return h; - } - - static final class Builder extends SimulatorContacts.Contact.Builder { - private String accountType; - private String accountName; - private String name; - private Boolean isStarred; - private ByteArrayOutputStream photoStream; - private List<SimulatorContacts.PhoneNumber> phoneNumbers; - private List<SimulatorContacts.Email> emails; - Builder() { - } - private Builder(SimulatorContacts.Contact source) { - this.accountType = source.getAccountType(); - this.accountName = source.getAccountName(); - this.name = source.getName(); - this.isStarred = source.getIsStarred(); - this.photoStream = source.getPhotoStream(); - this.phoneNumbers = source.getPhoneNumbers(); - this.emails = source.getEmails(); - } - @Override - SimulatorContacts.Contact.Builder setAccountType(String accountType) { - this.accountType = accountType; - return this; - } - @Override - SimulatorContacts.Contact.Builder setAccountName(String accountName) { - this.accountName = accountName; - return this; - } - @Override - SimulatorContacts.Contact.Builder setName(@Nullable String name) { - this.name = name; - return this; - } - @Override - SimulatorContacts.Contact.Builder setIsStarred(boolean isStarred) { - this.isStarred = isStarred; - return this; - } - @Override - SimulatorContacts.Contact.Builder setPhotoStream(@Nullable ByteArrayOutputStream photoStream) { - this.photoStream = photoStream; - return this; - } - @Override - SimulatorContacts.Contact.Builder setPhoneNumbers(List<SimulatorContacts.PhoneNumber> phoneNumbers) { - this.phoneNumbers = phoneNumbers; - return this; - } - @Override - SimulatorContacts.Contact.Builder setEmails(List<SimulatorContacts.Email> emails) { - this.emails = emails; - return this; - } - @Override - SimulatorContacts.Contact build() { - String missing = ""; - if (this.accountType == null) { - missing += " accountType"; - } - if (this.accountName == null) { - missing += " accountName"; - } - if (this.isStarred == null) { - missing += " isStarred"; - } - if (this.phoneNumbers == null) { - missing += " phoneNumbers"; - } - if (this.emails == null) { - missing += " emails"; - } - if (!missing.isEmpty()) { - throw new IllegalStateException("Missing required properties:" + missing); - } - return new AutoValue_SimulatorContacts_Contact( - this.accountType, - this.accountName, - this.name, - this.isStarred, - this.photoStream, - this.phoneNumbers, - this.emails); - } - } - -} diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java deleted file mode 100644 index 58934801c..000000000 --- a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java +++ /dev/null @@ -1,184 +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.simulator.impl; - -import android.support.annotation.NonNull; -import javax.annotation.Generated; - -@Generated("com.google.auto.value.processor.AutoValueProcessor") - final class AutoValue_SimulatorVoicemail_Voicemail extends SimulatorVoicemail.Voicemail { - - private final String phoneNumber; - private final String transcription; - private final long durationSeconds; - private final long timeMillis; - private final boolean isRead; - - private AutoValue_SimulatorVoicemail_Voicemail( - String phoneNumber, - String transcription, - long durationSeconds, - long timeMillis, - boolean isRead) { - this.phoneNumber = phoneNumber; - this.transcription = transcription; - this.durationSeconds = durationSeconds; - this.timeMillis = timeMillis; - this.isRead = isRead; - } - - @NonNull - @Override - String getPhoneNumber() { - return phoneNumber; - } - - @NonNull - @Override - String getTranscription() { - return transcription; - } - - @Override - long getDurationSeconds() { - return durationSeconds; - } - - @Override - long getTimeMillis() { - return timeMillis; - } - - @Override - boolean getIsRead() { - return isRead; - } - - @Override - public String toString() { - return "Voicemail{" - + "phoneNumber=" + phoneNumber + ", " - + "transcription=" + transcription + ", " - + "durationSeconds=" + durationSeconds + ", " - + "timeMillis=" + timeMillis + ", " - + "isRead=" + isRead - + "}"; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o instanceof SimulatorVoicemail.Voicemail) { - SimulatorVoicemail.Voicemail that = (SimulatorVoicemail.Voicemail) o; - return (this.phoneNumber.equals(that.getPhoneNumber())) - && (this.transcription.equals(that.getTranscription())) - && (this.durationSeconds == that.getDurationSeconds()) - && (this.timeMillis == that.getTimeMillis()) - && (this.isRead == that.getIsRead()); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= this.phoneNumber.hashCode(); - h *= 1000003; - h ^= this.transcription.hashCode(); - h *= 1000003; - h ^= (this.durationSeconds >>> 32) ^ this.durationSeconds; - h *= 1000003; - h ^= (this.timeMillis >>> 32) ^ this.timeMillis; - h *= 1000003; - h ^= this.isRead ? 1231 : 1237; - return h; - } - - static final class Builder extends SimulatorVoicemail.Voicemail.Builder { - private String phoneNumber; - private String transcription; - private Long durationSeconds; - private Long timeMillis; - private Boolean isRead; - Builder() { - } - private Builder(SimulatorVoicemail.Voicemail source) { - this.phoneNumber = source.getPhoneNumber(); - this.transcription = source.getTranscription(); - this.durationSeconds = source.getDurationSeconds(); - this.timeMillis = source.getTimeMillis(); - this.isRead = source.getIsRead(); - } - @Override - SimulatorVoicemail.Voicemail.Builder setPhoneNumber(String phoneNumber) { - this.phoneNumber = phoneNumber; - return this; - } - @Override - SimulatorVoicemail.Voicemail.Builder setTranscription(String transcription) { - this.transcription = transcription; - return this; - } - @Override - SimulatorVoicemail.Voicemail.Builder setDurationSeconds(long durationSeconds) { - this.durationSeconds = durationSeconds; - return this; - } - @Override - SimulatorVoicemail.Voicemail.Builder setTimeMillis(long timeMillis) { - this.timeMillis = timeMillis; - return this; - } - @Override - SimulatorVoicemail.Voicemail.Builder setIsRead(boolean isRead) { - this.isRead = isRead; - return this; - } - @Override - SimulatorVoicemail.Voicemail build() { - String missing = ""; - if (this.phoneNumber == null) { - missing += " phoneNumber"; - } - if (this.transcription == null) { - missing += " transcription"; - } - if (this.durationSeconds == null) { - missing += " durationSeconds"; - } - if (this.timeMillis == null) { - missing += " timeMillis"; - } - if (this.isRead == null) { - missing += " isRead"; - } - if (!missing.isEmpty()) { - throw new IllegalStateException("Missing required properties:" + missing); - } - return new AutoValue_SimulatorVoicemail_Voicemail( - this.phoneNumber, - this.transcription, - this.durationSeconds, - this.timeMillis, - this.isRead); - } - } - -} diff --git a/java/com/android/dialer/simulator/impl/SimulatorCallLog.java b/java/com/android/dialer/simulator/impl/SimulatorCallLog.java index 9ace047d0..f127d5603 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorCallLog.java +++ b/java/com/android/dialer/simulator/impl/SimulatorCallLog.java @@ -26,7 +26,7 @@ import android.provider.CallLog.Calls; import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; import com.android.dialer.common.Assert; - +import com.google.auto.value.AutoValue; import java.util.ArrayList; import java.util.concurrent.TimeUnit; @@ -96,7 +96,7 @@ final class SimulatorCallLog { } } - + @AutoValue abstract static class CallEntry { @NonNull abstract String getNumber(); @@ -121,7 +121,7 @@ final class SimulatorCallLog { return values; } - + @AutoValue.Builder abstract static class Builder { abstract Builder setNumber(@NonNull String number); diff --git a/java/com/android/dialer/simulator/impl/SimulatorContacts.java b/java/com/android/dialer/simulator/impl/SimulatorContacts.java index 89315094a..c5e25b357 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorContacts.java +++ b/java/com/android/dialer/simulator/impl/SimulatorContacts.java @@ -31,7 +31,7 @@ import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.text.TextUtils; import com.android.dialer.common.Assert; - +import com.google.auto.value.AutoValue; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.List; @@ -190,7 +190,7 @@ final class SimulatorContacts { } } - + @AutoValue abstract static class Contact { @NonNull abstract String getAccountType(); @@ -221,7 +221,7 @@ final class SimulatorContacts { .setEmails(new ArrayList<>()); } - + @AutoValue.Builder abstract static class Builder { @NonNull private final List<PhoneNumber> phoneNumbers = new ArrayList<>(); @NonNull private final List<Email> emails = new ArrayList<>(); diff --git a/java/com/android/dialer/simulator/impl/SimulatorImpl.java b/java/com/android/dialer/simulator/impl/SimulatorImpl.java new file mode 100644 index 000000000..9c6826940 --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorImpl.java @@ -0,0 +1,40 @@ +/* + * 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.simulator.impl; + +import android.content.Context; +import android.view.ActionProvider; +import com.android.dialer.buildtype.BuildType; +import com.android.dialer.common.LogUtil; +import com.android.dialer.simulator.Simulator; +import javax.inject.Inject; + +/** The entry point for the simulator feature. */ +final public class SimulatorImpl implements Simulator { + @Inject + public SimulatorImpl() {} + + @Override + public boolean shouldShow() { + return BuildType.get() == BuildType.BUGFOOD || LogUtil.isDebugEnabled(); + } + + @Override + public ActionProvider getActionProvider(Context context) { + return new SimulatorActionProvider(context); + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorModule.java b/java/com/android/dialer/simulator/impl/SimulatorModule.java index 0f8ad3954..c0cca271b 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorModule.java +++ b/java/com/android/dialer/simulator/impl/SimulatorModule.java @@ -16,19 +16,15 @@ package com.android.dialer.simulator.impl; -import android.content.Context; -import android.view.ActionProvider; import com.android.dialer.simulator.Simulator; +import dagger.Binds; +import dagger.Module; +import javax.inject.Singleton; -/** The entry point for the simulator module. */ -public final class SimulatorModule implements Simulator { - @Override - public boolean shouldShow() { - return true; - } - - @Override - public ActionProvider getActionProvider(Context context) { - return new SimulatorActionProvider(context); - } +/** This module provides an instance of the simulator. */ +@Module +public abstract class SimulatorModule { + @Binds + @Singleton + public abstract Simulator bindsSimulator(SimulatorImpl simulator); } diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java b/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java index ffb9191dc..04de201ae 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java +++ b/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java @@ -26,7 +26,7 @@ import android.support.annotation.WorkerThread; import android.telecom.PhoneAccountHandle; import android.telephony.TelephonyManager; import com.android.dialer.common.Assert; - +import com.google.auto.value.AutoValue; import java.util.concurrent.TimeUnit; /** Populates the device database with voicemail entries. */ @@ -105,7 +105,7 @@ final class SimulatorVoicemail { context.getContentResolver().insert(Status.buildSourceUri(context.getPackageName()), values); } - + @AutoValue abstract static class Voicemail { @NonNull abstract String getPhoneNumber(); @@ -134,7 +134,7 @@ final class SimulatorVoicemail { return values; } - + @AutoValue.Builder abstract static class Builder { abstract Builder setPhoneNumber(@NonNull String phoneNumber); diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java index a11e7f77a..87ddda58b 100644 --- a/java/com/android/dialer/telecom/TelecomUtil.java +++ b/java/com/android/dialer/telecom/TelecomUtil.java @@ -23,12 +23,13 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.provider.CallLog.Calls; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.v4.content.ContextCompat; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.text.TextUtils; -import android.util.Log; +import com.android.dialer.common.LogUtil; import java.util.ArrayList; import java.util.List; @@ -41,6 +42,8 @@ public class TelecomUtil { private static final String TAG = "TelecomUtil"; private static boolean sWarningLogged = false; + private static Boolean isDefaultDialerForTesting; + private static Boolean hasPermissionForTesting; public static void showInCallScreen(Context context, boolean showDialpad) { if (hasReadPhoneStatePermission(context)) { @@ -48,7 +51,7 @@ public class TelecomUtil { getTelecomManager(context).showInCallScreen(showDialpad); } catch (SecurityException e) { // Just in case - Log.w(TAG, "TelecomManager.showInCallScreen called without permission."); + LogUtil.w(TAG, "TelecomManager.showInCallScreen called without permission."); } } } @@ -59,7 +62,7 @@ public class TelecomUtil { getTelecomManager(context).silenceRinger(); } catch (SecurityException e) { // Just in case - Log.w(TAG, "TelecomManager.silenceRinger called without permission."); + LogUtil.w(TAG, "TelecomManager.silenceRinger called without permission."); } } } @@ -69,7 +72,7 @@ public class TelecomUtil { try { getTelecomManager(context).cancelMissedCallsNotification(); } catch (SecurityException e) { - Log.w(TAG, "TelecomManager.cancelMissedCalls called without permission."); + LogUtil.w(TAG, "TelecomManager.cancelMissedCalls called without permission."); } } } @@ -79,7 +82,7 @@ public class TelecomUtil { try { return getTelecomManager(context).getAdnUriForPhoneAccount(handle); } catch (SecurityException e) { - Log.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission."); + LogUtil.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission."); } } return null; @@ -95,7 +98,7 @@ public class TelecomUtil { return getTelecomManager(context).handleMmi(dialString, handle); } } catch (SecurityException e) { - Log.w(TAG, "TelecomManager.handleMmi called without permission."); + LogUtil.w(TAG, "TelecomManager.handleMmi called without permission."); } } return false; @@ -186,11 +189,17 @@ public class TelecomUtil { } private static boolean hasPermission(Context context, String permission) { + if (hasPermissionForTesting != null) { + return hasPermissionForTesting; + } return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED; } public static boolean isDefaultDialer(Context context) { + if (isDefaultDialerForTesting != null) { + return isDefaultDialerForTesting; + } final boolean result = TextUtils.equals( context.getPackageName(), getTelecomManager(context).getDefaultDialerPackage()); @@ -199,7 +208,7 @@ public class TelecomUtil { } else { if (!sWarningLogged) { // Log only once to prevent spam. - Log.w(TAG, "Dialer is not currently set to be default dialer"); + LogUtil.w(TAG, "Dialer is not currently set to be default dialer"); sWarningLogged = true; } } @@ -209,4 +218,14 @@ public class TelecomUtil { private static TelecomManager getTelecomManager(Context context) { return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public static void setIsDefaultDialerForTesting(Boolean defaultDialer) { + isDefaultDialerForTesting = defaultDialer; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public static void setHasPermissionForTesting(Boolean hasPermission) { + hasPermissionForTesting = hasPermission; + } } diff --git a/java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.png Binary files differnew file mode 100644 index 000000000..2ccc89d24 --- /dev/null +++ b/java/com/android/dialer/theme/res/drawable-hdpi/ic_block_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-hdpi/ic_call_arrow.png Binary files differindex 14a33e39f..14a33e39f 100644 --- a/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png +++ b/java/com/android/dialer/theme/res/drawable-hdpi/ic_call_arrow.png diff --git a/java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.png Binary files differnew file mode 100644 index 000000000..ec1b33f0e --- /dev/null +++ b/java/com/android/dialer/theme/res/drawable-mdpi/ic_block_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-mdpi/ic_call_arrow.png Binary files differindex 169cf2934..169cf2934 100644 --- a/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png +++ b/java/com/android/dialer/theme/res/drawable-mdpi/ic_call_arrow.png diff --git a/java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.png Binary files differnew file mode 100644 index 000000000..7aba97b65 --- /dev/null +++ b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_block_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_call_arrow.png Binary files differindex 6f1366018..6f1366018 100644 --- a/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png +++ b/java/com/android/dialer/theme/res/drawable-xhdpi/ic_call_arrow.png diff --git a/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.png Binary files differnew file mode 100644 index 000000000..fddfa54b8 --- /dev/null +++ b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_block_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_call_arrow.png Binary files differindex 0364ee015..0364ee015 100644 --- a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png +++ b/java/com/android/dialer/theme/res/drawable-xxhdpi/ic_call_arrow.png diff --git a/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.png b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.png Binary files differnew file mode 100644 index 000000000..0378d1bed --- /dev/null +++ b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_block_24dp.png diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_call_arrow.png Binary files differindex 8243c2536..8243c2536 100644 --- a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png +++ b/java/com/android/dialer/theme/res/drawable-xxxhdpi/ic_call_arrow.png diff --git a/java/com/android/dialer/theme/res/values/dimens.xml b/java/com/android/dialer/theme/res/values/dimens.xml index 2d11ecc84..fa750c625 100644 --- a/java/com/android/dialer/theme/res/values/dimens.xml +++ b/java/com/android/dialer/theme/res/values/dimens.xml @@ -25,4 +25,9 @@ <!-- actionbar height + tab height --> <dimen name="actionbar_and_tab_height">107dp</dimen> <dimen name="actionbar_contentInsetStart">72dp</dimen> + + <dimen name="toolbar_title_text_size">20sp</dimen> + <dimen name="toolbar_subtitle_text_size">14sp</dimen> + + <dimen name="call_log_icon_margin">4dp</dimen> </resources> diff --git a/java/com/android/dialer/theme/res/values/styles.xml b/java/com/android/dialer/theme/res/values/styles.xml index ac94d0687..b5e89ff48 100644 --- a/java/com/android/dialer/theme/res/values/styles.xml +++ b/java/com/android/dialer/theme/res/values/styles.xml @@ -53,4 +53,15 @@ <item name="android:background">@color/actionbar_background_color</item> <item name="background">@color/actionbar_background_color</item> </style> + + <style name="toolbar_title_text"> + <item name="android:textSize">@dimen/toolbar_title_text_size</item> + <item name="android:textColor">@color/background_dialer_white</item> + <item name="android:fontFamily">sans-serif-medium</item> + </style> + + <style name="toolbar_subtitle_text"> + <item name="android:textSize">@dimen/toolbar_subtitle_text_size</item> + <item name="android:textColor">@color/background_dialer_white</item> + </style> </resources> diff --git a/java/com/android/dialer/util/AndroidManifest.xml b/java/com/android/dialer/util/AndroidManifest.xml index 499df9b4e..ba22c1781 100644 --- a/java/com/android/dialer/util/AndroidManifest.xml +++ b/java/com/android/dialer/util/AndroidManifest.xml @@ -1,3 +1,19 @@ +<!-- + ~ 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 package="com.android.dialer.util"> </manifest> diff --git a/java/com/android/dialer/util/PermissionsUtil.java b/java/com/android/dialer/util/PermissionsUtil.java index 70b96dfe1..5741e734a 100644 --- a/java/com/android/dialer/util/PermissionsUtil.java +++ b/java/com/android/dialer/util/PermissionsUtil.java @@ -47,6 +47,10 @@ public class PermissionsUtil { return hasPermission(context, permission.CAMERA); } + public static boolean hasMicrophonePermissions(Context context) { + return hasPermission(context, permission.RECORD_AUDIO); + } + public static boolean hasPermission(Context context, String permission) { return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED; diff --git a/java/com/android/dialer/util/SettingsUtil.java b/java/com/android/dialer/util/SettingsUtil.java index c61c09b6c..5043c3d56 100644 --- a/java/com/android/dialer/util/SettingsUtil.java +++ b/java/com/android/dialer/util/SettingsUtil.java @@ -69,6 +69,15 @@ public class SettingsUtil { } } } + getRingtoneName(context, handler, ringtoneUri, msg, defaultRingtone); + } + + public static void getRingtoneName(Context context, Handler handler, Uri ringtoneUri, int msg) { + getRingtoneName(context, handler, ringtoneUri, msg, false); + } + + public static void getRingtoneName( + Context context, Handler handler, Uri ringtoneUri, int msg, boolean defaultRingtone) { CharSequence summary = context.getString(R.string.ringtone_unknown); // Is it a silent ringtone? if (ringtoneUri == null) { diff --git a/java/com/android/dialer/util/ViewUtil.java b/java/com/android/dialer/util/ViewUtil.java index de08e41a7..81a32f985 100644 --- a/java/com/android/dialer/util/ViewUtil.java +++ b/java/com/android/dialer/util/ViewUtil.java @@ -27,6 +27,7 @@ import android.text.TextUtils; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.ViewTreeObserver.OnPreDrawListener; import android.widget.TextView; import java.util.Locale; @@ -113,6 +114,18 @@ public class ViewUtil { }); } + public static void doOnGlobalLayout(@NonNull final View view, final ViewRunnable runnable) { + view.getViewTreeObserver() + .addOnGlobalLayoutListener( + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + view.getViewTreeObserver().removeOnGlobalLayoutListener(this); + runnable.run(view); + } + }); + } + /** * Returns {@code true} if animations should be disabled. * diff --git a/java/com/android/dialer/widget/MessageFragment.java b/java/com/android/dialer/widget/MessageFragment.java new file mode 100644 index 000000000..ab47f2463 --- /dev/null +++ b/java/com/android/dialer/widget/MessageFragment.java @@ -0,0 +1,172 @@ +/* + * 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.widget; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.android.dialer.common.Assert; +import com.android.dialer.common.FragmentUtils; + +/** Fragment used to compose call with message fragment. */ +public class MessageFragment extends Fragment implements OnClickListener, TextWatcher { + private static final String CHAR_LIMIT_KEY = "char_limit"; + private static final String SHOW_SEND_ICON_KEY = "show_send_icon"; + private static final String MESSAGE_LIST_KEY = "message_list"; + + public static final int NO_CHAR_LIMIT = -1; + + private EditText customMessage; + private ImageView sendMessage; + private TextView remainingChar; + private int charLimit; + + private static MessageFragment newInstance(Builder builder) { + MessageFragment fragment = new MessageFragment(); + Bundle args = new Bundle(); + args.putInt(CHAR_LIMIT_KEY, builder.charLimit); + args.putBoolean(SHOW_SEND_ICON_KEY, builder.showSendIcon); + args.putStringArray(MESSAGE_LIST_KEY, builder.messages); + fragment.setArguments(args); + return fragment; + } + + @Nullable + public String getMessage() { + return customMessage == null ? null : customMessage.getText().toString(); + } + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_message, container, false); + + sendMessage = (ImageView) view.findViewById(R.id.send_message); + if (getArguments().getBoolean(SHOW_SEND_ICON_KEY, false)) { + sendMessage.setVisibility(View.VISIBLE); + sendMessage.setEnabled(false); + sendMessage.setOnClickListener(this); + } + + customMessage = (EditText) view.findViewById(R.id.custom_message); + customMessage.addTextChangedListener(this); + charLimit = getArguments().getInt(CHAR_LIMIT_KEY, NO_CHAR_LIMIT); + if (charLimit != NO_CHAR_LIMIT) { + remainingChar = (TextView) view.findViewById(R.id.remaining_characters); + remainingChar.setVisibility(View.VISIBLE); + remainingChar = (TextView) view.findViewById(R.id.remaining_characters); + remainingChar.setText("" + charLimit); + customMessage.setFilters(new InputFilter[] {new InputFilter.LengthFilter(charLimit)}); + } + + LinearLayout messageContainer = (LinearLayout) view.findViewById(R.id.message_container); + for (String message : getArguments().getStringArray(MESSAGE_LIST_KEY)) { + TextView textView = (TextView) inflater.inflate(R.layout.selectable_text_view, null); + textView.setOnClickListener(this); + textView.setText(message); + messageContainer.addView(textView); + } + return view; + } + + @Override + public void onClick(View view) { + if (view == sendMessage) { + getListener().onMessageFragmentSendMessage(customMessage.getText().toString()); + } else if (view.getId() == R.id.selectable_text_view) { + customMessage.setText(((TextView) view).getText()); + customMessage.setSelection(customMessage.getText().length()); + } else { + Assert.fail("Unknown view clicked"); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + sendMessage.setEnabled(s.length() > 0); + } + + @Override + public void afterTextChanged(Editable s) { + if (charLimit != NO_CHAR_LIMIT) { + remainingChar.setText("" + (charLimit - s.length())); + } + getListener().onMessageFragmentAfterTextChange(s.toString()); + } + + private Listener getListener() { + return FragmentUtils.getParentUnsafe(this, Listener.class); + } + + public static Builder builder() { + return new Builder(); + } + + /** Builder for {@link MessageFragment}. */ + public static class Builder { + private String[] messages; + private boolean showSendIcon; + private int charLimit = NO_CHAR_LIMIT; + + /** + * @throws NullPointerException if message is null + * @throws IllegalArgumentException if messages.length is outside the range [1,3]. + */ + public Builder setMessages(String... messages) { + // Since we only allow up to 3 messages, crash if more are set. + Assert.checkArgument(messages.length > 0 && messages.length <= 3); + this.messages = messages; + return this; + } + + public Builder showSendIcon() { + showSendIcon = true; + return this; + } + + public Builder setCharLimit(int charLimit) { + this.charLimit = charLimit; + return this; + } + + public MessageFragment build() { + return MessageFragment.newInstance(this); + } + } + + /** Interface for parent activity to implement to listen for important events. */ + public interface Listener { + void onMessageFragmentSendMessage(String message); + + void onMessageFragmentAfterTextChange(String message); + } +} diff --git a/java/com/android/dialer/widget/res/color/dialer_tint_state.xml b/java/com/android/dialer/widget/res/color/dialer_tint_state.xml new file mode 100644 index 000000000..c29f334ac --- /dev/null +++ b/java/com/android/dialer/widget/res/color/dialer_tint_state.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2015 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="@color/dialer_edit_text_hint_color" android:state_enabled="false"/> + <item android:color="@color/dialer_theme_color"/> +</selector>
\ No newline at end of file diff --git a/java/com/android/dialer/widget/res/layout/fragment_message.xml b/java/com/android/dialer/widget/res/layout/fragment_message.xml new file mode 100644 index 000000000..f09c54f57 --- /dev/null +++ b/java/com/android/dialer/widget/res/layout/fragment_message.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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 + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="bottom" + android:background="@color/background_dialer_white"> + + <LinearLayout + android:id="@+id/message_container" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + <View + android:layout_width="match_parent" + android:layout_height="@dimen/message_divider_height" + android:background="#12000000"/> + + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <EditText + android:id="@+id/custom_message" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/textview_item_padding" + android:textSize="@dimen/message_item_text_size" + android:hint="@string/custom_message_hint" + android:textColor="@color/dialer_primary_text_color" + android:textColorHint="@color/dialer_edit_text_hint_color" + android:background="@color/background_dialer_white" + android:textCursorDrawable="@drawable/searchedittext_custom_cursor" + android:layout_toStartOf="@+id/count_and_send_container"/> + + <LinearLayout + android:id="@+id/count_and_send_container" + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:layout_marginEnd="@dimen/textview_item_padding" + android:gravity="center"> + + <ImageView + android:id="@+id/send_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:src="@drawable/quantum_ic_send_white_24" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:tint="@color/dialer_tint_state"/> + + <TextView + android:id="@+id/remaining_characters" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:textSize="@dimen/message_remaining_char_text_size" + android:textColor="@color/dialer_edit_text_hint_color"/> + </LinearLayout> + </RelativeLayout> +</LinearLayout>
\ No newline at end of file diff --git a/java/com/android/dialer/widget/res/layout/selectable_text_view.xml b/java/com/android/dialer/widget/res/layout/selectable_text_view.xml new file mode 100644 index 000000000..3d120d13d --- /dev/null +++ b/java/com/android/dialer/widget/res/layout/selectable_text_view.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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 + --> +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/selectable_text_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="16sp" + android:textColor="@color/dialer_primary_text_color" + android:padding="16dp" + android:background="@drawable/item_background_material_light"/>
\ No newline at end of file diff --git a/java/com/android/dialer/widget/res/values/dimens.xml b/java/com/android/dialer/widget/res/values/dimens.xml new file mode 100644 index 000000000..6c4ea604f --- /dev/null +++ b/java/com/android/dialer/widget/res/values/dimens.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2016 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> + <!-- Message Fragment --> + <dimen name="message_item_text_size">16sp</dimen> + <dimen name="textview_item_padding">16dp</dimen> + <dimen name="message_remaining_char_text_size">12sp</dimen> + <dimen name="message_divider_height">1dp</dimen> +</resources>
\ No newline at end of file diff --git a/java/com/android/dialer/widget/res/values/strings.xml b/java/com/android/dialer/widget/res/values/strings.xml new file mode 100644 index 000000000..6904c2de1 --- /dev/null +++ b/java/com/android/dialer/widget/res/values/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Hint in a text field to compose a custom message to send with a phone call [CHAR LIMIT=27] --> + <string name="custom_message_hint">Write a custom message</string> +</resources>
\ No newline at end of file |