diff options
Diffstat (limited to 'src/com/android/dialer')
22 files changed, 863 insertions, 108 deletions
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java index 72a50127e..d95b55ecb 100644 --- a/src/com/android/dialer/CallDetailActivity.java +++ b/src/com/android/dialer/CallDetailActivity.java @@ -31,6 +31,7 @@ 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; @@ -39,6 +40,7 @@ import android.widget.Toast; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.interactions.TouchPointManager; import com.android.contacts.common.GeoUtil; import com.android.contacts.common.CallUtil; import com.android.contacts.common.util.UriUtils; @@ -262,6 +264,14 @@ public class CallDetailActivity extends AppCompatActivity 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, getCallLogEntryUris(), mCallLogAsyncTaskListener); } diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java index dbff276e5..f8fb17f6b 100644 --- a/src/com/android/dialer/DialtactsActivity.java +++ b/src/com/android/dialer/DialtactsActivity.java @@ -83,6 +83,7 @@ import com.android.dialer.list.SmartDialSearchFragment; import com.android.dialer.list.SpeedDialFragment; import com.android.dialer.settings.DialerSettingsActivity; import com.android.dialer.util.IntentUtil; +import com.android.dialer.util.TelecomUtil; import com.android.dialer.util.IntentUtil.CallIntentBuilder; import com.android.dialer.util.DialerUtils; import com.android.dialer.widget.ActionBarController; @@ -158,6 +159,8 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O */ private SmartDialSearchFragment mSmartDialSearchFragment; + private boolean mIsVisible; + /** * Animation that slides in. */ @@ -563,6 +566,25 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O } @Override + protected void onStart() { + super.onStart(); + mIsVisible = true; + } + + @Override + protected void onStop() { + mIsVisible = false; + super.onStop(); + } + + /** + * Returns true when the Activity is currently visible (between onStart and onStop). + */ + /* package */ boolean isVisible() { + return mIsVisible; + } + + @Override protected void onPause() { if (mClearSearchOnPause) { hideDialpadAndSearchUi(); @@ -576,6 +598,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O @Override protected void onSaveInstanceState(Bundle outState) { + mIsVisible = false; super.onSaveInstanceState(outState); outState.putString(KEY_SEARCH_QUERY, mSearchQuery); outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch); @@ -648,6 +671,10 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O @Override public boolean onMenuItemClick(MenuItem item) { + if (!isVisible()) { + return true; + } + switch (item.getItemId()) { case R.id.menu_history: // Use explicit CallLogActivity intent instead of ACTION_VIEW + @@ -1182,7 +1209,7 @@ public class DialtactsActivity extends TransactionSafeActivity implements View.O } private boolean phoneIsInUse() { - return getTelecomManager().isInCall(); + return TelecomUtil.isInCall(this); } private boolean canIntentBeHandled(Intent intent) { diff --git a/src/com/android/dialer/calllog/CallLogActivity.java b/src/com/android/dialer/calllog/CallLogActivity.java index ff0726df7..ad795f9bd 100644 --- a/src/com/android/dialer/calllog/CallLogActivity.java +++ b/src/com/android/dialer/calllog/CallLogActivity.java @@ -48,6 +48,8 @@ public class CallLogActivity extends AppCompatActivity implements ViewPager.OnPa private CallLogFragment mAllCallsFragment; private CallLogFragment mMissedCallsFragment; + private boolean mIsVisible; + private String[] mTabTitles; private static final int TAB_INDEX_ALL = 0; @@ -161,6 +163,31 @@ public class CallLogActivity extends AppCompatActivity implements ViewPager.OnPa } @Override + protected void onStart() { + super.onStart(); + mIsVisible = false; + } + + @Override + protected void onStop() { + mIsVisible = false; + super.onStop(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + mIsVisible = false; + super.onSaveInstanceState(outState); + } + + /** + * Returns true when the Activity is currently visible (between onStart and onStop). + */ + /* package */ boolean isVisible() { + return mIsVisible; + } + + @Override public boolean onCreateOptionsMenu(Menu menu) { final MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.call_log_options, menu); @@ -180,6 +207,10 @@ public class CallLogActivity extends AppCompatActivity implements ViewPager.OnPa @Override public boolean onOptionsItemSelected(MenuItem item) { + if (!isVisible()) { + return true; + } + switch (item.getItemId()) { case android.R.id.home: final Intent intent = new Intent(this, DialtactsActivity.class); diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java index 26e39651b..b7f068e1f 100644 --- a/src/com/android/dialer/calllog/CallLogFragment.java +++ b/src/com/android/dialer/calllog/CallLogFragment.java @@ -362,6 +362,10 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis mAdapter.startCache(); rescheduleDisplayUpdate(); + + if (mVoicemailPlaybackPresenter != null) { + mVoicemailPlaybackPresenter.onResume(); + } } @Override diff --git a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java index 521b2a429..8b2eb6081 100644 --- a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java +++ b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java @@ -291,25 +291,22 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder .setOnMenuItemClickListener(this); } - try { - mFilteredNumberAsyncQueryHandler.isBlocked( - new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() { - @Override - public void onCheckComplete(Integer id) { - blockId = id; - int blockTitleId = blockId == null ? R.string.call_log_block_number - : R.string.call_log_unblock_number; - final MenuItem blockItem = menu.add( - ContextMenu.NONE, - R.id.context_menu_block_number, - ContextMenu.NONE, - blockTitleId); - blockItem.setOnMenuItemClickListener( - CallLogListItemViewHolder.this); - } - }, info.normalizedNumber, number, countryIso); - } catch (IllegalArgumentException e) { - } + mFilteredNumberAsyncQueryHandler.startBlockedQuery( + new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() { + @Override + public void onCheckComplete(Integer id) { + blockId = id; + int blockTitleId = blockId == null ? R.string.call_log_block_number + : R.string.call_log_unblock_number; + final MenuItem blockItem = menu.add( + ContextMenu.NONE, + R.id.context_menu_block_number, + ContextMenu.NONE, + blockTitleId); + blockItem.setOnMenuItemClickListener( + CallLogListItemViewHolder.this); + } + }, info.normalizedNumber, number, countryIso); } @Override @@ -320,6 +317,8 @@ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder FilterNumberDialogFragment.newInstance(blockId, info.normalizedNumber, number, countryIso, info.formattedNumber); newFragment.setQueryHandler(mFilteredNumberAsyncQueryHandler); + newFragment.setParentView( + ((Activity) mContext).findViewById(R.id.floating_action_button_container)); newFragment.show(((Activity) mContext).getFragmentManager(), FilterNumberDialogFragment.BLOCK_DIALOG_FRAGMENT); return true; diff --git a/src/com/android/dialer/database/DialerDatabaseHelper.java b/src/com/android/dialer/database/DialerDatabaseHelper.java index 413e8673f..cc8631d99 100644 --- a/src/com/android/dialer/database/DialerDatabaseHelper.java +++ b/src/com/android/dialer/database/DialerDatabaseHelper.java @@ -75,7 +75,7 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { * 0-98 KitKat * </pre> */ - public static final int DATABASE_VERSION = 6; + public static final int DATABASE_VERSION = 7; public static final String DATABASE_NAME = "dialer.db"; /** @@ -457,7 +457,7 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { return; } - if (oldVersion < 6) { + if (oldVersion < 7) { db.execSQL("DROP TABLE IF EXISTS " + Tables.FILTERED_NUMBER_TABLE); db.execSQL("CREATE TABLE " + Tables.FILTERED_NUMBER_TABLE + " (" + FilteredNumberColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," @@ -470,7 +470,7 @@ public class DialerDatabaseHelper extends SQLiteOpenHelper { + FilteredNumberColumns.TYPE + " INTEGER," + FilteredNumberColumns.SOURCE + " INTEGER" + ");"); - oldVersion = 6; + oldVersion = 7; } if (oldVersion != DATABASE_VERSION) { diff --git a/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java b/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java index 2fdea0d13..9da9cc15b 100644 --- a/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java +++ b/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java @@ -26,6 +26,7 @@ import android.database.sqlite.SQLiteDatabaseCorruptException; import android.net.Uri; import android.telephony.PhoneNumberUtils; +import com.android.contacts.common.util.PhoneNumberHelper; import com.android.dialer.database.FilteredNumberContract.FilteredNumber; import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources; @@ -79,22 +80,30 @@ public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler { @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { - ((Listener) cookie).onQueryComplete(token, cookie, cursor); + if (cookie != null) { + ((Listener) cookie).onQueryComplete(token, cookie, cursor); + } } @Override protected void onInsertComplete(int token, Object cookie, Uri uri) { - ((Listener) cookie).onInsertComplete(token, cookie, uri); + if (cookie != null) { + ((Listener) cookie).onInsertComplete(token, cookie, uri); + } } @Override protected void onUpdateComplete(int token, Object cookie, int result) { - ((Listener) cookie).onUpdateComplete(token, cookie, result); + if (cookie != null) { + ((Listener) cookie).onUpdateComplete(token, cookie, result); + } } @Override protected void onDeleteComplete(int token, Object cookie, int result) { - ((Listener) cookie).onDeleteComplete(token, cookie, result); + if (cookie != null) { + ((Listener) cookie).onDeleteComplete(token, cookie, result); + } } private static Uri getContentUri(Integer id) { @@ -105,26 +114,43 @@ public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler { return uri; } + public final void incrementFilteredCount(Integer id) { + startUpdate(NO_TOKEN, null, + ContentUris.withAppendedId(FilteredNumber.CONTENT_URI_INCREMENT_FILTERED_COUNT, id), + null, null, null); + } + /** * Check if the number + country iso given has been blocked. * This method normalizes the number for the lookup if normalizedNumber is null. + * @return {@code true} if the number was invalid and couldn't be checked, + * {@code false} otherwise, */ - public final void isBlocked(final OnCheckBlockedListener listener, - String normalizedNumber, String number, String countryIso) { + public final boolean startBlockedQuery(final OnCheckBlockedListener listener, + String normalizedNumber, String number, String countryIso) { if (normalizedNumber == null) { - normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso); + normalizedNumber = getNormalizedNumber(number, countryIso); if (normalizedNumber == null) { - throw new IllegalArgumentException("Invalid phone number"); + return true; } } - isBlocked(listener, normalizedNumber); + startBlockedQuery(listener, normalizedNumber); + return false; + } + + public static String getNormalizedNumber(String number, String countryIso) { + if (PhoneNumberHelper.isUriNumber(number)) { + return number; + } else { + return PhoneNumberUtils.formatNumberToE164(number, countryIso); + } } /** * Check if the normalized number given has been blocked. */ - public final void isBlocked(final OnCheckBlockedListener listener, - String normalizedNumber) { + public final void startBlockedQuery(final OnCheckBlockedListener listener, + String normalizedNumber) { startQuery(NO_TOKEN, new Listener() { @Override @@ -156,7 +182,7 @@ public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler { public final void blockNumber(final OnBlockNumberListener listener, String normalizedNumber, String number, String countryIso) { if (normalizedNumber == null) { - normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso); + normalizedNumber = getNormalizedNumber(number, countryIso); } ContentValues v = new ContentValues(); v.put(FilteredNumberColumns.NORMALIZED_NUMBER, normalizedNumber); @@ -183,8 +209,8 @@ public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler { /** * Removes row from database. - * Caller should call {@link FilteredNumberAsyncQueryHandler#isBlocked} first. - * @param id The ID of row to remove, from {@link FilteredNumberAsyncQueryHandler#isBlocked}. + * Caller should call {@link FilteredNumberAsyncQueryHandler#startBlockedQuery} first. + * @param id The ID of row to remove, from {@link FilteredNumberAsyncQueryHandler#startBlockedQuery}. */ public final void unblock(final OnUnblockNumberListener listener, Integer id) { if (id == null) { diff --git a/src/com/android/dialer/database/FilteredNumberContract.java b/src/com/android/dialer/database/FilteredNumberContract.java index 0ec171b15..f3966816c 100644 --- a/src/com/android/dialer/database/FilteredNumberContract.java +++ b/src/com/android/dialer/database/FilteredNumberContract.java @@ -16,13 +16,8 @@ package com.android.dialer.database; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.database.Cursor; import android.net.Uri; import android.provider.BaseColumns; -import android.telephony.PhoneNumberUtils; import com.android.dialerbind.ObjectFactory; @@ -61,7 +56,7 @@ public final class FilteredNumberContract { public interface FilteredNumberColumns { // TYPE: INTEGER - static final String _ID = "id"; + static final String _ID = "_id"; /** * Represents the number to be filtered, normalized to compare phone numbers for equality. * @@ -137,11 +132,17 @@ public final class FilteredNumberContract { public static class FilteredNumber implements BaseColumns { public static final String FILTERED_NUMBERS_TABLE = "filtered_numbers_table"; + public static final String FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT = + "filtered_numbers_increment_filtered_count"; public static final Uri CONTENT_URI = Uri.withAppendedPath( AUTHORITY_URI, FILTERED_NUMBERS_TABLE); + public static final Uri CONTENT_URI_INCREMENT_FILTERED_COUNT = Uri.withAppendedPath( + AUTHORITY_URI, + FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT); + /** * This utility class cannot be instantiated. */ diff --git a/src/com/android/dialer/database/FilteredNumberProvider.java b/src/com/android/dialer/database/FilteredNumberProvider.java index 2bea7cae1..acb5e7f67 100644 --- a/src/com/android/dialer/database/FilteredNumberProvider.java +++ b/src/com/android/dialer/database/FilteredNumberProvider.java @@ -22,12 +22,12 @@ import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; +import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Binder; import android.text.TextUtils; -import android.text.format.Time; import android.util.Log; import com.android.contacts.common.GeoUtil; @@ -49,6 +49,7 @@ public class FilteredNumberProvider extends ContentProvider { private static final int FILTERED_NUMBERS_TABLE = 1; private static final int FILTERED_NUMBERS_TABLE_ID = 2; + private static final int FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT = 3; private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); @@ -64,6 +65,10 @@ public class FilteredNumberProvider extends ContentProvider { sUriMatcher.addURI(ObjectFactory.getFilteredNumberProviderAuthority(), FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_TABLE + "/#", FILTERED_NUMBERS_TABLE_ID); + sUriMatcher.addURI(ObjectFactory.getFilteredNumberProviderAuthority(), + FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT + + "/#", + FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT); return true; } @@ -121,9 +126,7 @@ public class FilteredNumberProvider extends ContentProvider { @VisibleForTesting protected long getCurrentTimeMs() { - Time timeNow = new Time(); - timeNow.setToNow(); - return timeNow.toMillis(false); + return System.currentTimeMillis(); } private void setDefaultValues(ContentValues values) { @@ -171,6 +174,20 @@ public class FilteredNumberProvider extends ContentProvider { case FILTERED_NUMBERS_TABLE_ID: selection = getSelectionWithId(selection, ContentUris.parseId(uri)); break; + case FILTERED_NUMBERS_INCREMENT_FILTERED_COUNT: + final long id = ContentUris.parseId(uri); + try { + db.execSQL(" UPDATE " + DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE + + " SET" + FilteredNumberColumns.TIMES_FILTERED + "=" + + FilteredNumberColumns.TIMES_FILTERED + "+1," + + FilteredNumberColumns.LAST_TIME_FILTERED + "=" + + getCurrentTimeMs() + + " WHERE " + FilteredNumberColumns._ID + "=" + id); + } catch (SQLException e) { + Log.d(TAG, "Could not update blocked statistics for " + id); + return 0; + } + return 1; default: throw new IllegalArgumentException("Unknown uri: " + uri); } diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java index d2628da5e..54e4b8946 100644 --- a/src/com/android/dialer/dialpad/DialpadFragment.java +++ b/src/com/android/dialer/dialpad/DialpadFragment.java @@ -81,6 +81,7 @@ import com.android.dialer.R; import com.android.dialer.SpecialCharSequenceMgr; import com.android.dialer.calllog.PhoneAccountUtils; import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.TelecomUtil; import com.android.dialer.util.IntentUtil.CallIntentBuilder; import com.android.incallui.Call.LogState; import com.android.phone.common.CallLogAsync; @@ -1478,8 +1479,12 @@ public class DialpadFragment extends Fragment * @return true if the phone is "in use", meaning that at least one line * is active (ie. off hook or ringing or dialing, or on hold). */ - public boolean isPhoneInUse() { - return getTelecomManager().isInCall(); + private boolean isPhoneInUse() { + final Context context = getActivity(); + if (context != null) { + return TelecomUtil.isInCall(context); + } + return false; } /** diff --git a/src/com/android/dialer/filterednumber/BlockedNumberAdapter.java b/src/com/android/dialer/filterednumber/BlockedNumberAdapter.java new file mode 100644 index 000000000..504b5205b --- /dev/null +++ b/src/com/android/dialer/filterednumber/BlockedNumberAdapter.java @@ -0,0 +1,148 @@ +/* + * 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. + */ +package com.android.dialer.filterednumber; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.provider.ContactsContract; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.view.View; +import android.widget.QuickContactBadge; +import android.widget.SimpleCursorAdapter; +import android.widget.TextView; + +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.R; +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.calllog.ContactInfoHelper; +import com.android.dialer.database.FilteredNumberAsyncQueryHandler; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; +import com.android.dialer.util.PhoneNumberUtil; + +public class BlockedNumberAdapter extends SimpleCursorAdapter { + + private Context mContext; + private ContactInfoHelper mContactInfoHelper; + private Resources mResources; + private BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); + private ContactPhotoManager mContactPhotoManager; + private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + + public BlockedNumberAdapter(Context context, + FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) { + super(context, R.layout.blocked_number_item, null, new String[]{}, new int[]{}, 0); + mContext = context; + mContactInfoHelper = new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)); + mContactPhotoManager = ContactPhotoManager.getInstance(context); + mResources = context.getResources(); + mFilteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + super.bindView(view, context, cursor); + final TextView callerName = (TextView) view.findViewById(R.id.caller_name); + final TextView callerNumber = (TextView) view.findViewById(R.id.caller_number); + final View deleteNumber = view.findViewById(R.id.delete_button); + final QuickContactBadge quickContactBadge = + (QuickContactBadge) view.findViewById(R.id.quick_contact_photo); + quickContactBadge.setOverlay(null); + quickContactBadge.setPrioritizedMimeType( + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE); + + final Integer id = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)); + final String countryIso = cursor.getString(cursor.getColumnIndex( + FilteredNumberColumns.COUNTRY_ISO)); + final String number = cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NUMBER)); + final String normalizedNumber = cursor.getString(cursor.getColumnIndex( + FilteredNumberColumns.NORMALIZED_NUMBER)); + final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso); + final CharSequence locationOrType = getNumberTypeOrLocation(info); + final String displayNumber = getDisplayNumber(info); + final String displayNumberStr = mBidiFormatter.unicodeWrap( + displayNumber.toString(), TextDirectionHeuristics.LTR); + + deleteNumber.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + FilterNumberDialogFragment newFragment = + FilterNumberDialogFragment.newInstance(id, normalizedNumber, number, + countryIso, displayNumber); + newFragment.setQueryHandler(mFilteredNumberAsyncQueryHandler); + newFragment.setParentView(view); + newFragment.show(((Activity) mContext).getFragmentManager(), + FilterNumberDialogFragment.BLOCK_DIALOG_FRAGMENT); + } + }); + + String nameForDefaultImage; + if (!TextUtils.isEmpty(info.name)) { + nameForDefaultImage = info.name; + callerName.setText(info.name); + callerNumber.setText(locationOrType + " " + displayNumberStr); + } else { + nameForDefaultImage = displayNumber; + callerName.setText(displayNumberStr); + if (!TextUtils.isEmpty(locationOrType)) { + callerNumber.setText(locationOrType); + callerNumber.setVisibility(View.VISIBLE); + } else { + callerNumber.setVisibility(View.GONE); + } + } + loadContactPhoto(info, nameForDefaultImage, quickContactBadge); + } + + private void loadContactPhoto(ContactInfo info, String displayName, QuickContactBadge badge) { + final String lookupKey = info.lookupUri == null + ? null : UriUtils.getLookupKeyFromUri(info.lookupUri); + final int contactType = mContactInfoHelper.isBusiness(info.sourceType) + ? ContactPhotoManager.TYPE_BUSINESS : ContactPhotoManager.TYPE_DEFAULT; + final DefaultImageRequest request = new DefaultImageRequest(displayName, lookupKey, + contactType, true /* isCircular */); + badge.assignContactUri(info.lookupUri); + badge.setContentDescription( + mResources.getString(R.string.description_contact_details, displayName)); + mContactPhotoManager.loadDirectoryPhoto(badge, info.photoUri, + false /* darkTheme */, true /* isCircular */, request); + } + + private String getDisplayNumber(ContactInfo info) { + if (!TextUtils.isEmpty(info.formattedNumber)) { + return info.formattedNumber; + } else if (!TextUtils.isEmpty(info.number)) { + return info.number; + } else { + return ""; + } + } + + private CharSequence getNumberTypeOrLocation(ContactInfo info) { + if (!TextUtils.isEmpty(info.name)) { + return ContactsContract.CommonDataKinds.Phone.getTypeLabel(mResources, info.type, + info.label); + } else { + return PhoneNumberUtil.getGeoDescription(mContext, info.number); + } + } +} diff --git a/src/com/android/dialer/filterednumber/BlockedNumberFragment.java b/src/com/android/dialer/filterednumber/BlockedNumberFragment.java new file mode 100644 index 000000000..69fba34d4 --- /dev/null +++ b/src/com/android/dialer/filterednumber/BlockedNumberFragment.java @@ -0,0 +1,154 @@ +/* + * 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. + */ +package com.android.dialer.filterednumber; + +import android.app.AlertDialog; +import android.app.ListFragment; +import android.app.LoaderManager; +import android.content.CursorLoader; +import android.content.DialogInterface; +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.dialog.IndeterminateProgressDialog; +import com.android.dialer.R; +import com.android.dialer.database.FilteredNumberAsyncQueryHandler; +import com.android.dialer.database.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener; +import com.android.dialer.database.FilteredNumberContract; + +public class BlockedNumberFragment extends ListFragment implements + LoaderManager.LoaderCallbacks<Cursor>, View.OnClickListener { + + private BlockedNumberAdapter mAdapter; + private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + LayoutInflater inflater = LayoutInflater.from(getContext()); + getListView().addHeaderView(inflater.inflate(R.layout.blocked_number_header, null)); + mFilteredNumberAsyncQueryHandler = + new FilteredNumberAsyncQueryHandler(getActivity().getContentResolver()); + if (mAdapter == null) { + mAdapter = new BlockedNumberAdapter(getContext(), mFilteredNumberAsyncQueryHandler); + } + setListAdapter(mAdapter); + final Button addNumberBtn = (Button) getActivity().findViewById(R.id.add_number_button); + addNumberBtn.setOnClickListener(this); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + setListAdapter(null); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getLoaderManager().initLoader(0, null, this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.blocked_number_fragment, container, false); + return view; + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + final String[] projection = { + FilteredNumberContract.FilteredNumberColumns._ID, + FilteredNumberContract.FilteredNumberColumns.COUNTRY_ISO, + FilteredNumberContract.FilteredNumberColumns.NUMBER, + FilteredNumberContract.FilteredNumberColumns.NORMALIZED_NUMBER + }; + final String selection = FilteredNumberContract.FilteredNumberColumns.TYPE + + "=" + FilteredNumberContract.FilteredNumberTypes.BLOCKED_NUMBER; + final CursorLoader cursorLoader = new CursorLoader( + getContext(), FilteredNumberContract.FilteredNumber.CONTENT_URI, projection, + selection, null, null); + return cursorLoader; + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + mAdapter.swapCursor(data); + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + mAdapter.swapCursor(null); + } + + @Override + public void onClick(final View v) { + final String countryIso = GeoUtil.getCurrentCountryIso(getContext()); + final EditText numberField = new EditText(getContext()); + final DialogInterface.OnClickListener okListener = new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + final String number = numberField.getText().toString(); + final IndeterminateProgressDialog progressDialog = + IndeterminateProgressDialog.show(getFragmentManager(), + getString(R.string.checkingNumber, number), null, 1000); + final String normalizedNumber = + FilteredNumberAsyncQueryHandler.getNormalizedNumber(number, countryIso); + if (normalizedNumber == null) { + progressDialog.dismiss(); + Toast.makeText(getContext(), getString(R.string.invalidNumber, number), + Toast.LENGTH_LONG).show(); + } else { + final OnCheckBlockedListener onCheckListener = new OnCheckBlockedListener() { + @Override + public void onCheckComplete(Integer id) { + progressDialog.dismiss(); + if (id == null) { + FilterNumberDialogFragment newFragment = + FilterNumberDialogFragment.newInstance(id, normalizedNumber, + number, countryIso, number); + newFragment.setQueryHandler(mFilteredNumberAsyncQueryHandler); + newFragment.setParentView(v); + newFragment.show(getActivity().getFragmentManager(), + FilterNumberDialogFragment.BLOCK_DIALOG_FRAGMENT); + } else { + Toast.makeText(getContext(), + getString(R.string.alreadyBlocked, number), + Toast.LENGTH_LONG).show(); + } + } + }; + mFilteredNumberAsyncQueryHandler.startBlockedQuery( + onCheckListener, normalizedNumber, number, countryIso); + } + } + }; + new AlertDialog.Builder(getContext()) + .setTitle(getString(R.string.blockNumber)) + .setView(numberField) + .setPositiveButton(getString(R.string.blockNumberOk), okListener) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } +}
\ No newline at end of file diff --git a/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java b/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java index f94d0f842..e9a88c845 100644 --- a/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java +++ b/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java @@ -39,11 +39,16 @@ public class FilterNumberDialogFragment extends DialogFragment { private static final String ARG_DISPLAY_NUMBER = "argDisplayNumber"; private FilteredNumberAsyncQueryHandler mHandler; + private View mParentView; public void setQueryHandler (FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) { mHandler = filteredNumberAsyncQueryHandler; } + public void setParentView(View view) { + mParentView = view; + } + public static FilterNumberDialogFragment newInstance(Integer blockId, String normalizedNumber, String number, String countryIso, String displayNumber) { final FilterNumberDialogFragment fragment = new FilterNumberDialogFragment(); @@ -91,7 +96,6 @@ public class FilterNumberDialogFragment extends DialogFragment { } public void blockNumber() { - final View view = getActivity().findViewById(R.id.floating_action_button_container); final String displayNumber = getArguments().getString(ARG_DISPLAY_NUMBER); final String message = getString(R.string.snackbar_number_blocked, displayNumber); final String undoMessage = getString(R.string.snackbar_number_unblocked, displayNumber); @@ -99,7 +103,7 @@ public class FilterNumberDialogFragment extends DialogFragment { new FilteredNumberAsyncQueryHandler.OnUnblockNumberListener() { @Override public void onUnblockComplete(int rows, ContentValues values) { - Snackbar.make(view, undoMessage, Snackbar.LENGTH_LONG).show(); + Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show(); } }; @@ -107,7 +111,7 @@ public class FilterNumberDialogFragment extends DialogFragment { new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() { @Override public void onBlockComplete(final Uri uri) { - Snackbar.make(view, message, Snackbar.LENGTH_LONG) + Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG) .setAction(R.string.block_number_undo, // Delete the newly created row on 'undo'. new View.OnClickListener() { @@ -123,7 +127,6 @@ public class FilterNumberDialogFragment extends DialogFragment { } public void unblockNumber() { - final View view = getActivity().findViewById(R.id.floating_action_button_container); final String displayNumber = getArguments().getString(ARG_DISPLAY_NUMBER); final String message = getString(R.string.snackbar_number_unblocked, displayNumber); final String undoMessage = getString(R.string.snackbar_number_blocked, displayNumber); @@ -131,14 +134,14 @@ public class FilterNumberDialogFragment extends DialogFragment { new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() { @Override public void onBlockComplete(final Uri uri) { - Snackbar.make(view, undoMessage, Snackbar.LENGTH_LONG).show(); + Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show(); } }; mHandler.unblock( new FilteredNumberAsyncQueryHandler.OnUnblockNumberListener() { @Override public void onUnblockComplete(int rows, final ContentValues values) { - Snackbar.make(view, message, Snackbar.LENGTH_LONG) + Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG) .setAction(R.string.block_number_undo, new View.OnClickListener() { // Re-insert the row on 'undo', with a new ID. diff --git a/src/com/android/dialer/list/RegularSearchFragment.java b/src/com/android/dialer/list/RegularSearchFragment.java index 5c3117d89..ec771e8ef 100644 --- a/src/com/android/dialer/list/RegularSearchFragment.java +++ b/src/com/android/dialer/list/RegularSearchFragment.java @@ -15,12 +15,10 @@ */ package com.android.dialer.list; -import static android.Manifest.permission.ACCESS_FINE_LOCATION; import static android.Manifest.permission.READ_CONTACTS; import android.app.Activity; import android.content.pm.PackageManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.ViewGroup; diff --git a/src/com/android/dialer/settings/DialerSettingsActivity.java b/src/com/android/dialer/settings/DialerSettingsActivity.java index 01a9fcf5f..2b7277224 100644 --- a/src/com/android/dialer/settings/DialerSettingsActivity.java +++ b/src/com/android/dialer/settings/DialerSettingsActivity.java @@ -15,24 +15,20 @@ */ package com.android.dialer.settings; -import android.app.AppOpsManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import android.os.Process; import android.os.UserManager; -import android.preference.PreferenceActivity; import android.preference.PreferenceManager; import android.provider.Settings; import android.telecom.TelecomManager; import android.telephony.TelephonyManager; -import android.util.Log; import android.view.MenuItem; import android.widget.Toast; -import com.android.contacts.common.util.PermissionsUtil; import com.android.dialer.R; +import com.android.dialer.filterednumber.BlockedNumberFragment; import java.util.List; @@ -90,6 +86,11 @@ public class DialerSettingsActivity extends AppCompatPreferenceActivity { target.add(phoneAccountSettingsHeader); } + Header blockedCallsHeader = new Header(); + blockedCallsHeader.titleRes = R.string.blocked_calls_settings_label; + blockedCallsHeader.fragment = BlockedNumberFragment.class.getName(); + target.add(blockedCallsHeader); + if (telephonyManager.isTtyModeSupported() || telephonyManager.isHearingAidCompatibilitySupported()) { Header accessibilitySettingsHeader = new Header(); diff --git a/src/com/android/dialer/util/DialerUtils.java b/src/com/android/dialer/util/DialerUtils.java index fbe14ba9e..8870f76e4 100644 --- a/src/com/android/dialer/util/DialerUtils.java +++ b/src/com/android/dialer/util/DialerUtils.java @@ -33,18 +33,12 @@ import android.text.TextDirectionHeuristics; import android.text.TextUtils; import android.view.View; import android.view.inputmethod.InputMethodManager; -import android.widget.ImageView; -import android.widget.TextView; import android.widget.Toast; import com.android.contacts.common.ContactsUtils; import com.android.contacts.common.interactions.TouchPointManager; import com.android.dialer.R; -import com.android.dialer.widget.EmptyContentView; -import com.android.incallui.CallCardFragment; -import com.android.incallui.Log; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -94,7 +88,14 @@ public class DialerUtils { } final TelecomManager tm = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); - tm.placeCall(intent.getData(), intent.getExtras()); + if (TelecomUtil.hasCallPhonePermission(context)) { + tm.placeCall(intent.getData(), intent.getExtras()); + } else { + // TODO: Make calling activity show request permission dialog and handle + // callback results appropriately. + Toast.makeText(context, "Cannot place call without Phone permission", + Toast.LENGTH_SHORT); + } } else { context.startActivity(intent); } diff --git a/src/com/android/dialer/util/PhoneNumberUtil.java b/src/com/android/dialer/util/PhoneNumberUtil.java index 84f58aa85..539d8b9ba 100644 --- a/src/com/android/dialer/util/PhoneNumberUtil.java +++ b/src/com/android/dialer/util/PhoneNumberUtil.java @@ -25,13 +25,19 @@ import android.util.Log; import android.util.Pair; import com.android.contacts.common.util.PhoneNumberHelper; +import com.android.contacts.common.util.TelephonyManagerUtils; import com.google.common.collect.Sets; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.Phonenumber; +import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Set; public class PhoneNumberUtil { + private static final String TAG = "PhoneNumberUtil"; private static final Set<String> LEGACY_UNKNOWN_NUMBERS = Sets.newHashSet("-1", "-2", "-3"); /** Returns true if it is possible to place a call to the given number. */ @@ -92,4 +98,41 @@ public class PhoneNumberUtil { public static boolean isLegacyUnknownNumbers(CharSequence number) { return number != null && LEGACY_UNKNOWN_NUMBERS.contains(number.toString()); } + + /** + * @return a geographical description string for the specified number. + * @see com.android.i18n.phonenumbers.PhoneNumberOfflineGeocoder + */ + public static String getGeoDescription(Context context, String number) { + Log.v(TAG, "getGeoDescription('" + number + "')..."); + + if (TextUtils.isEmpty(number)) { + return null; + } + + com.google.i18n.phonenumbers.PhoneNumberUtil util = + com.google.i18n.phonenumbers.PhoneNumberUtil.getInstance(); + PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance(); + + Locale locale = context.getResources().getConfiguration().locale; + String countryIso = TelephonyManagerUtils.getCurrentCountryIso(context, locale); + Phonenumber.PhoneNumber pn = null; + try { + Log.v(TAG, "parsing '" + number + + "' for countryIso '" + countryIso + "'..."); + pn = util.parse(number, countryIso); + Log.v(TAG, "- parsed number: " + pn); + } catch (NumberParseException e) { + Log.v(TAG, "getGeoDescription: NumberParseException for incoming number '" + + number + "'"); + } + + if (pn != null) { + String description = geocoder.getDescriptionForNumber(pn, locale); + Log.v(TAG, "- got description: '" + description + "'"); + return description; + } + + return null; + } } diff --git a/src/com/android/dialer/util/TelecomUtil.java b/src/com/android/dialer/util/TelecomUtil.java index 1cd270c9b..43b9a72a3 100644 --- a/src/com/android/dialer/util/TelecomUtil.java +++ b/src/com/android/dialer/util/TelecomUtil.java @@ -26,6 +26,9 @@ import android.telecom.TelecomManager; import android.text.TextUtils; import android.util.Log; +import java.util.ArrayList; +import java.util.List; + public class TelecomUtil { private static final String TAG = "TelecomUtil"; private static boolean sWarningLogged = false; @@ -78,6 +81,20 @@ public class TelecomUtil { return false; } + public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(Context context) { + if (hasReadPhoneStatePermission(context)) { + return getTelecomManager(context).getCallCapablePhoneAccounts(); + } + return new ArrayList<>(); + } + + public static boolean isInCall(Context context) { + if (hasReadPhoneStatePermission(context)) { + return getTelecomManager(context).isInCall(); + } + return false; + } + public static Uri getCallLogUri(Context context) { return hasReadWriteVoicemailPermissions(context) ? Calls.CONTENT_URI_WITH_VOICEMAIL : Calls.CONTENT_URI; @@ -94,6 +111,16 @@ public class TelecomUtil { || hasPermission(context, Manifest.permission.MODIFY_PHONE_STATE); } + public static boolean hasReadPhoneStatePermission(Context context) { + return isDefaultDialer(context) + || hasPermission(context, Manifest.permission.READ_PHONE_STATE); + } + + public static boolean hasCallPhonePermission(Context context) { + return isDefaultDialer(context) + || hasPermission(context, Manifest.permission.CALL_PHONE); + } + private static boolean hasPermission(Context context, String permission) { return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED; diff --git a/src/com/android/dialer/voicemail/VoicemailAudioManager.java b/src/com/android/dialer/voicemail/VoicemailAudioManager.java index e64e180b6..267eeca09 100644 --- a/src/com/android/dialer/voicemail/VoicemailAudioManager.java +++ b/src/com/android/dialer/voicemail/VoicemailAudioManager.java @@ -19,25 +19,36 @@ package com.android.dialer.voicemail; import android.content.Context; import android.media.AudioManager; import android.media.AudioManager.OnAudioFocusChangeListener; +import android.telecom.CallAudioState; import android.util.Log; +import java.util.Objects; import java.util.concurrent.RejectedExecutionException; /** * This class manages all audio changes for voicemail playback. */ -final class VoicemailAudioManager implements OnAudioFocusChangeListener { +final class VoicemailAudioManager implements OnAudioFocusChangeListener, + WiredHeadsetManager.Listener { private static final String TAG = VoicemailAudioManager.class.getSimpleName(); public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL; private AudioManager mAudioManager; private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; + private WiredHeadsetManager mWiredHeadsetManager; + private boolean mWasSpeakerOn; + private CallAudioState mCallAudioState; public VoicemailAudioManager(Context context, VoicemailPlaybackPresenter voicemailPlaybackPresenter) { mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; + mWiredHeadsetManager = new WiredHeadsetManager(context); + mWiredHeadsetManager.setListener(this); + + mCallAudioState = getInitialAudioState(); + Log.i(TAG, "Initial audioState = " + mCallAudioState); } public void requestAudioFocus() { @@ -60,14 +71,131 @@ final class VoicemailAudioManager implements OnAudioFocusChangeListener { mVoicemailPlaybackPresenter.onAudioFocusChange(focusChange == AudioManager.AUDIOFOCUS_GAIN); } - public void turnOnSpeaker(boolean on) { + @Override + public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) { + Log.i(TAG, "wired headset was plugged in changed: " + oldIsPluggedIn + + " -> "+ newIsPluggedIn); + + if (oldIsPluggedIn == newIsPluggedIn) { + return; + } + + int newRoute = mCallAudioState.getRoute(); // start out with existing route + if (newIsPluggedIn) { + newRoute = CallAudioState.ROUTE_WIRED_HEADSET; + } else { + if (mWasSpeakerOn) { + newRoute = CallAudioState.ROUTE_SPEAKER; + } else { + newRoute = CallAudioState.ROUTE_EARPIECE; + } + } + + mVoicemailPlaybackPresenter.setSpeakerphoneOn(newRoute == CallAudioState.ROUTE_SPEAKER); + + // We need to call this every time even if we do not change the route because the supported + // routes changed either to include or not include WIRED_HEADSET. + setSystemAudioState( + new CallAudioState(false /* muted */, newRoute, calculateSupportedRoutes())); + } + + public void setSpeakerphoneOn(boolean on) { + setAudioRoute(on ? CallAudioState.ROUTE_SPEAKER : CallAudioState.ROUTE_WIRED_OR_EARPIECE); + } + + public boolean isWiredHeadsetPluggedIn() { + return mWiredHeadsetManager.isPluggedIn(); + } + + public void registerReceivers() { + // Receivers is plural because we expect to add bluetooth support. + mWiredHeadsetManager.registerReceiver(); + } + + public void unregisterReceivers() { + mWiredHeadsetManager.unregisterReceiver(); + } + + /** + * Change the audio route, for example from earpiece to speakerphone. + * + * @param route The new audio route to use. See {@link CallAudioState}. + */ + void setAudioRoute(int route) { + Log.v(TAG, "setAudioRoute, route: " + CallAudioState.audioRouteToString(route)); + + // Change ROUTE_WIRED_OR_EARPIECE to a single entry. + int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask()); + + // If route is unsupported, do nothing. + if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) { + Log.w(TAG, "Asking to set to a route that is unsupported: " + newRoute); + return; + } + + if (mCallAudioState.getRoute() != newRoute) { + // Remember the new speaker state so it can be restored when the user plugs and unplugs + // a headset. + mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER; + setSystemAudioState(new CallAudioState(false /* muted */, newRoute, + mCallAudioState.getSupportedRouteMask())); + } + } + + private CallAudioState getInitialAudioState() { + int supportedRouteMask = calculateSupportedRoutes(); + int route = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE, + supportedRouteMask); + return new CallAudioState(false /* muted */, route, supportedRouteMask); + } + + private int calculateSupportedRoutes() { + int routeMask = CallAudioState.ROUTE_SPEAKER; + if (mWiredHeadsetManager.isPluggedIn()) { + routeMask |= CallAudioState.ROUTE_WIRED_HEADSET; + } else { + routeMask |= CallAudioState.ROUTE_EARPIECE; + } + return routeMask; + } + + private int selectWiredOrEarpiece(int route, int supportedRouteMask) { + // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of + // ROUTE_WIRED_OR_EARPIECE so that callers don't have to make a call to check which is + // supported before calling setAudioRoute. + if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) { + route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask; + if (route == 0) { + Log.wtf(TAG, "One of wired headset or earpiece should always be valid."); + // assume earpiece in this case. + route = CallAudioState.ROUTE_EARPIECE; + } + } + return route; + } + + private void setSystemAudioState(CallAudioState callAudioState) { + CallAudioState oldAudioState = mCallAudioState; + mCallAudioState = callAudioState; + + Log.i(TAG, "setSystemAudioState: changing from " + oldAudioState + " to " + + mCallAudioState); + + // Audio route. + if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) { + turnOnSpeaker(true); + } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE || + mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) { + // Just handle turning off the speaker, the system will handle switching between wired + // headset and earpiece. + turnOnSpeaker(false); + } + } + + private void turnOnSpeaker(boolean on) { if (mAudioManager.isSpeakerphoneOn() != on) { Log.i(TAG, "turning speaker phone on: " + on); mAudioManager.setSpeakerphoneOn(on); } } - - public boolean isSpeakerphoneOn() { - return mAudioManager.isSpeakerphoneOn(); - } }
\ No newline at end of file diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java index 14c5473ae..f86fc5539 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java @@ -19,6 +19,7 @@ package com.android.dialer.voicemail; import android.app.Activity; import android.app.Fragment; import android.content.Context; +import android.graphics.drawable.Drawable; import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; @@ -238,6 +239,8 @@ public class VoicemailPlaybackLayout extends LinearLayout private TextView mTotalDurationText; private PositionUpdater mPositionUpdater; + private Drawable mVoicemailSeekHandleEnabled; + private Drawable mVoicemailSeekHandleDisabled; public VoicemailPlaybackLayout(Context context) { this(context, null); @@ -276,9 +279,12 @@ public class VoicemailPlaybackLayout extends LinearLayout mDeleteButton.setOnClickListener(mDeleteButtonListener); mPositionText.setText(formatAsMinutesAndSeconds(0)); - mPositionText.setVisibility(View.INVISIBLE); mTotalDurationText.setText(formatAsMinutesAndSeconds(0)); - mTotalDurationText.setVisibility(View.INVISIBLE); + + mVoicemailSeekHandleEnabled = getResources().getDrawable( + R.drawable.ic_voicemail_seek_handle, mContext.getTheme()); + mVoicemailSeekHandleDisabled = getResources().getDrawable( + R.drawable.ic_voicemail_seek_handle_disabled, mContext.getTheme()); } @Override @@ -317,6 +323,7 @@ public class VoicemailPlaybackLayout extends LinearLayout mStateText.setText(getString(R.string.voicemail_playback_error)); } + @Override public void onSpeakerphoneOn(boolean on) { if (on) { mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp); @@ -354,7 +361,7 @@ public class VoicemailPlaybackLayout extends LinearLayout @Override public void setFetchContentTimeout() { - disableUiElements(); + mStartStopButton.setEnabled(true); mStateText.setText(getString(R.string.voicemail_fetching_timout)); } @@ -366,20 +373,22 @@ public class VoicemailPlaybackLayout extends LinearLayout @Override public void disableUiElements() { mStartStopButton.setEnabled(false); - mPlaybackSeek.setProgress(0); mPlaybackSeek.setEnabled(false); - - mPositionText.setText(formatAsMinutesAndSeconds(0)); - mTotalDurationText.setText(formatAsMinutesAndSeconds(0)); + mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled); } @Override public void enableUiElements() { mStartStopButton.setEnabled(true); mPlaybackSeek.setEnabled(true); + mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled); + } - mPositionText.setVisibility(View.VISIBLE); - mTotalDurationText.setVisibility(View.VISIBLE); + @Override + public void resetSeekBar() { + mPlaybackSeek.setProgress(0); + mPlaybackSeek.setEnabled(false); + mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled); } @Override diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java index 95622bfa2..8b8b7c539 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java @@ -85,6 +85,7 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene void setFetchContentTimeout(); void setIsFetchingContent(); void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri); + void resetSeekBar(); } public interface OnVoicemailDeletedListener { @@ -101,6 +102,7 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene private static final String[] HAS_CONTENT_PROJECTION = new String[] { VoicemailContract.Voicemails.HAS_CONTENT, + VoicemailContract.Voicemails.DURATION }; private static final int NUMBER_OF_THREADS_IN_POOL = 2; @@ -251,19 +253,19 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene mPosition = 0; // Default to earpiece. setSpeakerphoneOn(false); + mVoicemailAudioManager.setSpeakerphoneOn(false); } else { // Update the view to the current speakerphone state. mView.onSpeakerphoneOn(mIsSpeakerphoneOn); } - mDuration.set(0); + checkForContent(); if (startPlayingImmediately) { // Since setPlaybackView can get called during the view binding process, we don't // want to reset mIsPlaying to false if the user is currently playing the // voicemail and the view is rebound. mIsPlaying = startPlayingImmediately; - checkForContent(); } } } @@ -272,16 +274,20 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene * Reset the presenter for playback back to its original state. */ public void resetAll() { - reset(); + pausePresenter(true); mView = null; mVoicemailUri = null; } /** - * Reset the presenter such that it is as if the voicemail has not been played. + * When navigating away from voicemail playback, we need to release the media player, + * pause the UI and save the position. + * + * @param reset {@code true} if we want to reset the position of the playback, {@code false} if + * we want to retain the current position (in case we return to the voicemail). */ - public void reset() { + public void pausePresenter(boolean reset) { if (mMediaPlayer != null) { mMediaPlayer.release(); mMediaPlayer = null; @@ -291,19 +297,35 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene mIsPrepared = false; mIsPlaying = false; - mPosition = 0; - mDuration.set(0); + + if (reset) { + // We want to reset the position whether or not the view is valid. + mPosition = 0; + } if (mView != null) { mView.onPlaybackStopped(); - mView.setClipPosition(0, mDuration.get()); + if (reset) { + mView.setClipPosition(0, mDuration.get()); + } else { + mPosition = mView.getDesiredClipPosition(); + } } } /** + * Must be invoked when the parent activity is resumed. + */ + public void onResume() { + mVoicemailAudioManager.registerReceivers(); + } + + /** * Must be invoked when the parent activity is paused. */ public void onPause() { + mVoicemailAudioManager.unregisterReceivers(); + if (mContext != null && mIsPrepared && mInitialOrientation != mContext.getResources().getConfiguration().orientation) { // If an orientation change triggers the pause, retain the MediaPlayer. @@ -312,11 +334,12 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene } // Release the media player, otherwise there may be failures. - reset(); + pausePresenter(false); if (mActivity != null) { mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); } + } /** @@ -345,8 +368,8 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene * voicemail we've been asked to play has any content available. * <p> * Notify the user that we are fetching the content, then check to see if the content field in - * the DB is set. If set, we proceed to {@link #prepareContent()} method. If not set, make - * a request to fetch the content asynchronously via {@link #requestContent()}. + * the DB is set. If set, we proceed to {@link #prepareContent()} method. We get the duration of + * the voicemail from the query and set it if the content is not available. */ private void checkForContent() { mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() { @@ -360,7 +383,8 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene if (hasContent) { prepareContent(); } else { - requestContent(); + mView.resetSeekBar(); + mView.setClipPosition(0, mDuration.get()); } } }); @@ -373,10 +397,14 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene ContentResolver contentResolver = mContext.getContentResolver(); Cursor cursor = contentResolver.query( - voicemailUri, HAS_CONTENT_PROJECTION, null, null, null); + voicemailUri, null, null, null, null); try { if (cursor != null && cursor.moveToNext()) { - return cursor.getInt(cursor.getColumnIndexOrThrow( + int duration = cursor.getInt(cursor.getColumnIndex( + VoicemailContract.Voicemails.DURATION)); + // Convert database duration (seconds) into mDuration (milliseconds) + mDuration.set(duration > 0 ? duration * 1000 : 0); + return cursor.getInt(cursor.getColumnIndex( VoicemailContract.Voicemails.HAS_CONTENT)) == 1; } } finally { @@ -519,7 +547,6 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene mIsPrepared = true; mDuration.set(mMediaPlayer.getDuration()); - mPosition = mMediaPlayer.getCurrentPosition(); Log.d(TAG, "onPrepared: mPosition=" + mPosition); mView.setClipPosition(mPosition, mDuration.get()); @@ -604,7 +631,7 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene if (!mIsPrepared) { // If we haven't downloaded the voicemail yet, attempt to download it. - checkForContent(); + requestContent(); mIsPlaying = true; return; } @@ -614,15 +641,15 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene if (!mMediaPlayer.isPlaying()) { // Clamp the start position between 0 and the duration. mPosition = Math.max(0, Math.min(mPosition, mDuration.get())); + mMediaPlayer.seekTo(mPosition); try { // Grab audio focus. // Can throw RejectedExecutionException. mVoicemailAudioManager.requestAudioFocus(); - - setSpeakerphoneOn(mIsSpeakerphoneOn); mMediaPlayer.start(); + setSpeakerphoneOn(mIsSpeakerphoneOn); } catch (RejectedExecutionException e) { handleError(e); } @@ -708,21 +735,29 @@ public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListene } } + /** + * This is for use by UI interactions only. It simplifies UI logic. + */ public void toggleSpeakerphone() { + mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn); setSpeakerphoneOn(!mIsSpeakerphoneOn); } - private void setSpeakerphoneOn(boolean on) { + /** + * This method only handles app-level changes to the speakerphone. Audio layer changes should + * be handled separately. This is so that the VoicemailAudioManager can trigger changes to + * the presenter without the presenter triggering the audio manager and duplicating actions. + */ + public void setSpeakerphoneOn(boolean on) { mView.onSpeakerphoneOn(on); - if (mIsSpeakerphoneOn == on) { - return; - } mIsSpeakerphoneOn = on; - mVoicemailAudioManager.turnOnSpeaker(on); + // This should run even if speakerphone is not being toggled because we may be switching + // from earpiece to headphone and vise versa. Also upon initial setup the default audio + // source is the earpiece, so we want to trigger the proximity sensor. if (mIsPlaying) { - if (on) { + if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) { disableProximitySensor(false /* waitForFarState */); if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) { mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); diff --git a/src/com/android/dialer/voicemail/WiredHeadsetManager.java b/src/com/android/dialer/voicemail/WiredHeadsetManager.java new file mode 100644 index 000000000..7351f4f01 --- /dev/null +++ b/src/com/android/dialer/voicemail/WiredHeadsetManager.java @@ -0,0 +1,88 @@ +/* + * 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. + */ +package com.android.dialer.voicemail; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.util.Log; + +/** Listens for and caches headset state. */ +class WiredHeadsetManager { + private static final String TAG = WiredHeadsetManager.class.getSimpleName(); + + interface Listener { + void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn); + } + + /** Receiver for wired headset plugged and unplugged events. */ + private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (AudioManager.ACTION_HEADSET_PLUG.equals(intent.getAction())) { + boolean isPluggedIn = intent.getIntExtra("state", 0) == 1; + Log.v(TAG, "ACTION_HEADSET_PLUG event, plugged in: " + isPluggedIn); + onHeadsetPluggedInChanged(isPluggedIn); + } + } + } + + private final WiredHeadsetBroadcastReceiver mReceiver; + private boolean mIsPluggedIn; + private Listener mListener; + private Context mContext; + + WiredHeadsetManager(Context context) { + mContext = context; + mReceiver = new WiredHeadsetBroadcastReceiver(); + + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + mIsPluggedIn = audioManager.isWiredHeadsetOn(); + + } + + void setListener(Listener listener) { + mListener = listener; + } + + boolean isPluggedIn() { + return mIsPluggedIn; + } + + void registerReceiver() { + // Register for misc other intent broadcasts. + IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); + mContext.registerReceiver(mReceiver, intentFilter); + } + + void unregisterReceiver() { + mContext.unregisterReceiver(mReceiver); + } + + private void onHeadsetPluggedInChanged(boolean isPluggedIn) { + if (mIsPluggedIn != isPluggedIn) { + Log.v(TAG, "onHeadsetPluggedInChanged, mIsPluggedIn: " + mIsPluggedIn + " -> " + + isPluggedIn); + boolean oldIsPluggedIn = mIsPluggedIn; + mIsPluggedIn = isPluggedIn; + if (mListener != null) { + mListener.onWiredHeadsetPluggedInChanged(oldIsPluggedIn, mIsPluggedIn); + } + } + } +}
\ No newline at end of file |