From 7f78e9a692d7d7ca1f1204421adce91545a880f8 Mon Sep 17 00:00:00 2001 From: yueg Date: Tue, 12 Sep 2017 11:10:45 -0700 Subject: Log swiping and clicking for switching tabs. If it's a swipe, onPageScrolled() is called several times before onPageScrollStateChanged(SCROLL_STATE_SETTLING) and onPageSelected(). If it's a click, only onPageScrollStateChanged(SCROLL_STATE_SETTLING) is called before onPageSelected(). And onPageScrollStateChanged(SCROLL_STATE_SETTLING) will not be called if user don't switch to a new tab. We use the difference to tell if user switching tabs by swiping or clicking. Test: DialtactsActivityTest PiperOrigin-RevId: 168403148 Change-Id: Iaaf84ab9c4955d0bc2c1e9857ba59fd37b3984af --- java/com/android/dialer/simulator/Simulator.java | 22 +++ .../simulator/impl/SimulatorActionProvider.java | 165 ----------------- .../dialer/simulator/impl/SimulatorConnection.java | 39 +++- .../simulator/impl/SimulatorConnectionService.java | 120 ++----------- .../dialer/simulator/impl/SimulatorImpl.java | 2 +- .../dialer/simulator/impl/SimulatorMainMenu.java | 113 ++++++++++++ .../simulator/impl/SimulatorMissedCallCreator.java | 2 +- .../simulator/impl/SimulatorNotifications.java | 78 ++------ .../simulator/impl/SimulatorPreviewCamera.java | 166 +++++++++++++++++ .../simulator/impl/SimulatorRemoteVideo.java | 163 +++++++++++++++++ .../simulator/impl/SimulatorSimCallManager.java | 199 +++++++++++++++++++++ .../simulator/impl/SimulatorSpamCallCreator.java | 2 +- .../dialer/simulator/impl/SimulatorSubMenu.java | 100 +++++++++++ .../dialer/simulator/impl/SimulatorVideoCall.java | 164 +++++++++++++++++ .../simulator/impl/SimulatorVideoProvider.java | 125 +++++++++++++ .../dialer/simulator/impl/SimulatorVoiceCall.java | 103 ++++++++++- 16 files changed, 1207 insertions(+), 356 deletions(-) delete mode 100644 java/com/android/dialer/simulator/impl/SimulatorActionProvider.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorMainMenu.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorPreviewCamera.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorRemoteVideo.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorSubMenu.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorVideoCall.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorVideoProvider.java (limited to 'java/com/android/dialer/simulator') diff --git a/java/com/android/dialer/simulator/Simulator.java b/java/com/android/dialer/simulator/Simulator.java index f4164158e..f753e5f6b 100644 --- a/java/com/android/dialer/simulator/Simulator.java +++ b/java/com/android/dialer/simulator/Simulator.java @@ -22,6 +22,7 @@ import android.support.annotation.Nullable; import android.view.ActionProvider; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; /** Used to add menu items to the Dialer menu to test the app using simulated calls and data. */ public interface Simulator { @@ -42,6 +43,7 @@ public interface Simulator { DISCONNECT, STATE_CHANGE, DTMF, + SESSION_MODIFY_REQUEST, }) public @interface Type {} @@ -53,6 +55,7 @@ public interface Simulator { public static final int DISCONNECT = 5; public static final int STATE_CHANGE = 6; public static final int DTMF = 7; + public static final int SESSION_MODIFY_REQUEST = 8; @Type public final int type; /** Holds event specific information. For example, for DTMF this could be the keycode. */ @@ -71,5 +74,24 @@ public interface Simulator { this.data1 = data1; this.data2 = data2; } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Event)) { + return false; + } + Event event = (Event) other; + return type == event.type + && Objects.equals(data1, event.data1) + && Objects.equals(data2, event.data2); + } + + @Override + public int hashCode() { + return Objects.hash(Integer.valueOf(type), data1, data2); + } } } diff --git a/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java b/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java deleted file mode 100644 index f095a5993..000000000 --- a/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.dialer.simulator.impl; - -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.provider.VoicemailContract; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.view.ActionProvider; -import android.view.MenuItem; -import android.view.SubMenu; -import android.view.View; -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.DialerExecutors; -import com.android.dialer.databasepopulator.CallLogPopulator; -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; - -/** Implements the simulator submenu. */ -final class SimulatorActionProvider extends ActionProvider { - @NonNull private final Context context; - - private static class ShareLogWorker implements Worker { - - @Nullable - @Override - public String doInBackground(Void unused) { - return PersistentLogger.dumpLogToString(); - } - } - - public SimulatorActionProvider(@NonNull Context context) { - super(Assert.isNotNull(context)); - this.context = context; - } - - @Override - public View onCreateActionView() { - LogUtil.enterBlock("SimulatorActionProvider.onCreateActionView(null)"); - return null; - } - - @Override - public View onCreateActionView(MenuItem forItem) { - LogUtil.enterBlock("SimulatorActionProvider.onCreateActionView(MenuItem)"); - return null; - } - - @Override - public boolean hasSubMenu() { - LogUtil.enterBlock("SimulatorActionProvider.hasSubMenu"); - return true; - } - - @Override - public void onPrepareSubMenu(SubMenu subMenu) { - super.onPrepareSubMenu(subMenu); - LogUtil.enterBlock("SimulatorActionProvider.onPrepareSubMenu"); - subMenu.clear(); - - subMenu - .add("Add call") - .setOnMenuItemClickListener( - (item) -> { - SimulatorVoiceCall.addNewIncomingCall(context); - return true; - }); - - subMenu - .add("Notifiations") - .setActionProvider(SimulatorNotifications.getActionProvider(context)); - subMenu - .add("Populate database") - .setOnMenuItemClickListener( - (item) -> { - populateDatabase(); - return true; - }); - subMenu - .add("Clean database") - .setOnMenuItemClickListener( - (item) -> { - cleanDatabase(); - return true; - }); - subMenu - .add("Sync Voicemail") - .setOnMenuItemClickListener( - (item) -> { - Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL); - context.sendBroadcast(intent); - return true; - }); - - subMenu - .add("Share persistent log") - .setOnMenuItemClickListener( - (item) -> { - DialerExecutors.createNonUiTaskBuilder(new ShareLogWorker()) - .onSuccess( - (String log) -> { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, log); - if (intent.resolveActivity(context.getPackageManager()) != null) { - context.startActivity(intent); - } - }) - .build() - .executeSerial(null); - return true; - }); - subMenu - .add("Enriched call simulator") - .setOnMenuItemClickListener( - (item) -> { - context.startActivity(EnrichedCallSimulatorActivity.newIntent(context)); - return true; - }); - } - - private void populateDatabase() { - new AsyncTask() { - @Override - public Void doInBackground(Void... params) { - ContactsPopulator.populateContacts(context); - CallLogPopulator.populateCallLog(context); - VoicemailPopulator.populateVoicemail(context); - return null; - } - }.execute(); - } - - private void cleanDatabase() { - new AsyncTask() { - @Override - public Void doInBackground(Void... params) { - ContactsPopulator.deleteAllContacts(context); - CallLogPopulator.deleteAllCallLog(context); - VoicemailPopulator.deleteAllVoicemail(context); - return null; - } - }.execute(); - } -} diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnection.java b/java/com/android/dialer/simulator/impl/SimulatorConnection.java index b462b5405..70c1095dc 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorConnection.java +++ b/java/com/android/dialer/simulator/impl/SimulatorConnection.java @@ -16,8 +16,11 @@ package com.android.dialer.simulator.impl; +import android.content.Context; import android.support.annotation.NonNull; import android.telecom.Connection; +import android.telecom.ConnectionRequest; +import android.telecom.VideoProfile; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.simulator.Simulator.Event; @@ -30,6 +33,18 @@ public final class SimulatorConnection extends Connection { private final List events = new ArrayList<>(); private int currentState = STATE_NEW; + SimulatorConnection(@NonNull Context context, @NonNull ConnectionRequest request) { + Assert.isNotNull(context); + Assert.isNotNull(request); + putExtras(request.getExtras()); + setConnectionCapabilities( + CAPABILITY_MUTE + | CAPABILITY_SUPPORT_HOLD + | CAPABILITY_HOLD + | CAPABILITY_CAN_UPGRADE_TO_VIDEO); + setVideoProvider(new SimulatorVideoProvider(context, this)); + } + public void addListener(@NonNull Listener listener) { listeners.add(Assert.isNotNull(listener)); } @@ -44,9 +59,9 @@ public final class SimulatorConnection extends Connection { } @Override - public void onAnswer() { + public void onAnswer(int videoState) { LogUtil.enterBlock("SimulatorConnection.onAnswer"); - onEvent(new Event(Event.ANSWER)); + onEvent(new Event(Event.ANSWER, Integer.toString(videoState), null)); } @Override @@ -75,9 +90,14 @@ public final class SimulatorConnection extends Connection { @Override public void onStateChanged(int newState) { - LogUtil.enterBlock("SimulatorConnection.onStateChanged"); - onEvent(new Event(Event.STATE_CHANGE, stateToString(currentState), stateToString(newState))); + LogUtil.i( + "SimulatorConnection.onStateChanged", + "%s -> %s", + stateToString(currentState), + stateToString(newState)); + int oldState = currentState; currentState = newState; + onEvent(new Event(Event.STATE_CHANGE, stateToString(oldState), stateToString(newState))); } @Override @@ -86,13 +106,22 @@ public final class SimulatorConnection extends Connection { onEvent(new Event(Event.DTMF, Character.toString(c), null)); } - private void onEvent(@NonNull Event event) { + void onEvent(@NonNull Event event) { events.add(Assert.isNotNull(event)); for (Listener listener : listeners) { listener.onEvent(this, event); } } + void handleSessionModifyRequest(@NonNull Event event) { + VideoProfile fromProfile = new VideoProfile(Integer.parseInt(event.data1)); + VideoProfile toProfile = new VideoProfile(Integer.parseInt(event.data2)); + setVideoState(toProfile.getVideoState()); + getVideoProvider() + .receiveSessionModifyResponse( + Connection.VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS, fromProfile, toProfile); + } + /** Callback for when a new event arrives. */ public interface Listener { void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event); diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java index 06c2591cc..25d4a7240 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java +++ b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java @@ -16,10 +16,7 @@ package com.android.dialer.simulator.impl; -import android.content.ComponentName; -import android.content.Context; import android.net.Uri; -import android.os.Bundle; import android.support.annotation.NonNull; import android.telecom.Connection; import android.telecom.ConnectionRequest; @@ -34,72 +31,15 @@ import com.android.dialer.common.LogUtil; import java.util.ArrayList; import java.util.List; -/** Simple connection provider to create an incoming call. This is useful for emulators. */ +/** Simple connection provider to create phone calls. This is useful for emulators. */ public class SimulatorConnectionService extends ConnectionService { - - private static final String PHONE_ACCOUNT_ID = "SIMULATOR_ACCOUNT_ID"; - private static final String EXTRA_IS_SIMULATOR_CONNECTION = "is_simulator_connection"; private static final List listeners = new ArrayList<>(); private static SimulatorConnectionService instance; - private static void register(@NonNull Context context) { - LogUtil.enterBlock("SimulatorConnectionService.register"); - Assert.isNotNull(context); - context.getSystemService(TelecomManager.class).registerPhoneAccount(buildPhoneAccount(context)); - } - - private static void unregister(@NonNull Context context) { - LogUtil.enterBlock("SimulatorConnectionService.unregister"); - Assert.isNotNull(context); - context - .getSystemService(TelecomManager.class) - .unregisterPhoneAccount(buildPhoneAccount(context).getAccountHandle()); - } - public static SimulatorConnectionService getInstance() { return instance; } - public static void addNewOutgoingCall( - @NonNull Context context, @NonNull Bundle extras, @NonNull String phoneNumber) { - LogUtil.enterBlock("SimulatorConnectionService.addNewOutgoingCall"); - Assert.isNotNull(context); - Assert.isNotNull(extras); - Assert.isNotNull(phoneNumber); - - register(context); - - Bundle bundle = new Bundle(extras); - bundle.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true); - Bundle outgoingCallExtras = new Bundle(); - outgoingCallExtras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, bundle); - - // Use the system's phone account so that these look like regular SIM call. - TelecomManager telecomManager = context.getSystemService(TelecomManager.class); - telecomManager.placeCall( - Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null), outgoingCallExtras); - } - - public static void addNewIncomingCall( - @NonNull Context context, @NonNull Bundle extras, @NonNull String callerId) { - LogUtil.enterBlock("SimulatorConnectionService.addNewIncomingCall"); - Assert.isNotNull(context); - Assert.isNotNull(extras); - Assert.isNotNull(callerId); - - register(context); - - Bundle bundle = new Bundle(extras); - bundle.putString(TelephonyManager.EXTRA_INCOMING_NUMBER, callerId); - bundle.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true); - - // Use the system's phone account so that these look like regular SIM call. - TelecomManager telecomManager = context.getSystemService(TelecomManager.class); - PhoneAccountHandle systemPhoneAccount = - telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL); - telecomManager.addNewIncomingCall(systemPhoneAccount, bundle); - } - public static void addListener(@NonNull Listener listener) { listeners.add(Assert.isNotNull(listener)); } @@ -108,32 +48,6 @@ public class SimulatorConnectionService extends ConnectionService { listeners.remove(Assert.isNotNull(listener)); } - @NonNull - private static PhoneAccount buildPhoneAccount(Context context) { - PhoneAccount.Builder builder = - new PhoneAccount.Builder( - getConnectionServiceHandle(context), "Simulator connection service"); - List uriSchemes = new ArrayList<>(); - uriSchemes.add(PhoneAccount.SCHEME_TEL); - - return builder - .setCapabilities( - PhoneAccount.CAPABILITY_CALL_PROVIDER | PhoneAccount.CAPABILITY_CONNECTION_MANAGER) - .setShortDescription("Simulator Connection Service") - .setSupportedUriSchemes(uriSchemes) - .build(); - } - - public static PhoneAccountHandle getConnectionServiceHandle(Context context) { - return new PhoneAccountHandle( - new ComponentName(context, SimulatorConnectionService.class), PHONE_ACCOUNT_ID); - } - - private static Uri getPhoneNumber(ConnectionRequest request) { - String phoneNumber = request.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER); - return Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null); - } - @Override public void onCreate() { super.onCreate(); @@ -151,25 +65,19 @@ public class SimulatorConnectionService extends ConnectionService { public Connection onCreateOutgoingConnection( PhoneAccountHandle phoneAccount, ConnectionRequest request) { LogUtil.enterBlock("SimulatorConnectionService.onCreateOutgoingConnection"); - if (!isSimulatorConnectionRequest(request)) { + if (!SimulatorSimCallManager.isSimulatorConnectionRequest(request)) { LogUtil.i( "SimulatorConnectionService.onCreateOutgoingConnection", "outgoing call not from simulator, unregistering"); - Toast.makeText( - this, "Unregistering Dialer simulator, making a real phone call", Toast.LENGTH_LONG) + Toast.makeText(this, "Unregistering simulator, making a real phone call", Toast.LENGTH_LONG) .show(); - unregister(this); + SimulatorSimCallManager.unregister(this); return null; } - SimulatorConnection connection = new SimulatorConnection(); + SimulatorConnection connection = new SimulatorConnection(this, request); connection.setDialing(); connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED); - connection.setConnectionCapabilities( - Connection.CAPABILITY_MUTE - | Connection.CAPABILITY_SUPPORT_HOLD - | Connection.CAPABILITY_HOLD); - connection.putExtras(request.getExtras()); for (Listener listener : listeners) { listener.onNewOutgoingConnection(connection); @@ -181,23 +89,19 @@ public class SimulatorConnectionService extends ConnectionService { public Connection onCreateIncomingConnection( PhoneAccountHandle phoneAccount, ConnectionRequest request) { LogUtil.enterBlock("SimulatorConnectionService.onCreateIncomingConnection"); - if (!isSimulatorConnectionRequest(request)) { + if (!SimulatorSimCallManager.isSimulatorConnectionRequest(request)) { LogUtil.i( "SimulatorConnectionService.onCreateIncomingConnection", "incoming call not from simulator, unregistering"); - Toast.makeText( - this, "Unregistering Dialer simulator, got a real incoming call", Toast.LENGTH_LONG) + Toast.makeText(this, "Unregistering simulator, got a real incoming call", Toast.LENGTH_LONG) .show(); - unregister(this); + SimulatorSimCallManager.unregister(this); return null; } - SimulatorConnection connection = new SimulatorConnection(); + SimulatorConnection connection = new SimulatorConnection(this, request); connection.setRinging(); connection.setAddress(getPhoneNumber(request), TelecomManager.PRESENTATION_ALLOWED); - connection.setConnectionCapabilities( - Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD); - connection.putExtras(request.getExtras()); for (Listener listener : listeners) { listener.onNewIncomingConnection(connection); @@ -205,9 +109,9 @@ public class SimulatorConnectionService extends ConnectionService { return connection; } - private static boolean isSimulatorConnectionRequest(@NonNull ConnectionRequest request) { - return request.getExtras() != null - && request.getExtras().getBoolean(EXTRA_IS_SIMULATOR_CONNECTION); + private static Uri getPhoneNumber(ConnectionRequest request) { + String phoneNumber = request.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER); + return Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null); } /** Callback used to notify listeners when a new connection has been added. */ diff --git a/java/com/android/dialer/simulator/impl/SimulatorImpl.java b/java/com/android/dialer/simulator/impl/SimulatorImpl.java index 2dd180ef4..d6ee5ef38 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorImpl.java +++ b/java/com/android/dialer/simulator/impl/SimulatorImpl.java @@ -35,6 +35,6 @@ final class SimulatorImpl implements Simulator { @Override public ActionProvider getActionProvider(Context context) { - return new SimulatorActionProvider(context); + return SimulatorMainMenu.getActionProvider(context); } } diff --git a/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java b/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java new file mode 100644 index 000000000..d663d584d --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorMainMenu.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.simulator.impl; + +import android.content.Context; +import android.content.Intent; +import android.provider.VoicemailContract; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.ActionProvider; +import com.android.dialer.common.concurrent.DialerExecutor.Worker; +import com.android.dialer.common.concurrent.DialerExecutors; +import com.android.dialer.databasepopulator.CallLogPopulator; +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; + +/** Implements the top level simulator menu. */ +final class SimulatorMainMenu { + + static ActionProvider getActionProvider(@NonNull Context context) { + return new SimulatorSubMenu(context) + .addItem("Voice call", SimulatorVoiceCall.getActionProvider(context)) + .addItem("IMS video", SimulatorVideoCall.getActionProvider(context)) + .addItem("Notifications", SimulatorNotifications.getActionProvider(context)) + .addItem("Populate database", () -> populateDatabase(context)) + .addItem("Clean database", () -> cleanDatabase(context)) + .addItem("Sync voicemail", () -> syncVoicemail(context)) + .addItem("Share persistent log", () -> sharePersistentLog(context)) + .addItem( + "Enriched call simulator", + () -> context.startActivity(EnrichedCallSimulatorActivity.newIntent(context))); + } + + private static void populateDatabase(@NonNull Context context) { + DialerExecutors.createNonUiTaskBuilder(new PopulateDatabaseWorker()) + .build() + .executeSerial(context); + } + + private static void cleanDatabase(@NonNull Context context) { + DialerExecutors.createNonUiTaskBuilder(new CleanDatabaseWorker()) + .build() + .executeSerial(context); + } + + private static void syncVoicemail(@NonNull Context context) { + Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL); + context.sendBroadcast(intent); + } + + private static void sharePersistentLog(@NonNull Context context) { + DialerExecutors.createNonUiTaskBuilder(new ShareLogWorker()) + .onSuccess( + (String log) -> { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, log); + if (intent.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(intent); + } + }) + .build() + .executeSerial(null); + } + + private SimulatorMainMenu() {} + + private static class PopulateDatabaseWorker implements Worker { + @Nullable + @Override + public Void doInBackground(Context context) { + ContactsPopulator.populateContacts(context); + CallLogPopulator.populateCallLog(context); + VoicemailPopulator.populateVoicemail(context); + return null; + } + } + + private static class CleanDatabaseWorker implements Worker { + @Nullable + @Override + public Void doInBackground(Context context) { + ContactsPopulator.deleteAllContacts(context); + CallLogPopulator.deleteAllCallLog(context); + VoicemailPopulator.deleteAllVoicemail(context); + return null; + } + } + + private static class ShareLogWorker implements Worker { + @Nullable + @Override + public String doInBackground(Void unused) { + return PersistentLogger.dumpLogToString(); + } + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java b/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java index 22eb96731..f85f46602 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java +++ b/java/com/android/dialer/simulator/impl/SimulatorMissedCallCreator.java @@ -74,7 +74,7 @@ final class SimulatorMissedCallCreator implements SimulatorConnectionService.Lis extras.putInt(EXTRA_CALL_COUNT, callCount - 1); extras.putBoolean(EXTRA_IS_MISSED_CALL_CONNECTION, true); - SimulatorConnectionService.addNewIncomingCall(context, extras, callerId); + SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */, extras); } private static boolean isMissedCallConnection(@NonNull Connection connection) { diff --git a/java/com/android/dialer/simulator/impl/SimulatorNotifications.java b/java/com/android/dialer/simulator/impl/SimulatorNotifications.java index ebe8ecd46..3f402d317 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorNotifications.java +++ b/java/com/android/dialer/simulator/impl/SimulatorNotifications.java @@ -20,10 +20,6 @@ import android.content.Context; import android.provider.VoicemailContract.Voicemails; import android.support.annotation.NonNull; import android.view.ActionProvider; -import android.view.MenuItem; -import android.view.SubMenu; -import android.view.View; -import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.databasepopulator.VoicemailPopulator; import java.util.concurrent.TimeUnit; @@ -33,68 +29,18 @@ final class SimulatorNotifications { private static final int NOTIFICATION_COUNT = 12; static ActionProvider getActionProvider(@NonNull Context context) { - return new NotificationsActionProvider(context); - } - - private static class NotificationsActionProvider extends ActionProvider { - @NonNull private final Context context; - - public NotificationsActionProvider(@NonNull Context context) { - super(Assert.isNotNull(context)); - this.context = context; - } - - @Override - public View onCreateActionView() { - return null; - } - - @Override - public View onCreateActionView(MenuItem forItem) { - return null; - } - - @Override - public boolean hasSubMenu() { - return true; - } - - @Override - public void onPrepareSubMenu(@NonNull SubMenu subMenu) { - LogUtil.enterBlock("NotificationsActionProvider.onPrepareSubMenu"); - Assert.isNotNull(subMenu); - super.onPrepareSubMenu(subMenu); - - subMenu.clear(); - subMenu - .add("Missed Calls") - .setOnMenuItemClickListener( - (item) -> { - new SimulatorMissedCallCreator(context).start(NOTIFICATION_COUNT); - return true; - }); - subMenu - .add("Voicemails") - .setOnMenuItemClickListener( - (item) -> { - addVoicemailNotifications(context); - return true; - }); - subMenu - .add("Non spam") - .setOnMenuItemClickListener( - (item) -> { - new SimulatorSpamCallCreator(context, false /* isSpam */).start(NOTIFICATION_COUNT); - return true; - }); - subMenu - .add("Confirm spam") - .setOnMenuItemClickListener( - (item) -> { - new SimulatorSpamCallCreator(context, true /* isSpam */).start(NOTIFICATION_COUNT); - return true; - }); - } + return new SimulatorSubMenu(context) + .addItem( + "Missed calls", () -> new SimulatorMissedCallCreator(context).start(NOTIFICATION_COUNT)) + .addItem("Voicemails", () -> addVoicemailNotifications(context)) + .addItem( + "Non spam", + () -> + new SimulatorSpamCallCreator(context, false /* isSpam */).start(NOTIFICATION_COUNT)) + .addItem( + "Confirm spam", + () -> + new SimulatorSpamCallCreator(context, true /* isSpam */).start(NOTIFICATION_COUNT)); } private static void addVoicemailNotifications(@NonNull Context context) { diff --git a/java/com/android/dialer/simulator/impl/SimulatorPreviewCamera.java b/java/com/android/dialer/simulator/impl/SimulatorPreviewCamera.java new file mode 100644 index 000000000..e089f75ff --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorPreviewCamera.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.simulator.impl; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.telecom.VideoProfile.CameraCapabilities; +import android.util.Size; +import android.view.Surface; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import java.util.Arrays; + +/** + * Used by the video provider to draw the local camera. The in-call UI is responsible for setting + * the camera (front or back) and the view to draw to. The video provider then uses this class to + * capture frames from the given camera and draw to the given view. + */ +final class SimulatorPreviewCamera { + @NonNull private final Context context; + @NonNull private final String cameraId; + @NonNull private final Surface surface; + @Nullable private CameraDevice camera; + private boolean isStopped; + + SimulatorPreviewCamera( + @NonNull Context context, @NonNull String cameraId, @NonNull Surface surface) { + this.context = Assert.isNotNull(context); + this.cameraId = Assert.isNotNull(cameraId); + this.surface = Assert.isNotNull(surface); + } + + void startCamera() { + LogUtil.enterBlock("SimulatorPreviewCamera.startCamera"); + Assert.checkState(!isStopped); + try { + context + .getSystemService(CameraManager.class) + .openCamera(cameraId, new CameraListener(), null /* handler */); + } catch (CameraAccessException | SecurityException e) { + throw Assert.createIllegalStateFailException("camera error: " + e); + } + } + + void stopCamera() { + LogUtil.enterBlock("SimulatorPreviewCamera.stopCamera"); + isStopped = true; + if (camera != null) { + camera.close(); + camera = null; + } + } + + @Nullable + static CameraCapabilities getCameraCapabilities( + @NonNull Context context, @Nullable String cameraId) { + if (cameraId == null) { + LogUtil.e("SimulatorPreviewCamera.getCameraCapabilities", "null camera ID"); + return null; + } + + CameraManager cameraManager = context.getSystemService(CameraManager.class); + CameraCharacteristics characteristics; + try { + characteristics = cameraManager.getCameraCharacteristics(cameraId); + } catch (CameraAccessException e) { + throw Assert.createIllegalStateFailException("camera error: " + e); + } + + StreamConfigurationMap map = + characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + Size previewSize = map.getOutputSizes(SurfaceTexture.class)[0]; + LogUtil.i("SimulatorPreviewCamera.getCameraCapabilities", "preview size: " + previewSize); + return new CameraCapabilities(previewSize.getWidth(), previewSize.getHeight()); + } + + private final class CameraListener extends CameraDevice.StateCallback { + @Override + public void onOpened(CameraDevice camera) { + LogUtil.enterBlock("SimulatorPreviewCamera.CameraListener.onOpened"); + SimulatorPreviewCamera.this.camera = camera; + if (isStopped) { + LogUtil.i("SimulatorPreviewCamera.CameraListener.onOpened", "stopped"); + stopCamera(); + return; + } + + try { + camera.createCaptureSession( + Arrays.asList(Assert.isNotNull(surface)), + new CaptureSessionCallback(), + null /* handler */); + } catch (CameraAccessException e) { + throw Assert.createIllegalStateFailException("camera error: " + e); + } + } + + @Override + public void onError(CameraDevice camera, int error) { + LogUtil.i("SimulatorPreviewCamera.CameraListener.onError", "error: " + error); + stopCamera(); + } + + @Override + public void onDisconnected(CameraDevice camera) { + LogUtil.enterBlock("SimulatorPreviewCamera.CameraListener.onDisconnected"); + stopCamera(); + } + + @Override + public void onClosed(CameraDevice camera) { + LogUtil.enterBlock("SimulatorPreviewCamera.CameraListener.onCLosed"); + } + } + + private final class CaptureSessionCallback extends CameraCaptureSession.StateCallback { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + LogUtil.enterBlock("SimulatorPreviewCamera.CaptureSessionCallback.onConfigured"); + + if (isStopped) { + LogUtil.i("SimulatorPreviewCamera.CaptureSessionCallback.onConfigured", "stopped"); + stopCamera(); + return; + } + try { + CaptureRequest.Builder builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + builder.addTarget(surface); + builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + session.setRepeatingRequest( + builder.build(), null /* captureCallback */, null /* handler */); + } catch (CameraAccessException e) { + throw Assert.createIllegalStateFailException("camera error: " + e); + } + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + LogUtil.enterBlock("SimulatorPreviewCamera.CaptureSessionCallback.onConfigureFailed"); + } + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorRemoteVideo.java b/java/com/android/dialer/simulator/impl/SimulatorRemoteVideo.java new file mode 100644 index 000000000..b14bba39a --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorRemoteVideo.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.simulator.impl; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.os.Handler; +import android.os.HandlerThread; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.view.Surface; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; + +/** + * Used by the video provider to draw the remote party's video. The in-call UI is responsible for + * setting the view to draw to. Since the simulator doesn't have a remote party we simply draw a + * green screen with a ball bouncing around. + */ +final class SimulatorRemoteVideo { + @NonNull private final RenderThread thread; + private boolean isStopped; + + SimulatorRemoteVideo(@NonNull Surface surface) { + thread = new RenderThread(new Renderer(surface)); + } + + void startVideo() { + LogUtil.enterBlock("SimulatorRemoteVideo.startVideo"); + Assert.checkState(!isStopped); + thread.start(); + } + + void stopVideo() { + LogUtil.enterBlock("SimulatorRemoteVideo.stopVideo"); + isStopped = true; + thread.quitSafely(); + } + + @VisibleForTesting + Runnable getRenderer() { + return thread.getRenderer(); + } + + private static class Renderer implements Runnable { + private static final int FRAME_DELAY_MILLIS = 33; + private static final float CIRCLE_STEP = 16.0f; + + @NonNull private final Surface surface; + private float circleX; + private float circleY; + private float radius; + private double angle; + + Renderer(@NonNull Surface surface) { + this.surface = Assert.isNotNull(surface); + } + + @Override + public void run() { + drawFrame(); + schedule(); + } + + @WorkerThread + void schedule() { + Assert.isWorkerThread(); + new Handler().postDelayed(this, FRAME_DELAY_MILLIS); + } + + @WorkerThread + private void drawFrame() { + Assert.isWorkerThread(); + Canvas canvas; + try { + canvas = surface.lockCanvas(null /* dirtyRect */); + } catch (IllegalArgumentException e) { + // This can happen when the video fragment tears down. + LogUtil.e("SimulatorRemoteVideo.RenderThread.drawFrame", "unable to lock canvas", e); + return; + } + + LogUtil.i( + "SimulatorRemoteVideo.RenderThread.drawFrame", + "size; %d x %d", + canvas.getWidth(), + canvas.getHeight()); + canvas.drawColor(Color.GREEN); + moveCircle(canvas); + drawCircle(canvas); + surface.unlockCanvasAndPost(canvas); + } + + @WorkerThread + private void moveCircle(Canvas canvas) { + Assert.isWorkerThread(); + int width = canvas.getWidth(); + int height = canvas.getHeight(); + if (circleX == 0 && circleY == 0) { + circleX = width / 2.0f; + circleY = height / 2.0f; + angle = Math.PI / 4.0; + radius = Math.min(canvas.getWidth(), canvas.getHeight()) * 0.15f; + } else { + circleX += (float) Math.cos(angle) * CIRCLE_STEP; + circleY += (float) Math.sin(angle) * CIRCLE_STEP; + // Bounce the circle off the edge. + if (circleX + radius >= width + || circleX - radius <= 0 + || circleY + radius >= height + || circleY - radius <= 0) { + angle += Math.PI / 2.0; + } + } + } + + @WorkerThread + private void drawCircle(Canvas canvas) { + Assert.isWorkerThread(); + Paint paint = new Paint(); + paint.setColor(Color.MAGENTA); + paint.setStyle(Paint.Style.FILL); + canvas.drawCircle(circleX, circleY, radius, paint); + } + } + + private static class RenderThread extends HandlerThread { + @NonNull private final Renderer renderer; + + RenderThread(@NonNull Renderer renderer) { + super("SimulatorRemoteVideo"); + this.renderer = Assert.isNotNull(renderer); + } + + @Override + @WorkerThread + protected void onLooperPrepared() { + Assert.isWorkerThread(); + renderer.schedule(); + } + + @VisibleForTesting + Runnable getRenderer() { + return renderer; + } + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java new file mode 100644 index 000000000..33eac51d1 --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.simulator.impl; + +import android.content.ComponentName; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.telecom.ConnectionRequest; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telephony.TelephonyManager; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +/** + * Utility to use the simulator connection service to add phone calls. To ensure that the added + * calls are routed through the simulator we register ourselves as a SIM call manager using + * CAPABILITY_CONNECTION_MANAGER. This ensures that all calls on the device must first go through + * our connection service. + * + *

