diff options
author | twyen <twyen@google.com> | 2017-11-18 06:11:45 +0000 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2017-11-18 06:11:45 +0000 |
commit | 0e633ea385f141014242d99490e86b7ecb12ec45 (patch) | |
tree | d79faaba4496b80182d6d2a17efd96e4880c3a25 | |
parent | de4197fff69fb13424e1ea923fdbfc65fc0fbd58 (diff) | |
parent | 9c04a6a796768e97ecbc1061a3d2d1c47b24a3a9 (diff) |
Merge changes I6a5a8ad8,I096ea022,Id164d08c
am: 9c04a6a796
Change-Id: I2dbce58e548d10ab62b60425071918b64f3cee64
13 files changed, 687 insertions, 108 deletions
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/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 @@ +<!-- + ~ Copyright (C) 2017 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.dialer.precall.externalreceiver"> + + <application> + <activity + android:excludeFromRecents="true" + android:exported="true" + android:name="com.android.dialer.precall.externalreceiver.LaunchPreCallActivity" + android:noHistory="true" + android:permission="android.permission.CALL_PHONE" + android:theme="@style/Theme.PreCall.DialogHolder"> + <intent-filter> + <action android:name="com.android.dialer.LAUNCH_PRE_CALL"/> + <category android:name="android.intent.category.DEFAULT" /> + <data android:scheme="tel" /> + </intent-filter> + <intent-filter> + <action android:name="com.android.dialer.LAUNCH_PRE_CALL"/> + <category android:name="android.intent.category.DEFAULT" /> + <data android:scheme="voicemail" /> + </intent-filter> + </activity> + </application> +</manifest> 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/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> 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<String> dataId = Optional.absent(); + } + + private static class PreferredAccountWorker + implements Worker<Context, PreferredAccountWorkerResult> { + + 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<String> 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<String> 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<PhoneAccountHandle> 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<WritePreferredAccountWorkerInput, Void> { + + @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<PhoneAccountHandle> 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<Context, Void> { + @Nullable + @Override + public Void doInBackground(Context context) { + context.getContentResolver().delete(PreferredSimFallbackContract.CONTENT_URI, null, null); + return null; + } + } + private static class ShareLogWorker implements Worker<Void, String> { @Nullable @Override 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<NewVoicemailViewHol private final Cursor cursor; private final Clock clock; + private final FragmentManager fragmentManager; /** A valid id for {@link VoicemailEntry} is greater than 0 */ private int currentlyExpandedViewHolderId = -1; @@ -40,13 +42,16 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter<NewVoicemailViewHol private final Set<NewVoicemailViewHolder> 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<NewVoicemailViewHol @Override public void onBindViewHolder(NewVoicemailViewHolder viewHolder, int position) { - LogUtil.i("onBindViewHolder", "position" + position); cursor.moveToPosition(position); - viewHolder.bind(cursor); + viewHolder.bind(cursor, fragmentManager); expandOrCollapseViewHolder(viewHolder); } diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java b/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java index 9c1fd8b85..9a89dbe3e 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java @@ -54,7 +54,9 @@ public final class NewVoicemailFragment extends Fragment implements LoaderCallba public void onLoadFinished(Loader<Cursor> loader, Cursor data) { LogUtil.i("NewVoicemailFragment.onCreateLoader", "cursor size is %d", data.getCount()); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(new NewVoicemailAdapter(data, 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<Context, Uri>, Pair<Boolean, Uri>> checkVoicemailHasContent = + this::queryVoicemailHasContent; + SuccessListener<Pair<Boolean, Uri>> checkVoicemailHasContentCallBack = this::prepareMediaPlayer; + + DialerExecutorComponent.get(getContext()) + .dialerExecutorFactory() + .createUiTaskBuilder(fragmentManager, "lookup_voicemail_content", checkVoicemailHasContent) + .onSuccess(checkVoicemailHasContentCallBack) + .build() + .executeSerial(new Pair<>(getContext(), voicemailUri)); + } + + private Pair<Boolean, Uri> queryVoicemailHasContent(Pair<Context, Uri> 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<Boolean, Uri> 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"/> - <com.android.dialer.voicemail.listui.NewVoicemailMediaPlayer + <com.android.dialer.voicemail.listui.NewVoicemailMediaPlayerView android:id="@+id/new_voicemail_media_player" android:layout_width="match_parent" android:layout_height="wrap_content" 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 \ |