From 1ecb3e2751218d377cfd75d9623c6715174c8dc5 Mon Sep 17 00:00:00 2001 From: uabdullah Date: Mon, 13 Nov 2017 14:42:50 -0800 Subject: Play voicemails downloaded from the voicemail server in the NUI VM Fragment Voicemails that are available locally on the device (have been downloaded from the voicemail server) will have their "HAS_CONTENT" column set to one. This CL adds the plumbing logic of checking that when a user presses the play button, if the voicemail is available locally, it is played. Bug: 64882313,68382421 Test: Existing unit tests PiperOrigin-RevId: 175590204 Change-Id: Id164d08c16b89c84a75af0c4a7c899c02d64fff7 --- .../voicemail/listui/NewVoicemailAdapter.java | 10 +- .../voicemail/listui/NewVoicemailFragment.java | 4 +- .../voicemail/listui/NewVoicemailMediaPlayer.java | 87 -------- .../listui/NewVoicemailMediaPlayerView.java | 221 +++++++++++++++++++++ .../voicemail/listui/NewVoicemailViewHolder.java | 11 +- .../listui/res/layout/new_voicemail_entry.xml | 2 +- 6 files changed, 241 insertions(+), 94 deletions(-) delete mode 100644 java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayer.java create mode 100644 java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java index d04143f59..6f6a87cef 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java @@ -15,6 +15,7 @@ */ package com.android.dialer.voicemail.listui; +import android.app.FragmentManager; import android.database.Cursor; import android.support.v7.widget.RecyclerView; import android.util.ArraySet; @@ -33,6 +34,7 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter newVoicemailViewHolderSet = new ArraySet<>(); /** @param cursor whose projection is {@link VoicemailCursorLoader.VOICEMAIL_COLUMNS} */ - NewVoicemailAdapter(Cursor cursor, Clock clock) { + NewVoicemailAdapter(Cursor cursor, Clock clock, FragmentManager fragmentManager) { + LogUtil.enterBlock("NewVoicemailAdapter"); this.cursor = cursor; this.clock = clock; + this.fragmentManager = fragmentManager; } @Override public NewVoicemailViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + LogUtil.enterBlock("NewVoicemailAdapter.onCreateViewHolder"); LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); View view = inflater.inflate(R.layout.new_voicemail_entry, viewGroup, false); NewVoicemailViewHolder newVoicemailViewHolder = new NewVoicemailViewHolder(view, clock, this); @@ -56,9 +61,8 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter loader, Cursor data) { LogUtil.i("NewVoicemailFragment.onCreateLoader", "cursor size is %d", data.getCount()); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(new NewVoicemailAdapter(data, System::currentTimeMillis)); + recyclerView.setAdapter( + new NewVoicemailAdapter( + data, System::currentTimeMillis, getActivity().getFragmentManager())); } @Override diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayer.java b/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayer.java deleted file mode 100644 index 11aa9ac2e..000000000 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayer.java +++ /dev/null @@ -1,87 +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.voicemail.listui; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Button; -import android.widget.LinearLayout; -import com.android.dialer.common.LogUtil; - -/** - * The view of the media player that is visible when a {@link NewVoicemailViewHolder} is expanded. - */ -public class NewVoicemailMediaPlayer extends LinearLayout { - - private Button playButton; - private Button speakerButton; - private Button deleteButton; - - public NewVoicemailMediaPlayer(Context context, AttributeSet attrs) { - super(context, attrs); - LogUtil.enterBlock("NewVoicemailMediaPlayer"); - LayoutInflater inflater = - (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.new_voicemail_media_player_layout, this); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - LogUtil.enterBlock("NewVoicemailMediaPlayer.onFinishInflate"); - initializeMediaPlayerButtons(); - setupListenersForMediaPlayerButtons(); - } - - private void initializeMediaPlayerButtons() { - playButton = findViewById(R.id.playButton); - speakerButton = findViewById(R.id.speakerButton); - deleteButton = findViewById(R.id.deleteButton); - } - - private void setupListenersForMediaPlayerButtons() { - playButton.setOnClickListener(playButtonListener); - speakerButton.setOnClickListener(speakerButtonListener); - deleteButton.setOnClickListener(deleteButtonListener); - } - - private final View.OnClickListener playButtonListener = - new View.OnClickListener() { - @Override - public void onClick(View view) { - LogUtil.i("NewVoicemailMediaPlayer.playButtonListener", "onClick"); - } - }; - - private final View.OnClickListener speakerButtonListener = - new View.OnClickListener() { - @Override - public void onClick(View view) { - LogUtil.i("NewVoicemailMediaPlayer.speakerButtonListener", "onClick"); - } - }; - - private final View.OnClickListener deleteButtonListener = - new View.OnClickListener() { - @Override - public void onClick(View view) { - LogUtil.i("NewVoicemailMediaPlayer.deleteButtonListener", "onClick"); - } - }; -} diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java b/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java new file mode 100644 index 000000000..1e56a8189 --- /dev/null +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java @@ -0,0 +1,221 @@ +/* + * 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.voicemail.listui; + +import android.app.FragmentManager; +import android.content.Context; +import android.database.Cursor; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.MediaPlayer.OnPreparedListener; +import android.net.Uri; +import android.provider.VoicemailContract; +import android.support.annotation.VisibleForTesting; +import android.support.v4.util.Pair; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; +import com.android.dialer.common.concurrent.DialerExecutor.Worker; +import com.android.dialer.common.concurrent.DialerExecutorComponent; + +/** + * The view of the media player that is visible when a {@link NewVoicemailViewHolder} is expanded. + */ +public class NewVoicemailMediaPlayerView extends LinearLayout { + + private Button playButton; + private Button speakerButton; + private Button deleteButton; + private Uri voicemailUri; + private FragmentManager fragmentManager; + private MediaPlayer mediaPlayer; + + public NewVoicemailMediaPlayerView(Context context, AttributeSet attrs) { + super(context, attrs); + LogUtil.enterBlock("NewVoicemailMediaPlayer"); + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.new_voicemail_media_player_layout, this); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + LogUtil.enterBlock("NewVoicemailMediaPlayer.onFinishInflate"); + initializeMediaPlayerButtons(); + setupListenersForMediaPlayerButtons(); + } + + private void initializeMediaPlayerButtons() { + playButton = findViewById(R.id.playButton); + speakerButton = findViewById(R.id.speakerButton); + deleteButton = findViewById(R.id.deleteButton); + } + + private void setupListenersForMediaPlayerButtons() { + playButton.setOnClickListener(playButtonListener); + speakerButton.setOnClickListener(speakerButtonListener); + deleteButton.setOnClickListener(deleteButtonListener); + } + + private final View.OnClickListener playButtonListener = + view -> playVoicemailWhenAvailableLocally(); + + /** + * Plays the voicemail when we are able to play the voicemail locally from the device. This + * involves checking if the voicemail is available to play locally, if it is, then we setup the + * Media Player to play the voicemail. If the voicemail is not available, then we need download + * the voicemail from the voicemail server to the device, and then have the Media player play it. + */ + private void playVoicemailWhenAvailableLocally() { + LogUtil.enterBlock("playVoicemailWhenAvailableLocally"); + Worker, Pair> checkVoicemailHasContent = + this::queryVoicemailHasContent; + SuccessListener> checkVoicemailHasContentCallBack = this::prepareMediaPlayer; + + DialerExecutorComponent.get(getContext()) + .dialerExecutorFactory() + .createUiTaskBuilder(fragmentManager, "lookup_voicemail_content", checkVoicemailHasContent) + .onSuccess(checkVoicemailHasContentCallBack) + .build() + .executeSerial(new Pair<>(getContext(), voicemailUri)); + } + + private Pair queryVoicemailHasContent(Pair contextUriPair) { + Context context = contextUriPair.first; + Uri uri = contextUriPair.second; + + try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { + if (cursor != null && cursor.moveToNext()) { + return new Pair<>( + cursor.getInt(cursor.getColumnIndex(VoicemailContract.Voicemails.HAS_CONTENT)) == 1, + uri); + } + return new Pair<>(false, uri); + } + } + + /** + * If the voicemail is available to play locally, setup the media player to play it. Otherwise + * send a request to download the voicemail and then play it. + */ + private void prepareMediaPlayer(Pair booleanUriPair) { + boolean voicemailAvailableLocally = booleanUriPair.first; + Uri uri = booleanUriPair.second; + LogUtil.i( + "NewVoicemailMediaPlayer.prepareMediaPlayer", + "voicemail available locally: %b for voicemailUri: %s", + voicemailAvailableLocally, + uri.toString()); + + if (voicemailAvailableLocally) { + try { + mediaPlayer = new MediaPlayer(); + mediaPlayer.setOnPreparedListener(onPreparedListener); + mediaPlayer.setOnErrorListener(onErrorListener); + mediaPlayer.setOnCompletionListener(onCompletionListener); + + mediaPlayer.reset(); + mediaPlayer.setDataSource(getContext(), uri); + + mediaPlayer.prepareAsync(); + } catch (Exception e) { + LogUtil.e("NewVoicemailMediaPlayer.prepareMediaPlayer", "IOException " + e); + } + } else { + // TODO(a bug): Add logic for downloading voicemail content from the server. + LogUtil.i( + "NewVoicemailMediaPlayer.prepareVoicemailForMediaPlayer", "need to download content"); + } + } + + private final View.OnClickListener speakerButtonListener = + new View.OnClickListener() { + @Override + public void onClick(View view) { + LogUtil.i( + "NewVoicemailMediaPlayer.speakerButtonListener", + "speaker request for voicemailUri: %s", + voicemailUri.toString()); + } + }; + + private final View.OnClickListener deleteButtonListener = + new View.OnClickListener() { + @Override + public void onClick(View view) { + LogUtil.i( + "NewVoicemailMediaPlayer.deleteButtonListener", + "delete voicemailUri %s", + voicemailUri.toString()); + } + }; + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + OnCompletionListener onCompletionListener = + new OnCompletionListener() { + + @Override + public void onCompletion(MediaPlayer mp) { + LogUtil.i( + "NewVoicemailMediaPlayer.onCompletionListener", + "completed playing voicemailUri: %s", + voicemailUri.toString()); + } + }; + + private final OnPreparedListener onPreparedListener = + new OnPreparedListener() { + + @Override + public void onPrepared(MediaPlayer mp) { + LogUtil.i( + "NewVoicemailMediaPlayer.onPreparedListener", + "about to play voicemailUri: %s", + voicemailUri.toString()); + mediaPlayer.start(); + } + }; + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + OnErrorListener onErrorListener = + new OnErrorListener() { + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + LogUtil.i( + "NewVoicemailMediaPlayer.onErrorListener", + "error playing voicemailUri: %s", + voicemailUri.toString()); + return false; + } + }; + + public void setVoicemailUri(Uri voicemailUri) { + Assert.isNotNull(voicemailUri); + this.voicemailUri = voicemailUri; + } + + public void setFragmentManager(FragmentManager fragmentManager) { + this.fragmentManager = fragmentManager; + } +} diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java index d4bfefd22..078a029c9 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java @@ -15,6 +15,7 @@ */ package com.android.dialer.voicemail.listui; +import android.app.FragmentManager; import android.content.Context; import android.database.Cursor; import android.net.Uri; @@ -25,6 +26,7 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.QuickContactBadge; import android.widget.TextView; +import com.android.dialer.common.LogUtil; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.lettertile.LetterTileDrawable; import com.android.dialer.time.Clock; @@ -38,7 +40,7 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On private final TextView secondaryTextView; private final TextView transcriptionTextView; private final QuickContactBadge quickContactBadge; - private final View mediaPlayerView; + private final NewVoicemailMediaPlayerView mediaPlayerView; private final Clock clock; private boolean isViewHolderExpanded; private int viewHolderId; @@ -47,6 +49,7 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On NewVoicemailViewHolder( View view, Clock clock, NewVoicemailViewHolderListener newVoicemailViewHolderListener) { super(view); + LogUtil.enterBlock("NewVoicemailViewHolder"); this.context = view.getContext(); primaryTextView = view.findViewById(R.id.primary_text); secondaryTextView = view.findViewById(R.id.secondary_text); @@ -57,7 +60,7 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On voicemailViewHolderListener = newVoicemailViewHolderListener; } - void bind(Cursor cursor) { + void bind(Cursor cursor, FragmentManager fragmentManager) { VoicemailEntry voicemailEntry = VoicemailCursorLoader.toVoicemailEntry(cursor); viewHolderId = voicemailEntry.id(); primaryTextView.setText(VoicemailEntryText.buildPrimaryVoicemailText(context, voicemailEntry)); @@ -76,6 +79,8 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On itemView.setOnClickListener(this); setPhoto(voicemailEntry); + mediaPlayerView.setVoicemailUri(Uri.parse(voicemailEntry.voicemailUri())); + mediaPlayerView.setFragmentManager(fragmentManager); } // TODO(uabdullah): Consider/Implement TYPE (e.g Spam, TYPE_VOICEMAIL) @@ -97,6 +102,7 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On } void expandViewHolder() { + LogUtil.i("NewVoicemailViewHolder.expandViewHolder", "voicemail id: %d", viewHolderId); transcriptionTextView.setMaxLines(999); isViewHolderExpanded = true; mediaPlayerView.setVisibility(View.VISIBLE); @@ -117,6 +123,7 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On @Override public void onClick(View v) { + LogUtil.i("NewVoicemailViewHolder.onClick", "voicemail id: %d", viewHolderId); if (isViewHolderExpanded) { collapseViewHolder(); } else { diff --git a/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml b/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml index 80bb1b593..78d2785e9 100644 --- a/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml +++ b/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_entry.xml @@ -82,7 +82,7 @@ android:layout_gravity="center_vertical" android:visibility="gone"/> - Date: Mon, 13 Nov 2017 14:51:51 -0800 Subject: Allow external apps to start pre-call flow This CL adds an activity that will forward parameters to PreCall so external apps like contact can utilize SIM selection or assisted dialing features. The caller needs to have CALL_PHONE permission. Bug: 64213352 Test: LaunchPreCallActivityTest PiperOrigin-RevId: 175591732 Change-Id: I096ea022d5ed87c29ffb191cd1bdb04e3a17c945 --- .../precall/externalreceiver/AndroidManifest.xml | 40 +++++++++++++++ .../externalreceiver/LaunchPreCallActivity.java | 60 ++++++++++++++++++++++ packages.mk | 1 + 3 files changed, 101 insertions(+) create mode 100644 java/com/android/dialer/precall/externalreceiver/AndroidManifest.xml create mode 100644 java/com/android/dialer/precall/externalreceiver/LaunchPreCallActivity.java diff --git a/java/com/android/dialer/precall/externalreceiver/AndroidManifest.xml b/java/com/android/dialer/precall/externalreceiver/AndroidManifest.xml new file mode 100644 index 000000000..b1c625a0f --- /dev/null +++ b/java/com/android/dialer/precall/externalreceiver/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/precall/externalreceiver/LaunchPreCallActivity.java b/java/com/android/dialer/precall/externalreceiver/LaunchPreCallActivity.java new file mode 100644 index 000000000..121e6a6c9 --- /dev/null +++ b/java/com/android/dialer/precall/externalreceiver/LaunchPreCallActivity.java @@ -0,0 +1,60 @@ +/* + * 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.precall.externalreceiver; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import com.android.dialer.callintent.CallInitiationType.Type; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.precall.PreCall; + +/** + * Activity that forwards to {@link PreCall#start(Context, CallIntentBuilder)} so the pre-call flow + * can be initiated by external apps. This activity is exported but can only be started by apps with + * {@link android.Manifest.permission#CALL_PHONE}. Keyguard will be triggered if phone is locked. + * + * @see CallIntentBuilder + */ +public class LaunchPreCallActivity extends Activity { + + public static final String ACTION_LAUNCH_PRE_CALL = "com.android.dialer.LAUNCH_PRE_CALL"; + + public static final String EXTRA_PHONE_ACCOUNT_HANDLE = "phone_account_handle"; + + public static final String EXTRA_IS_VIDEO_CALL = "is_video_call"; + + public static final String EXTRA_CALL_SUBJECT = "call_subject"; + + public static final String EXTRA_ALLOW_ASSISTED_DIAL = "allow_assisted_dial"; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + CallIntentBuilder builder = new CallIntentBuilder(intent.getData(), Type.EXTERNAL_INITIATION); + builder + .setPhoneAccountHandle(intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE)) + .setIsVideoCall(intent.getBooleanExtra(EXTRA_IS_VIDEO_CALL, false)) + .setCallSubject(intent.getStringExtra(EXTRA_CALL_SUBJECT)) + .setAllowAssistedDial(intent.getBooleanExtra(EXTRA_ALLOW_ASSISTED_DIAL, false)); + PreCall.start(this, builder); + finish(); + } +} diff --git a/packages.mk b/packages.mk index 223e40d2e..86d84074f 100644 --- a/packages.mk +++ b/packages.mk @@ -41,6 +41,7 @@ LOCAL_AAPT_FLAGS := \ com.android.dialer.phonenumberutil \ com.android.dialer.postcall \ com.android.dialer.precall.impl \ + com.android.dialer.precall.externalreceiver \ com.android.dialer.preferredsim.impl \ com.android.dialer.searchfragment.common \ com.android.dialer.searchfragment.cp2 \ -- cgit v1.2.3 From a1c046f99a353985f41d2b0d9cecf31d53364ee6 Mon Sep 17 00:00:00 2001 From: twyen Date: Mon, 13 Nov 2017 14:57:58 -0800 Subject: Implement preferred SIM Before prompting the user to select the SIM, CallingAccountSelector will lookup the fallback preferred SIM database to see if a preferred SIM is already set and bypass the selection. If the number is in contacts the user will also have the option to store the selected SIM as preferred. Bug: 64213352 Test: CallingAccountSelectorTest PiperOrigin-RevId: 175592732 Change-Id: I6a5a8ad8772eccfb4a119c529dcd3945b9dc0b1e --- .../widget/SelectPhoneAccountDialogFragment.java | 5 + .../precall/impl/CallingAccountSelector.java | 241 +++++++++++++++++++-- .../dialer/precall/impl/PreferredAccountUtil.java | 94 ++++++++ .../dialer/simulator/impl/SimulatorMainMenu.java | 19 ++ 4 files changed, 345 insertions(+), 14 deletions(-) create mode 100644 java/com/android/dialer/precall/impl/PreferredAccountUtil.java diff --git a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java index 6c6aebc0b..e21fded97 100644 --- a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java +++ b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java @@ -123,6 +123,11 @@ public class SelectPhoneAccountDialogFragment extends DialogFragment { return mListener; } + @VisibleForTesting + public boolean canSetDefault() { + return getArguments().getBoolean(ARG_CAN_SET_DEFAULT); + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); diff --git a/java/com/android/dialer/precall/impl/CallingAccountSelector.java b/java/com/android/dialer/precall/impl/CallingAccountSelector.java index ca8798c5d..ca74bef08 100644 --- a/java/com/android/dialer/precall/impl/CallingAccountSelector.java +++ b/java/com/android/dialer/precall/impl/CallingAccountSelector.java @@ -17,10 +17,18 @@ package com.android.dialer.precall.impl; import android.app.Activity; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract.PhoneLookup; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.support.v4.util.ArraySet; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; @@ -28,9 +36,17 @@ import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment; import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DialerExecutor.Worker; +import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.precall.PreCallAction; import com.android.dialer.precall.PreCallCoordinator; +import com.android.dialer.precall.PreCallCoordinator.PendingAction; +import com.android.dialer.preferredsim.PreferredSimFallbackContract; +import com.android.dialer.preferredsim.PreferredSimFallbackContract.PreferredSim; +import com.google.common.base.Optional; import java.util.List; +import java.util.Set; /** PreCallAction to select which phone account to call with. Ignored if there's only one account */ @SuppressWarnings("MissingPermission") @@ -43,6 +59,7 @@ public class CallingAccountSelector implements PreCallAction { private boolean isDiscarding; @Override + @MainThread public void run(PreCallCoordinator coordinator) { CallIntentBuilder builder = coordinator.getBuilder(); if (builder.getPhoneAccountHandle() != null) { @@ -54,43 +71,189 @@ public class CallingAccountSelector implements PreCallAction { if (accounts.size() <= 1) { return; } - boolean isVoicemail = builder.getUri().getScheme().equals(PhoneAccount.SCHEME_VOICEMAIL); - - if (!isVoicemail) { - PhoneAccountHandle defaultPhoneAccount = - telecomManager.getDefaultOutgoingPhoneAccount(builder.getUri().getScheme()); - if (defaultPhoneAccount != null) { - builder.setPhoneAccountHandle(defaultPhoneAccount); - return; - } + switch (builder.getUri().getScheme()) { + case PhoneAccount.SCHEME_VOICEMAIL: + showDialog(coordinator, coordinator.startPendingAction(), null); + break; + case PhoneAccount.SCHEME_TEL: + processPreferredAccount(coordinator); + break; + default: + // might be PhoneAccount.SCHEME_SIP + LogUtil.e( + "CallingAccountSelector.run", + "unable to process scheme " + builder.getUri().getScheme()); + break; } + } + /** Initiates a background worker to find if there's any preferred account. */ + @MainThread + private void processPreferredAccount(PreCallCoordinator coordinator) { + Assert.isMainThread(); + CallIntentBuilder builder = coordinator.getBuilder(); + Activity activity = coordinator.getActivity(); + String phoneNumber = builder.getUri().getSchemeSpecificPart(); + PendingAction pendingAction = coordinator.startPendingAction(); + DialerExecutorComponent.get(coordinator.getActivity()) + .dialerExecutorFactory() + .createUiTaskBuilder( + activity.getFragmentManager(), + "PreferredAccountWorker", + new PreferredAccountWorker(phoneNumber)) + .onSuccess( + (result -> { + if (result.phoneAccountHandle.isPresent()) { + coordinator.getBuilder().setPhoneAccountHandle(result.phoneAccountHandle.get()); + pendingAction.finish(); + return; + } + PhoneAccountHandle defaultPhoneAccount = + activity + .getSystemService(TelecomManager.class) + .getDefaultOutgoingPhoneAccount(builder.getUri().getScheme()); + if (defaultPhoneAccount != null) { + builder.setPhoneAccountHandle(defaultPhoneAccount); + pendingAction.finish(); + return; + } + showDialog(coordinator, pendingAction, result.dataId.orNull()); + })) + .build() + .executeParallel(activity); + } + + @MainThread + private void showDialog( + PreCallCoordinator coordinator, PendingAction pendingAction, @Nullable String dataId) { + Assert.isMainThread(); selectPhoneAccountDialogFragment = SelectPhoneAccountDialogFragment.newInstance( R.string.pre_call_select_phone_account, - false /* canSetDefault */, // TODO(twyen): per contact defaults - accounts, - new SelectedListener(coordinator, coordinator.startPendingAction()), + dataId != null /* canSetDefault */, + coordinator + .getActivity() + .getSystemService(TelecomManager.class) + .getCallCapablePhoneAccounts(), + new SelectedListener(coordinator, pendingAction, dataId), null /* call ID */); selectPhoneAccountDialogFragment.show( - activity.getFragmentManager(), TAG_CALLING_ACCOUNT_SELECTOR); + coordinator.getActivity().getFragmentManager(), TAG_CALLING_ACCOUNT_SELECTOR); } + @MainThread @Override public void onDiscard() { isDiscarding = true; selectPhoneAccountDialogFragment.dismiss(); } + private static class PreferredAccountWorkerResult { + + /** The preferred phone account for the number. Absent if not set or invalid. */ + Optional phoneAccountHandle = Optional.absent(); + + /** + * {@link android.provider.ContactsContract.Data#_ID} of the row matching the number. If the + * preferred account is to be set it should be stored in this row + */ + Optional dataId = Optional.absent(); + } + + private static class PreferredAccountWorker + implements Worker { + + private final String phoneNumber; + + public PreferredAccountWorker(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + @NonNull + @Override + @WorkerThread + public PreferredAccountWorkerResult doInBackground(Context context) throws Throwable { + PreferredAccountWorkerResult result = new PreferredAccountWorkerResult(); + result.dataId = getDataId(context.getContentResolver(), phoneNumber); + if (result.dataId.isPresent()) { + result.phoneAccountHandle = getPreferredAccount(context, result.dataId.get()); + } + return result; + } + } + + @WorkerThread + @NonNull + private static Optional getDataId( + @NonNull ContentResolver contentResolver, @Nullable String phoneNumber) { + Assert.isWorkerThread(); + try (Cursor cursor = + contentResolver.query( + Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)), + new String[] {PhoneLookup.DATA_ID}, + null, + null, + null)) { + if (cursor == null) { + return Optional.absent(); + } + Set result = new ArraySet<>(); + while (cursor.moveToNext()) { + result.add(cursor.getString(0)); + } + // TODO(twyen): if there are multiples attempt to grab from the contact that initiated the + // call. + if (result.size() == 1) { + return Optional.of(result.iterator().next()); + } else { + LogUtil.i("CallingAccountSelector.getDataId", "lookup result not unique, ignoring"); + return Optional.absent(); + } + } + } + + @WorkerThread + @NonNull + private static Optional getPreferredAccount( + @NonNull Context context, @NonNull String dataId) { + Assert.isWorkerThread(); + Assert.isNotNull(dataId); + try (Cursor cursor = + context + .getContentResolver() + .query( + PreferredSimFallbackContract.CONTENT_URI, + new String[] { + PreferredSim.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, + PreferredSim.PREFERRED_PHONE_ACCOUNT_ID + }, + PreferredSim.DATA_ID + " = ?", + new String[] {dataId}, + null)) { + if (cursor == null) { + return Optional.absent(); + } + if (!cursor.moveToFirst()) { + return Optional.absent(); + } + return PreferredAccountUtil.getValidPhoneAccount( + context, cursor.getString(0), cursor.getString(1)); + } + } + private class SelectedListener extends SelectPhoneAccountListener { private final PreCallCoordinator coordinator; private final PreCallCoordinator.PendingAction listener; + private final String dataId; public SelectedListener( - @NonNull PreCallCoordinator builder, @NonNull PreCallCoordinator.PendingAction listener) { + @NonNull PreCallCoordinator builder, + @NonNull PreCallCoordinator.PendingAction listener, + @Nullable String dataId) { this.coordinator = Assert.isNotNull(builder); this.listener = Assert.isNotNull(listener); + this.dataId = dataId; } @MainThread @@ -98,6 +261,17 @@ public class CallingAccountSelector implements PreCallAction { public void onPhoneAccountSelected( PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) { coordinator.getBuilder().setPhoneAccountHandle(selectedAccountHandle); + + if (dataId != null && setDefault) { + DialerExecutorComponent.get(coordinator.getActivity()) + .dialerExecutorFactory() + .createNonUiTaskBuilder(new WritePreferredAccountWorker()) + .build() + .executeParallel( + new WritePreferredAccountWorkerInput( + coordinator.getActivity(), dataId, selectedAccountHandle)); + } + listener.finish(); } @@ -111,4 +285,43 @@ public class CallingAccountSelector implements PreCallAction { listener.finish(); } } + + private static class WritePreferredAccountWorkerInput { + private final Context context; + private final String dataId; + private final PhoneAccountHandle phoneAccountHandle; + + WritePreferredAccountWorkerInput( + @NonNull Context context, + @NonNull String dataId, + @NonNull PhoneAccountHandle phoneAccountHandle) { + this.context = Assert.isNotNull(context); + this.dataId = Assert.isNotNull(dataId); + this.phoneAccountHandle = Assert.isNotNull(phoneAccountHandle); + } + } + + private static class WritePreferredAccountWorker + implements Worker { + + @Nullable + @Override + @WorkerThread + public Void doInBackground(WritePreferredAccountWorkerInput input) throws Throwable { + ContentValues values = new ContentValues(); + values.put( + PreferredSim.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME, + input.phoneAccountHandle.getComponentName().flattenToString()); + values.put(PreferredSim.PREFERRED_PHONE_ACCOUNT_ID, input.phoneAccountHandle.getId()); + input + .context + .getContentResolver() + .update( + PreferredSimFallbackContract.CONTENT_URI, + values, + PreferredSim.DATA_ID + " = ?", + new String[] {String.valueOf(input.dataId)}); + return null; + } + } } diff --git a/java/com/android/dialer/precall/impl/PreferredAccountUtil.java b/java/com/android/dialer/precall/impl/PreferredAccountUtil.java new file mode 100644 index 000000000..a41cb6e78 --- /dev/null +++ b/java/com/android/dialer/precall/impl/PreferredAccountUtil.java @@ -0,0 +1,94 @@ +/* + * 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.precall.impl; + +import android.content.ComponentName; +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import com.android.dialer.common.LogUtil; +import com.google.common.base.Optional; + +/** + * Utilities for looking up and validating preferred {@link PhoneAccountHandle}. Contacts should + * follow the same logic. + */ +public class PreferredAccountUtil { + + /** + * Validates {@code componentNameString} and {@code idString} maps to SIM that is present on the + * device. + */ + @NonNull + public static Optional getValidPhoneAccount( + @NonNull Context context, @Nullable String componentNameString, @Nullable String idString) { + if (TextUtils.isEmpty(componentNameString) || TextUtils.isEmpty(idString)) { + LogUtil.i("PreferredAccountUtil.getValidPhoneAccount", "empty componentName or id"); + return Optional.absent(); + } + ComponentName componentName = ComponentName.unflattenFromString(componentNameString); + if (componentName == null) { + LogUtil.e("PreferredAccountUtil.getValidPhoneAccount", "cannot parse component name"); + return Optional.absent(); + } + PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(componentName, idString); + + if (isPhoneAccountValid(context, phoneAccountHandle)) { + return Optional.of(phoneAccountHandle); + } + return Optional.absent(); + } + + private static boolean isPhoneAccountValid( + Context context, PhoneAccountHandle phoneAccountHandle) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { + return context + .getSystemService(TelephonyManager.class) + .createForPhoneAccountHandle(phoneAccountHandle) + != null; + } + + PhoneAccount phoneAccount = + context.getSystemService(TelecomManager.class).getPhoneAccount(phoneAccountHandle); + if (phoneAccount == null) { + LogUtil.e("PreferredAccountUtil.isPhoneAccountValid", "invalid phone account"); + return false; + } + + if (!phoneAccount.isEnabled()) { + LogUtil.e("PreferredAccountUtil.isPhoneAccountValid", "disabled phone account"); + return false; + } + for (SubscriptionInfo info : + SubscriptionManager.from(context).getActiveSubscriptionInfoList()) { + if (phoneAccountHandle.getId().startsWith(info.getIccId())) { + LogUtil.i("PreferredAccountUtil.isPhoneAccountValid", "sim found"); + return true; + } + } + return false; + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java b/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java index f4b1916c5..e2082105b 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java +++ b/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java @@ -29,6 +29,7 @@ import com.android.dialer.databasepopulator.ContactsPopulator; import com.android.dialer.databasepopulator.VoicemailPopulator; import com.android.dialer.enrichedcall.simulator.EnrichedCallSimulatorActivity; import com.android.dialer.persistentlog.PersistentLogger; +import com.android.dialer.preferredsim.PreferredSimFallbackContract; /** Implements the top level simulator menu. */ final class SimulatorMainMenu { @@ -40,6 +41,7 @@ final class SimulatorMainMenu { .addItem("Notifications", SimulatorNotifications.getActionProvider(context)) .addItem("Populate database", () -> populateDatabase(context)) .addItem("Clean database", () -> cleanDatabase(context)) + .addItem("clear preferred SIM", () -> clearPreferredSim(context)) .addItem("Sync voicemail", () -> syncVoicemail(context)) .addItem("Share persistent log", () -> sharePersistentLog(context)) .addItem( @@ -63,6 +65,14 @@ final class SimulatorMainMenu { .executeSerial(context); } + private static void clearPreferredSim(Context context) { + DialerExecutorComponent.get(context) + .dialerExecutorFactory() + .createNonUiTaskBuilder(new ClearPreferredSimWorker()) + .build() + .executeSerial(context); + } + private static void syncVoicemail(@NonNull Context context) { Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL); context.sendBroadcast(intent); @@ -109,6 +119,15 @@ final class SimulatorMainMenu { } } + private static class ClearPreferredSimWorker implements Worker { + @Nullable + @Override + public Void doInBackground(Context context) { + context.getContentResolver().delete(PreferredSimFallbackContract.CONTENT_URI, null, null); + return null; + } + } + private static class ShareLogWorker implements Worker { @Nullable @Override -- cgit v1.2.3