For video calls this will only work if the underlying telephony phone account also supports + * video. To ensure that video always works we use a separate video account. The user must manually + * enable this account in call settings for video calls to work. + */ +public class SimulatorSimCallManager { + + private static final String SIM_CALL_MANAGER_ACCOUNT_ID = "SIMULATOR_ACCOUNT_ID"; + private static final String VIDEO_PROVIDER_ACCOUNT_ID = "SIMULATOR_VIDEO_ACCOUNT_ID"; + private static final String EXTRA_IS_SIMULATOR_CONNECTION = "is_simulator_connection"; + + static void register(@NonNull Context context) { + LogUtil.enterBlock("SimulatorSimCallManager.register"); + Assert.isNotNull(context); + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + telecomManager.registerPhoneAccount(buildSimCallManagerAccount(context)); + telecomManager.registerPhoneAccount(buildVideoProviderAccount(context)); + } + + static void unregister(@NonNull Context context) { + LogUtil.enterBlock("SimulatorSimCallManager.unregister"); + Assert.isNotNull(context); + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + telecomManager.unregisterPhoneAccount(getSimCallManagerHandle(context)); + telecomManager.unregisterPhoneAccount(getVideoProviderHandle(context)); + } + + @NonNull + public static String addNewOutgoingCall( + @NonNull Context context, @NonNull String phoneNumber, boolean isVideo) { + return addNewOutgoingCall(context, phoneNumber, isVideo, new Bundle()); + } + + @NonNull + public static String addNewOutgoingCall( + @NonNull Context context, + @NonNull String phoneNumber, + boolean isVideo, + @NonNull Bundle extras) { + LogUtil.enterBlock("SimulatorSimCallManager.addNewOutgoingCall"); + Assert.isNotNull(context); + Assert.isNotNull(extras); + Assert.isNotNull(phoneNumber); + Assert.isNotNull(extras); + + register(context); + + extras = new Bundle(extras); + extras.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true); + String connectionTag = createUniqueConnectionTag(); + extras.putBoolean(connectionTag, true); + + Bundle outgoingCallExtras = new Bundle(); + outgoingCallExtras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras); + outgoingCallExtras.putParcelable( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, + isVideo ? getVideoProviderHandle(context) : getSystemPhoneAccountHandle(context)); + + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + try { + telecomManager.placeCall( + Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null), outgoingCallExtras); + } catch (SecurityException e) { + throw Assert.createIllegalStateFailException("Unable to place call: " + e); + } + return connectionTag; + } + + @NonNull + public static String addNewIncomingCall( + @NonNull Context context, @NonNull String callerId, boolean isVideo) { + return addNewIncomingCall(context, callerId, isVideo, new Bundle()); + } + + @NonNull + public static String addNewIncomingCall( + @NonNull Context context, @NonNull String callerId, boolean isVideo, @NonNull Bundle extras) { + LogUtil.enterBlock("SimulatorSimCallManager.addNewIncomingCall"); + Assert.isNotNull(context); + Assert.isNotNull(callerId); + Assert.isNotNull(extras); + + register(context); + + extras = new Bundle(extras); + extras.putString(TelephonyManager.EXTRA_INCOMING_NUMBER, callerId); + extras.putBoolean(EXTRA_IS_SIMULATOR_CONNECTION, true); + String connectionTag = createUniqueConnectionTag(); + extras.putBoolean(connectionTag, true); + + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + telecomManager.addNewIncomingCall( + isVideo ? getVideoProviderHandle(context) : getSystemPhoneAccountHandle(context), extras); + return connectionTag; + } + + @NonNull + private static PhoneAccount buildSimCallManagerAccount(Context context) { + return new PhoneAccount.Builder(getSimCallManagerHandle(context), "Simulator SIM call manager") + .setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER) + .setShortDescription("Simulator SIM call manager") + .setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL)) + .build(); + } + + @NonNull + private static PhoneAccount buildVideoProviderAccount(Context context) { + return new PhoneAccount.Builder(getVideoProviderHandle(context), "Simulator video provider") + .setCapabilities( + PhoneAccount.CAPABILITY_CALL_PROVIDER + | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING + | PhoneAccount.CAPABILITY_VIDEO_CALLING) + .setShortDescription("Simulator video provider") + .setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL)) + .build(); + } + + @NonNull + public static PhoneAccountHandle getSimCallManagerHandle(@NonNull Context context) { + return new PhoneAccountHandle( + new ComponentName(context, SimulatorConnectionService.class), SIM_CALL_MANAGER_ACCOUNT_ID); + } + + @NonNull + static PhoneAccountHandle getVideoProviderHandle(@NonNull Context context) { + return new PhoneAccountHandle( + new ComponentName(context, SimulatorConnectionService.class), VIDEO_PROVIDER_ACCOUNT_ID); + } + + @NonNull + private static PhoneAccountHandle getSystemPhoneAccountHandle(@NonNull Context context) { + TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + List handles; + try { + handles = telecomManager.getCallCapablePhoneAccounts(); + } catch (SecurityException e) { + throw Assert.createIllegalStateFailException("Unable to get phone accounts: " + e); + } + for (PhoneAccountHandle handle : handles) { + PhoneAccount account = telecomManager.getPhoneAccount(handle); + if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { + return handle; + } + } + throw Assert.createIllegalStateFailException("no SIM phone account available"); + } + + public static boolean isSimulatorConnectionRequest(@NonNull ConnectionRequest request) { + return request.getExtras() != null + && request.getExtras().getBoolean(EXTRA_IS_SIMULATOR_CONNECTION); + } + + @NonNull + private static String createUniqueConnectionTag() { + int callId = new Random().nextInt(); + return String.format("simulator_phone_call_%x", Math.abs(callId)); + } + + private SimulatorSimCallManager() {} +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java b/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java index ae97bc162..757658ddf 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java +++ b/java/com/android/dialer/simulator/impl/SimulatorSpamCallCreator.java @@ -91,7 +91,7 @@ final class SimulatorSpamCallCreator implements SimulatorConnectionService.Liste // We need to clear the call log because spam notifications are only shown for new calls. clearCallLog(context); - SimulatorConnectionService.addNewIncomingCall(context, extras, callerId); + SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */, extras); } private static boolean isSpamCallConnection(@NonNull Connection connection) { diff --git a/java/com/android/dialer/simulator/impl/SimulatorSubMenu.java b/java/com/android/dialer/simulator/impl/SimulatorSubMenu.java new file mode 100644 index 000000000..64a2e7265 --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorSubMenu.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.simulator.impl; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.ActionProvider; +import android.view.MenuItem; +import android.view.SubMenu; +import android.view.View; +import com.android.dialer.common.Assert; +import java.util.ArrayList; +import java.util.List; + +/** Makes it easier to create submenus in the simulator. */ +final class SimulatorSubMenu extends ActionProvider { + List items = new ArrayList<>(); + + SimulatorSubMenu(@NonNull Context context) { + super(Assert.isNotNull(context)); + } + + SimulatorSubMenu addItem(@NonNull String title, @NonNull Runnable clickHandler) { + items.add(new Item(title, clickHandler)); + return this; + } + + SimulatorSubMenu addItem(@NonNull String title, @NonNull ActionProvider actionProvider) { + items.add(new Item(title, actionProvider)); + return this; + } + + @Override + public View onCreateActionView() { + return null; + } + + @Override + public View onCreateActionView(MenuItem forItem) { + return null; + } + + @Override + public boolean hasSubMenu() { + return true; + } + + @Override + public void onPrepareSubMenu(SubMenu subMenu) { + super.onPrepareSubMenu(subMenu); + subMenu.clear(); + + for (Item item : items) { + if (item.clickHandler != null) { + subMenu + .add(item.title) + .setOnMenuItemClickListener( + (i) -> { + item.clickHandler.run(); + return true; + }); + } else { + subMenu.add(item.title).setActionProvider(item.actionProvider); + } + } + } + + private static final class Item { + @NonNull final String title; + @Nullable final Runnable clickHandler; + @Nullable final ActionProvider actionProvider; + + Item(@NonNull String title, @NonNull Runnable clickHandler) { + this.title = Assert.isNotNull(title); + this.clickHandler = Assert.isNotNull(clickHandler); + actionProvider = null; + } + + Item(@NonNull String title, @NonNull ActionProvider actionProvider) { + this.title = Assert.isNotNull(title); + this.clickHandler = null; + this.actionProvider = Assert.isNotNull(actionProvider); + } + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java b/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java new file mode 100644 index 000000000..3f00ab183 --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorVideoCall.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.simulator.impl; + +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.telecom.Connection; +import android.telecom.DisconnectCause; +import android.telecom.TelecomManager; +import android.telecom.VideoProfile; +import android.view.ActionProvider; +import android.widget.Toast; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.ThreadUtil; +import com.android.dialer.simulator.Simulator.Event; + +/** Entry point in the simulator to create video calls. */ +final class SimulatorVideoCall + implements SimulatorConnectionService.Listener, SimulatorConnection.Listener { + @NonNull private final Context context; + private final int initialVideoCapability; + private final int initialVideoState; + @Nullable private String connectionTag; + + static ActionProvider getActionProvider(@NonNull Context context) { + return new SimulatorSubMenu(context) + .addItem( + "Incoming one way", + () -> + new SimulatorVideoCall(context, VideoProfile.STATE_RX_ENABLED).addNewIncomingCall()) + .addItem( + "Incoming two way", + () -> + new SimulatorVideoCall(context, VideoProfile.STATE_BIDIRECTIONAL) + .addNewIncomingCall()) + .addItem( + "Outgoing one way", + () -> + new SimulatorVideoCall(context, VideoProfile.STATE_TX_ENABLED).addNewOutgoingCall()) + .addItem( + "Outgoing two way", + () -> + new SimulatorVideoCall(context, VideoProfile.STATE_BIDIRECTIONAL) + .addNewOutgoingCall()); + } + + private SimulatorVideoCall(@NonNull Context context, int initialVideoState) { + this.context = Assert.isNotNull(context); + this.initialVideoCapability = + Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL + | Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL; + this.initialVideoState = initialVideoState; + SimulatorConnectionService.addListener(this); + } + + private void addNewIncomingCall() { + if (!isVideoAccountEnabled()) { + showVideoAccountSettings(); + return; + } + String callerId = "+44 (0) 20 7031 3000"; // Google London office + connectionTag = + SimulatorSimCallManager.addNewIncomingCall(context, callerId, true /* isVideo */); + } + + private void addNewOutgoingCall() { + if (!isVideoAccountEnabled()) { + showVideoAccountSettings(); + return; + } + String phoneNumber = "+44 (0) 20 7031 3000"; // Google London office + connectionTag = + SimulatorSimCallManager.addNewOutgoingCall(context, phoneNumber, true /* isVideo */); + } + + @Override + public void onNewOutgoingConnection(@NonNull SimulatorConnection connection) { + if (connection.getExtras().getBoolean(connectionTag)) { + LogUtil.i("SimulatorVideoCall.onNewOutgoingConnection", "connection created"); + handleNewConnection(connection); + // Telecom will force the connection to switch to Dialing when we return it. Wait until after + // we're returned it before changing call state. + ThreadUtil.postOnUiThread(() -> connection.setActive()); + } + } + + @Override + public void onNewIncomingConnection(@NonNull SimulatorConnection connection) { + if (connection.getExtras().getBoolean(connectionTag)) { + LogUtil.i("SimulatorVideoCall.onNewIncomingConnection", "connection created"); + handleNewConnection(connection); + } + } + + private boolean isVideoAccountEnabled() { + SimulatorSimCallManager.register(context); + return context + .getSystemService(TelecomManager.class) + .getPhoneAccount(SimulatorSimCallManager.getVideoProviderHandle(context)) + .isEnabled(); + } + + private void showVideoAccountSettings() { + context.startActivity(new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)); + Toast.makeText(context, "Please enable simulator video provider", Toast.LENGTH_LONG).show(); + } + + private void handleNewConnection(@NonNull SimulatorConnection connection) { + connection.addListener(this); + connection.setConnectionCapabilities( + connection.getConnectionCapabilities() | initialVideoCapability); + connection.setVideoState(initialVideoState); + } + + @Override + public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) { + switch (event.type) { + case Event.NONE: + throw Assert.createIllegalStateFailException(); + case Event.ANSWER: + connection.setVideoState(Integer.parseInt(event.data1)); + connection.setActive(); + break; + case Event.REJECT: + connection.setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); + break; + case Event.HOLD: + connection.setOnHold(); + break; + case Event.UNHOLD: + connection.setActive(); + break; + case Event.DISCONNECT: + connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + break; + case Event.STATE_CHANGE: + break; + case Event.DTMF: + break; + case Event.SESSION_MODIFY_REQUEST: + ThreadUtil.postDelayedOnUiThread(() -> connection.handleSessionModifyRequest(event), 2000); + break; + default: + throw Assert.createIllegalStateFailException(); + } + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorVideoProvider.java b/java/com/android/dialer/simulator/impl/SimulatorVideoProvider.java new file mode 100644 index 000000000..a596728a8 --- /dev/null +++ b/java/com/android/dialer/simulator/impl/SimulatorVideoProvider.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.simulator.impl; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.telecom.Connection; +import android.telecom.VideoProfile; +import android.view.Surface; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.simulator.Simulator.Event; + +/** + * Implements the telecom video provider API to simulate IMS video calling. A video capable phone + * always has one video provider associated with it. Actual drawing of local and remote video is + * done by {@link SimulatorPreviewCamera} and {@link SimulatorRemoteVideo} respectively. + */ +final class SimulatorVideoProvider extends Connection.VideoProvider { + @NonNull private final Context context; + @NonNull private final SimulatorConnection connection; + @Nullable private String previewCameraId;; + @Nullable private SimulatorPreviewCamera simulatorPreviewCamera; + @Nullable private SimulatorRemoteVideo simulatorRemoteVideo; + + SimulatorVideoProvider(@NonNull Context context, @NonNull SimulatorConnection connection) { + this.context = Assert.isNotNull(context); + this.connection = Assert.isNotNull(connection); + } + + @Override + public void onSetCamera(String previewCameraId) { + LogUtil.i("SimulatorVideoProvider.onSetCamera", "previewCameraId: " + previewCameraId); + this.previewCameraId = previewCameraId; + if (simulatorPreviewCamera != null) { + simulatorPreviewCamera.stopCamera(); + simulatorPreviewCamera = null; + } + } + + @Override + public void onSetPreviewSurface(Surface surface) { + LogUtil.enterBlock("SimulatorVideoProvider.onSetPreviewSurface"); + if (simulatorPreviewCamera != null) { + simulatorPreviewCamera.stopCamera(); + simulatorPreviewCamera = null; + } + if (surface != null && previewCameraId != null) { + simulatorPreviewCamera = new SimulatorPreviewCamera(context, previewCameraId, surface); + simulatorPreviewCamera.startCamera(); + } + } + + @Override + public void onSetDisplaySurface(Surface surface) { + LogUtil.enterBlock("SimulatorVideoProvider.onSetDisplaySurface"); + if (simulatorRemoteVideo != null) { + simulatorRemoteVideo.stopVideo(); + simulatorRemoteVideo = null; + } + if (surface != null) { + simulatorRemoteVideo = new SimulatorRemoteVideo(surface); + simulatorRemoteVideo.startVideo(); + } + } + + @Override + public void onSetDeviceOrientation(int rotation) { + LogUtil.i("SimulatorVideoProvider.onSetDeviceOrientation", "rotation: " + rotation); + } + + @Override + public void onSetZoom(float value) { + LogUtil.i("SimulatorVideoProvider.onSetZoom", "zoom: " + value); + } + + @Override + public void onSendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile) { + LogUtil.enterBlock("SimulatorVideoProvider.onSendSessionModifyRequest"); + connection.onEvent( + new Event( + Event.SESSION_MODIFY_REQUEST, + Integer.toString(fromProfile.getVideoState()), + Integer.toString(toProfile.getVideoState()))); + } + + @Override + public void onSendSessionModifyResponse(VideoProfile responseProfile) { + LogUtil.enterBlock("SimulatorVideoProvider.onSendSessionModifyResponse"); + } + + @Override + public void onRequestCameraCapabilities() { + LogUtil.enterBlock("SimulatorVideoProvider.onRequestCameraCapabilities"); + changeCameraCapabilities( + SimulatorPreviewCamera.getCameraCapabilities(context, previewCameraId)); + } + + @Override + public void onRequestConnectionDataUsage() { + LogUtil.enterBlock("SimulatorVideoProvider.onRequestConnectionDataUsage"); + setCallDataUsage(10 * 1024); + } + + @Override + public void onSetPauseImage(Uri uri) { + LogUtil.enterBlock("SimulatorVideoProvider.onSetPauseImage"); + } +} diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java index 2512828b8..8eefb48d9 100644 --- a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java +++ b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java @@ -17,18 +17,103 @@ package com.android.dialer.simulator.impl; import android.content.Context; -import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.telecom.Connection; +import android.telecom.DisconnectCause; +import android.view.ActionProvider; +import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.ThreadUtil; +import com.android.dialer.simulator.Simulator.Event; -/** Utilities to simulate phone calls. */ -final class SimulatorVoiceCall { - static void addNewIncomingCall(@NonNull Context context) { - LogUtil.enterBlock("SimulatorVoiceCall.addNewIncomingCall"); - // Set the caller ID to the Google London office. - String callerId = "+44 (0) 20 7031 3000"; - SimulatorConnectionService.addNewIncomingCall(context, new Bundle(), callerId); +/** Entry point in the simulator to create voice calls. */ +final class SimulatorVoiceCall + implements SimulatorConnectionService.Listener, SimulatorConnection.Listener { + @NonNull private final Context context; + @Nullable private String connectionTag; + + static ActionProvider getActionProvider(@NonNull Context context) { + return new SimulatorSubMenu(context) + .addItem("Incoming call", () -> new SimulatorVoiceCall(context).addNewIncomingCall(false)) + .addItem("Outgoing call", () -> new SimulatorVoiceCall(context).addNewOutgoingCall()) + .addItem("Spam call", () -> new SimulatorVoiceCall(context).addNewIncomingCall(true)); + } + + private SimulatorVoiceCall(@NonNull Context context) { + this.context = Assert.isNotNull(context); + SimulatorConnectionService.addListener(this); + } + + private void addNewIncomingCall(boolean isSpam) { + String callerId = + isSpam + ? "+1-661-778-3020" /* Blacklisted custom spam number */ + : "+44 (0) 20 7031 3000" /* Google London office */; + connectionTag = + SimulatorSimCallManager.addNewIncomingCall(context, callerId, false /* isVideo */); + } + + private void addNewOutgoingCall() { + String callerId = "+55-31-2128-6800"; // Brazil office. + connectionTag = + SimulatorSimCallManager.addNewOutgoingCall(context, callerId, false /* isVideo */); } - private SimulatorVoiceCall() {} + @Override + public void onNewOutgoingConnection(@NonNull SimulatorConnection connection) { + if (connection.getExtras().getBoolean(connectionTag)) { + LogUtil.i("SimulatorVoiceCall.onNewOutgoingConnection", "connection created"); + handleNewConnection(connection); + connection.setActive(); + } + } + + @Override + public void onNewIncomingConnection(@NonNull SimulatorConnection connection) { + if (connection.getExtras().getBoolean(connectionTag)) { + LogUtil.i("SimulatorVoiceCall.onNewIncomingConnection", "connection created"); + handleNewConnection(connection); + } + } + + private void handleNewConnection(@NonNull SimulatorConnection connection) { + connection.addListener(this); + connection.setConnectionCapabilities( + connection.getConnectionCapabilities() + | Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL + | Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL); + } + + @Override + public void onEvent(@NonNull SimulatorConnection connection, @NonNull Event event) { + switch (event.type) { + case Event.NONE: + throw Assert.createIllegalStateFailException(); + case Event.ANSWER: + connection.setActive(); + break; + case Event.REJECT: + connection.setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); + break; + case Event.HOLD: + connection.setOnHold(); + break; + case Event.UNHOLD: + connection.setActive(); + break; + case Event.DISCONNECT: + connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + break; + case Event.STATE_CHANGE: + break; + case Event.DTMF: + break; + case Event.SESSION_MODIFY_REQUEST: + ThreadUtil.postDelayedOnUiThread(() -> connection.handleSessionModifyRequest(event), 2000); + break; + default: + throw Assert.createIllegalStateFailException(); + } + } } -- cgit v1.2.3