Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

View File

@@ -0,0 +1,119 @@
package org.thoughtcrime.securesms.webrtc;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
* Manages the state of the WebRtc items in the Android notification bar.
*
* @author Moxie Marlinspike
*
*/
public class CallNotificationBuilder {
private static final int WEBRTC_NOTIFICATION = 313388;
private static final int WEBRTC_NOTIFICATION_RINGING = 313389;
public static final int TYPE_INCOMING_RINGING = 1;
public static final int TYPE_OUTGOING_RINGING = 2;
public static final int TYPE_ESTABLISHED = 3;
public static final int TYPE_INCOMING_CONNECTING = 4;
public static Notification getCallInProgressNotification(Context context, int type, Recipient recipient) {
Intent contentIntent = new Intent(context, WebRtcCallActivity.class);
contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getNotificationChannel(context, type))
.setSmallIcon(R.drawable.ic_call_secure_white_24dp)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setContentTitle(recipient.getDisplayName(context));
if (type == TYPE_INCOMING_CONNECTING) {
builder.setContentText(context.getString(R.string.CallNotificationBuilder_connecting));
builder.setPriority(NotificationCompat.PRIORITY_MIN);
} else if (type == TYPE_INCOMING_RINGING) {
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call));
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.ACTION_DENY_CALL, R.drawable.ic_close_grey600_32dp, R.string.NotificationBarManager__deny_call));
builder.addAction(getActivityNotificationAction(context, WebRtcCallActivity.ANSWER_ACTION, R.drawable.ic_phone_grey600_32dp, R.string.NotificationBarManager__answer_call));
if (callActivityRestricted(context)) {
builder.setFullScreenIntent(pendingIntent, true);
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
builder.setCategory(NotificationCompat.CATEGORY_CALL);
}
} else if (type == TYPE_OUTGOING_RINGING) {
builder.setContentText(context.getString(R.string.NotificationBarManager__establishing_signal_call));
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.ACTION_LOCAL_HANGUP, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__cancel_call));
} else {
builder.setContentText(context.getString(R.string.NotificationBarManager_signal_call_in_progress));
builder.addAction(getServiceNotificationAction(context, WebRtcCallService.ACTION_LOCAL_HANGUP, R.drawable.ic_call_end_grey600_32dp, R.string.NotificationBarManager__end_call));
}
return builder.build();
}
public static int getNotificationId(@NonNull Context context, int type) {
if (callActivityRestricted(context) && type == TYPE_INCOMING_RINGING) {
return WEBRTC_NOTIFICATION_RINGING;
} else {
return WEBRTC_NOTIFICATION;
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isWebRtcNotification(int notificationId) {
return notificationId == WEBRTC_NOTIFICATION || notificationId == WEBRTC_NOTIFICATION_RINGING;
}
private static @NonNull String getNotificationChannel(@NonNull Context context, int type) {
if (callActivityRestricted(context) && type == TYPE_INCOMING_RINGING) {
return NotificationChannels.CALLS;
} else {
return NotificationChannels.OTHER;
}
}
private static NotificationCompat.Action getServiceNotificationAction(Context context, String action, int iconResId, int titleResId) {
Intent intent = new Intent(context, WebRtcCallService.class);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
return new NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent);
}
private static NotificationCompat.Action getActivityNotificationAction(@NonNull Context context, @NonNull String action,
@DrawableRes int iconResId, @StringRes int titleResId)
{
Intent intent = new Intent(context, WebRtcCallActivity.class);
intent.setAction(action);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
return new NotificationCompat.Action(iconResId, context.getString(titleResId), pendingIntent);
}
private static boolean callActivityRestricted(@NonNull Context context) {
return Build.VERSION.SDK_INT >= 29 && !ApplicationContext.getInstance(context).isAppVisible();
}
}

View File

@@ -0,0 +1,78 @@
package org.thoughtcrime.securesms.webrtc;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.telephony.TelephonyManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Listens for incoming PSTN calls and rejects them if a RedPhone call is already in progress.
*
* Unstable use of reflection employed to gain access to ITelephony.
*
*/
public class IncomingPstnCallReceiver extends BroadcastReceiver {
private static final String TAG = IncomingPstnCallReceiver.class.getSimpleName();
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "Checking incoming call...");
if (intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) == null) {
Log.w(TAG, "Telephony event does not contain number...");
return;
}
if (!intent.getStringExtra(TelephonyManager.EXTRA_STATE).equals(TelephonyManager.EXTRA_STATE_RINGING)) {
Log.w(TAG, "Telephony event is not state ringing...");
return;
}
InCallListener listener = new InCallListener(context, new Handler());
WebRtcCallService.isCallActive(context, listener);
}
private static class InCallListener extends ResultReceiver {
private final Context context;
InCallListener(Context context, Handler handler) {
super(handler);
this.context = context.getApplicationContext();
}
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == 1) {
Log.i(TAG, "Attempting to deny incoming PSTN call.");
TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
try {
Method getTelephony = tm.getClass().getDeclaredMethod("getITelephony");
getTelephony.setAccessible(true);
Object telephonyService = getTelephony.invoke(tm);
Method endCall = telephonyService.getClass().getDeclaredMethod("endCall");
endCall.invoke(telephonyService);
Log.i(TAG, "Denied Incoming Call.");
} catch (NoSuchMethodException e) {
Log.w(TAG, "Unable to access ITelephony API", e);
} catch (IllegalAccessException e) {
Log.w(TAG, "Unable to access ITelephony API", e);
} catch (InvocationTargetException e) {
Log.w(TAG, "Unable to access ITelephony API", e);
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
package org.thoughtcrime.securesms.webrtc;
import org.thoughtcrime.securesms.logging.Log;
import java.util.ArrayList;
import java.util.List;
/**
* Allows multiple default uncaught exception handlers to be registered
*
* Calls all registered handlers in reverse order of registration.
* Errors in one handler do not prevent subsequent handlers from being called.
*/
public class UncaughtExceptionHandlerManager implements Thread.UncaughtExceptionHandler {
private final Thread.UncaughtExceptionHandler originalHandler;
private final List<Thread.UncaughtExceptionHandler> handlers = new ArrayList<Thread.UncaughtExceptionHandler>();
public UncaughtExceptionHandlerManager() {
originalHandler = Thread.getDefaultUncaughtExceptionHandler();
registerHandler(originalHandler);
Thread.setDefaultUncaughtExceptionHandler(this);
}
public void registerHandler(Thread.UncaughtExceptionHandler handler) {
handlers.add(handler);
}
public void unregister() {
Thread.setDefaultUncaughtExceptionHandler(originalHandler);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
for (int i = handlers.size() - 1; i >= 0; i--) {
try {
handlers.get(i).uncaughtException(thread, throwable);
} catch(Throwable t) {
Log.e("UncaughtExceptionHandlerManager", "Error in uncaught exception handling", t);
}
}
}
}

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.webrtc;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.text.TextUtils;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
public class VoiceCallShare extends Activity {
private static final String TAG = VoiceCallShare.class.getSimpleName();
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
if (getIntent().getData() != null && "content".equals(getIntent().getData().getScheme())) {
Cursor cursor = null;
try {
cursor = getContentResolver().query(getIntent().getData(), null, null, null, null);
if (cursor != null && cursor.moveToNext()) {
String destination = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1));
SimpleTask.run(() -> Recipient.external(this, destination), recipient -> {
if (!TextUtils.isEmpty(destination)) {
Intent serviceIntent = new Intent(this, WebRtcCallService.class);
serviceIntent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
serviceIntent.putExtra(WebRtcCallService.EXTRA_REMOTE_RECIPIENT, recipient.getId());
startService(serviceIntent);
Intent activityIntent = new Intent(this, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(activityIntent);
}
});
}
} finally {
if (cursor != null) cursor.close();
}
}
finish();
}
}

View File

@@ -0,0 +1,230 @@
package org.thoughtcrime.securesms.webrtc.audio;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class BluetoothStateManager {
private static final String TAG = BluetoothStateManager.class.getSimpleName();
private enum ScoConnection {
DISCONNECTED,
IN_PROGRESS,
CONNECTED
}
private final Object LOCK = new Object();
private final Context context;
private final BluetoothAdapter bluetoothAdapter;
private BluetoothScoReceiver bluetoothScoReceiver;
private BluetoothConnectionReceiver bluetoothConnectionReceiver;
private final BluetoothStateListener listener;
private final AtomicBoolean destroyed;
private BluetoothHeadset bluetoothHeadset = null;
private ScoConnection scoConnection = ScoConnection.DISCONNECTED;
private boolean wantsConnection = false;
public BluetoothStateManager(@NonNull Context context, @Nullable BluetoothStateListener listener) {
this.context = context.getApplicationContext();
this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
this.bluetoothScoReceiver = new BluetoothScoReceiver();
this.bluetoothConnectionReceiver = new BluetoothConnectionReceiver();
this.listener = listener;
this.destroyed = new AtomicBoolean(false);
if (this.bluetoothAdapter == null)
return;
requestHeadsetProxyProfile();
this.context.registerReceiver(bluetoothConnectionReceiver, new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED));
Intent sticky = this.context.registerReceiver(bluetoothScoReceiver, new IntentFilter(getScoChangeIntent()));
if (sticky != null) {
bluetoothScoReceiver.onReceive(context, sticky);
}
handleBluetoothStateChange();
}
public void onDestroy() {
destroyed.set(true);
if (bluetoothHeadset != null && bluetoothAdapter != null) {
this.bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
}
if (bluetoothConnectionReceiver != null) {
context.unregisterReceiver(bluetoothConnectionReceiver);
bluetoothConnectionReceiver = null;
}
if (bluetoothScoReceiver != null) {
context.unregisterReceiver(bluetoothScoReceiver);
bluetoothScoReceiver = null;
}
this.bluetoothHeadset = null;
}
public void setWantsConnection(boolean enabled) {
synchronized (LOCK) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
this.wantsConnection = enabled;
if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
audioManager.startBluetoothSco();
scoConnection = ScoConnection.IN_PROGRESS;
} else if (!wantsConnection && scoConnection == ScoConnection.CONNECTED) {
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
scoConnection = ScoConnection.DISCONNECTED;
} else if (!wantsConnection && scoConnection == ScoConnection.IN_PROGRESS) {
audioManager.stopBluetoothSco();
scoConnection = ScoConnection.DISCONNECTED;
}
}
}
private void handleBluetoothStateChange() {
if (listener != null && !destroyed.get()) listener.onBluetoothStateChanged(isBluetoothAvailable());
}
private boolean isBluetoothAvailable() {
try {
synchronized (LOCK) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) return false;
if (!audioManager.isBluetoothScoAvailableOffCall()) return false;
return bluetoothHeadset != null && !bluetoothHeadset.getConnectedDevices().isEmpty();
}
} catch (Exception e) {
Log.w(TAG, e);
return false;
}
}
private String getScoChangeIntent() {
return AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED;
}
private void requestHeadsetProxyProfile() {
this.bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (destroyed.get()) {
Log.w(TAG, "Got bluetooth profile event after the service was destroyed. Ignoring.");
return;
}
if (profile == BluetoothProfile.HEADSET) {
synchronized (LOCK) {
bluetoothHeadset = (BluetoothHeadset) proxy;
}
Intent sticky = context.registerReceiver(null, new IntentFilter(getScoChangeIntent()));
bluetoothScoReceiver.onReceive(context, sticky);
synchronized (LOCK) {
if (wantsConnection && isBluetoothAvailable() && scoConnection == ScoConnection.DISCONNECTED) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
audioManager.startBluetoothSco();
scoConnection = ScoConnection.IN_PROGRESS;
}
}
handleBluetoothStateChange();
}
}
@Override
public void onServiceDisconnected(int profile) {
Log.i(TAG, "onServiceDisconnected");
if (profile == BluetoothProfile.HEADSET) {
bluetoothHeadset = null;
handleBluetoothStateChange();
}
}
}, BluetoothProfile.HEADSET);
}
private class BluetoothScoReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) return;
Log.i(TAG, "onReceive");
synchronized (LOCK) {
if (getScoChangeIntent().equals(intent.getAction())) {
int status = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, AudioManager.SCO_AUDIO_STATE_ERROR);
if (status == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
if (bluetoothHeadset != null) {
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
for (BluetoothDevice device : devices) {
if (bluetoothHeadset.isAudioConnected(device)) {
int deviceClass = device.getBluetoothClass().getDeviceClass();
if (deviceClass == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE ||
deviceClass == BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO ||
deviceClass == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET)
{
scoConnection = ScoConnection.CONNECTED;
if (wantsConnection) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
audioManager.setBluetoothScoOn(true);
}
}
}
}
}
}
}
}
handleBluetoothStateChange();
}
}
private class BluetoothConnectionReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive");
handleBluetoothStateChange();
}
}
public interface BluetoothStateListener {
public void onBluetoothStateChanged(boolean isAvailable);
}
}

View File

@@ -0,0 +1,131 @@
package org.thoughtcrime.securesms.webrtc.audio;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Vibrator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.io.IOException;
public class IncomingRinger {
private static final String TAG = IncomingRinger.class.getSimpleName();
private static final long[] VIBRATE_PATTERN = {0, 1000, 1000};
private final Context context;
private final Vibrator vibrator;
private MediaPlayer player;
IncomingRinger(Context context) {
this.context = context.getApplicationContext();
this.vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
}
public void start(@Nullable Uri uri, boolean vibrate) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
if (player != null) player.release();
if (uri != null) player = createPlayer(uri);
int ringerMode = audioManager.getRingerMode();
if (shouldVibrate(context, player, ringerMode, vibrate)) {
Log.i(TAG, "Starting vibration");
vibrator.vibrate(VIBRATE_PATTERN, 1);
}
if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) {
try {
if (!player.isPlaying()) {
player.prepare();
player.start();
Log.i(TAG, "Playing ringtone now...");
} else {
Log.w(TAG, "Ringtone is already playing, declining to restart.");
}
} catch (IllegalStateException | IOException e) {
Log.w(TAG, e);
player = null;
}
} else {
Log.w(TAG, "Not ringing, mode: " + ringerMode);
}
}
public void stop() {
if (player != null) {
Log.i(TAG, "Stopping ringer");
player.release();
player = null;
}
Log.i(TAG, "Cancelling vibrator");
vibrator.cancel();
}
private boolean shouldVibrate(Context context, MediaPlayer player, int ringerMode, boolean vibrate) {
if (player == null) {
return true;
}
return shouldVibrateNew(context, ringerMode, vibrate);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private boolean shouldVibrateNew(Context context, int ringerMode, boolean vibrate) {
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator == null || !vibrator.hasVibrator()) {
return false;
}
if (vibrate) {
return ringerMode != AudioManager.RINGER_MODE_SILENT;
} else {
return ringerMode == AudioManager.RINGER_MODE_VIBRATE;
}
}
private boolean shouldVibrateOld(Context context, boolean vibrate) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
return vibrate && audioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_RINGER);
}
private MediaPlayer createPlayer(@NonNull Uri ringtoneUri) {
try {
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setOnErrorListener(new MediaPlayerErrorListener());
mediaPlayer.setDataSource(context, ringtoneUri);
mediaPlayer.setLooping(true);
mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING);
return mediaPlayer;
} catch (IOException e) {
Log.e(TAG, "Failed to create player for incoming call ringer");
return null;
}
}
private class MediaPlayerErrorListener implements MediaPlayer.OnErrorListener {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Log.w(TAG, "onError(" + mp + ", " + what + ", " + extra);
player = null;
return false;
}
}
}

View File

@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.webrtc.audio;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.R;
import java.io.IOException;
public class OutgoingRinger {
private static final String TAG = OutgoingRinger.class.getSimpleName();
public enum Type {
RINGING,
BUSY
}
private final Context context;
private MediaPlayer mediaPlayer;
public OutgoingRinger(@NonNull Context context) {
this.context = context;
}
public void start(Type type) {
int soundId;
if (type == Type.RINGING) soundId = R.raw.redphone_outring;
else if (type == Type.BUSY) soundId = R.raw.redphone_busy;
else throw new IllegalArgumentException("Not a valid sound type");
if( mediaPlayer != null ) {
mediaPlayer.release();
}
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
mediaPlayer.setLooping(true);
String packageName = context.getPackageName();
Uri dataUri = Uri.parse("android.resource://" + packageName + "/" + soundId);
try {
mediaPlayer.setDataSource(context, dataUri);
mediaPlayer.prepare();
mediaPlayer.start();
} catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) {
Log.w(TAG, e);
}
}
public void stop() {
if (mediaPlayer == null) return;
mediaPlayer.release();
mediaPlayer = null;
}
}

View File

@@ -0,0 +1,102 @@
package org.thoughtcrime.securesms.webrtc.audio;
import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ServiceUtil;
public class SignalAudioManager {
@SuppressWarnings("unused")
private static final String TAG = SignalAudioManager.class.getSimpleName();
private final Context context;
private final IncomingRinger incomingRinger;
private final OutgoingRinger outgoingRinger;
private final SoundPool soundPool;
private final int connectedSoundId;
private final int disconnectedSoundId;
public SignalAudioManager(@NonNull Context context) {
this.context = context.getApplicationContext();
this.incomingRinger = new IncomingRinger(context);
this.outgoingRinger = new OutgoingRinger(context);
this.soundPool = new SoundPool(1, AudioManager.STREAM_VOICE_CALL, 0);
this.connectedSoundId = this.soundPool.load(context, R.raw.webrtc_completed, 1);
this.disconnectedSoundId = this.soundPool.load(context, R.raw.webrtc_disconnected, 1);
}
public void initializeAudioForCall() {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE);
}
public void startIncomingRinger(@Nullable Uri ringtoneUri, boolean vibrate) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
boolean speaker = !audioManager.isWiredHeadsetOn() && !audioManager.isBluetoothScoOn();
audioManager.setMode(AudioManager.MODE_RINGTONE);
audioManager.setMicrophoneMute(false);
audioManager.setSpeakerphoneOn(speaker);
incomingRinger.start(ringtoneUri, vibrate);
}
public void startOutgoingRinger(OutgoingRinger.Type type) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
audioManager.setMicrophoneMute(false);
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
outgoingRinger.start(type);
}
public void silenceIncomingRinger() {
incomingRinger.stop();
}
public void startCommunication(boolean preserveSpeakerphone) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
incomingRinger.stop();
outgoingRinger.stop();
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
if (!preserveSpeakerphone) {
audioManager.setSpeakerphoneOn(false);
}
soundPool.play(connectedSoundId, 1.0f, 1.0f, 0, 0, 1.0f);
}
public void stop(boolean playDisconnected) {
AudioManager audioManager = ServiceUtil.getAudioManager(context);
incomingRinger.stop();
outgoingRinger.stop();
if (playDisconnected) {
soundPool.play(disconnectedSoundId, 1.0f, 1.0f, 0, 0, 1.0f);
}
if (audioManager.isBluetoothScoOn()) {
audioManager.setBluetoothScoOn(false);
audioManager.stopBluetoothSco();
}
audioManager.setSpeakerphoneOn(false);
audioManager.setMicrophoneMute(false);
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.abandonAudioFocus(null);
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thoughtcrime.securesms.webrtc.locks;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.os.Message;
import org.thoughtcrime.securesms.logging.Log;
/**
* This class is used to listen to the accelerometer to monitor the
* orientation of the phone. The client of this class is notified when
* the orientation changes between horizontal and vertical.
*/
public final class AccelerometerListener {
private static final String TAG = "AccelerometerListener";
private static final boolean DEBUG = true;
private static final boolean VDEBUG = false;
private SensorManager mSensorManager;
private Sensor mSensor;
// mOrientation is the orientation value most recently reported to the client.
private int mOrientation;
// mPendingOrientation is the latest orientation computed based on the sensor value.
// This is sent to the client after a rebounce delay, at which point it is copied to
// mOrientation.
private int mPendingOrientation;
private OrientationListener mListener;
// Device orientation
public static final int ORIENTATION_UNKNOWN = 0;
public static final int ORIENTATION_VERTICAL = 1;
public static final int ORIENTATION_HORIZONTAL = 2;
private static final int ORIENTATION_CHANGED = 1234;
private static final int VERTICAL_DEBOUNCE = 100;
private static final int HORIZONTAL_DEBOUNCE = 500;
private static final double VERTICAL_ANGLE = 50.0;
public interface OrientationListener {
public void orientationChanged(int orientation);
}
public AccelerometerListener(Context context, OrientationListener listener) {
mListener = listener;
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
public void enable(boolean enable) {
if (DEBUG) Log.d(TAG, "enable(" + enable + ")");
synchronized (this) {
if (enable) {
mOrientation = ORIENTATION_UNKNOWN;
mPendingOrientation = ORIENTATION_UNKNOWN;
mSensorManager.registerListener(mSensorListener, mSensor,
SensorManager.SENSOR_DELAY_NORMAL);
} else {
mSensorManager.unregisterListener(mSensorListener);
mHandler.removeMessages(ORIENTATION_CHANGED);
}
}
}
private void setOrientation(int orientation) {
synchronized (this) {
if (mPendingOrientation == orientation) {
// Pending orientation has not changed, so do nothing.
return;
}
// Cancel any pending messages.
// We will either start a new timer or cancel alltogether
// if the orientation has not changed.
mHandler.removeMessages(ORIENTATION_CHANGED);
if (mOrientation != orientation) {
// Set timer to send an event if the orientation has changed since its
// previously reported value.
mPendingOrientation = orientation;
Message m = mHandler.obtainMessage(ORIENTATION_CHANGED);
// set delay to our debounce timeout
int delay = (orientation == ORIENTATION_VERTICAL ? VERTICAL_DEBOUNCE
: HORIZONTAL_DEBOUNCE);
mHandler.sendMessageDelayed(m, delay);
} else {
// no message is pending
mPendingOrientation = ORIENTATION_UNKNOWN;
}
}
}
private void onSensorEvent(double x, double y, double z) {
if (VDEBUG) Log.d(TAG, "onSensorEvent(" + x + ", " + y + ", " + z + ")");
// If some values are exactly zero, then likely the sensor is not powered up yet.
// ignore these events to avoid false horizontal positives.
if (x == 0.0 || y == 0.0 || z == 0.0) return;
// magnitude of the acceleration vector projected onto XY plane
double xy = Math.sqrt(x * x + y * y);
// compute the vertical angle
double angle = Math.atan2(xy, z);
// convert to degrees
angle = angle * 180.0 / Math.PI;
int orientation = (angle > VERTICAL_ANGLE ? ORIENTATION_VERTICAL : ORIENTATION_HORIZONTAL);
if (VDEBUG) Log.d(TAG, "angle: " + angle + " orientation: " + orientation);
setOrientation(orientation);
}
SensorEventListener mSensorListener = new SensorEventListener() {
public void onSensorChanged(SensorEvent event) {
onSensorEvent(event.values[0], event.values[1], event.values[2]);
}
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// ignore
}
};
Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case ORIENTATION_CHANGED:
synchronized (this) {
mOrientation = mPendingOrientation;
if (DEBUG) {
Log.d(TAG, "orientation: " +
(mOrientation == ORIENTATION_HORIZONTAL ? "horizontal"
: (mOrientation == ORIENTATION_VERTICAL ? "vertical"
: "unknown")));
}
mListener.orientationChanged(mOrientation);
}
break;
}
}
};
}

View File

@@ -0,0 +1,153 @@
package org.thoughtcrime.securesms.webrtc.locks;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.provider.Settings;
import org.thoughtcrime.securesms.logging.Log;
/**
* Maintains wake lock state.
*
* @author Stuart O. Anderson
*/
public class LockManager {
private static final String TAG = LockManager.class.getSimpleName();
private final PowerManager.WakeLock fullLock;
private final PowerManager.WakeLock partialLock;
private final WifiManager.WifiLock wifiLock;
private final ProximityLock proximityLock;
private final AccelerometerListener accelerometerListener;
private final boolean wifiLockEnforced;
private int orientation = AccelerometerListener.ORIENTATION_UNKNOWN;
private boolean proximityDisabled = false;
public enum PhoneState {
IDLE,
PROCESSING, //used when the phone is active but before the user should be alerted.
INTERACTIVE,
IN_CALL,
IN_HANDS_FREE_CALL,
IN_VIDEO
}
private enum LockState {
FULL,
PARTIAL,
SLEEP,
PROXIMITY
}
public LockManager(Context context) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
fullLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "signal:full");
partialLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "signal:partial");
proximityLock = new ProximityLock(pm);
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "signal:wifi");
fullLock.setReferenceCounted(false);
partialLock.setReferenceCounted(false);
wifiLock.setReferenceCounted(false);
accelerometerListener = new AccelerometerListener(context, new AccelerometerListener.OrientationListener() {
@Override
public void orientationChanged(int newOrientation) {
orientation = newOrientation;
Log.d(TAG, "Orentation Update: " + newOrientation);
updateInCallLockState();
}
});
wifiLockEnforced = isWifiPowerActiveModeEnabled(context);
}
private boolean isWifiPowerActiveModeEnabled(Context context) {
int wifi_pwr_active_mode = Settings.Secure.getInt(context.getContentResolver(), "wifi_pwr_active_mode", -1);
Log.d(TAG, "Wifi Activity Policy: " + wifi_pwr_active_mode);
if (wifi_pwr_active_mode == 0) {
return false;
}
return true;
}
private void updateInCallLockState() {
if (orientation != AccelerometerListener.ORIENTATION_HORIZONTAL && wifiLockEnforced && !proximityDisabled) {
setLockState(LockState.PROXIMITY);
} else {
setLockState(LockState.FULL);
}
}
public void updatePhoneState(PhoneState state) {
switch(state) {
case IDLE:
setLockState(LockState.SLEEP);
accelerometerListener.enable(false);
break;
case PROCESSING:
setLockState(LockState.PARTIAL);
accelerometerListener.enable(false);
break;
case INTERACTIVE:
setLockState(LockState.FULL);
accelerometerListener.enable(false);
break;
case IN_HANDS_FREE_CALL:
setLockState(LockState.PARTIAL);
proximityDisabled = true;
accelerometerListener.enable(false);
break;
case IN_VIDEO:
proximityDisabled = true;
accelerometerListener.enable(false);
updateInCallLockState();
break;
case IN_CALL:
proximityDisabled = false;
accelerometerListener.enable(true);
updateInCallLockState();
break;
}
}
private synchronized void setLockState(LockState newState) {
switch(newState) {
case FULL:
fullLock.acquire();
partialLock.acquire();
wifiLock.acquire();
proximityLock.release();
break;
case PARTIAL:
partialLock.acquire();
wifiLock.acquire();
fullLock.release();
proximityLock.release();
break;
case SLEEP:
fullLock.release();
partialLock.release();
wifiLock.release();
proximityLock.release();
break;
case PROXIMITY:
partialLock.acquire();
proximityLock.acquire();
wifiLock.acquire();
fullLock.release();
break;
default:
throw new IllegalArgumentException("Unhandled Mode: " + newState);
}
Log.d(TAG, "Entered Lock State: " + newState);
}
}

View File

@@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.webrtc.locks;
import android.os.Build;
import android.os.PowerManager;
import org.thoughtcrime.securesms.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Controls access to the proximity lock.
* The proximity lock is not part of the public API.
*
* @author Stuart O. Anderson
*/
class ProximityLock {
private static final String TAG = ProximityLock.class.getSimpleName();
private final Method wakelockParameterizedRelease = getWakelockParamterizedReleaseMethod();
private final Optional<PowerManager.WakeLock> proximityLock;
private static final int PROXIMITY_SCREEN_OFF_WAKE_LOCK = 32;
private static final int WAIT_FOR_PROXIMITY_NEGATIVE = 1;
ProximityLock(PowerManager pm) {
proximityLock = getProximityLock(pm);
}
private Optional<PowerManager.WakeLock> getProximityLock(PowerManager pm) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (pm.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
return Optional.fromNullable(pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "signal:proximity"));
} else {
return Optional.absent();
}
} else {
try {
return Optional.fromNullable(pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, "signal:incall"));
} catch (Throwable t) {
Log.e(TAG, "Failed to create proximity lock", t);
return Optional.absent();
}
}
}
public void acquire() {
if (!proximityLock.isPresent() || proximityLock.get().isHeld()) {
return;
}
proximityLock.get().acquire();
}
public void release() {
if (!proximityLock.isPresent() || !proximityLock.get().isHeld()) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
proximityLock.get().release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
} else {
boolean released = false;
if (wakelockParameterizedRelease != null) {
try {
wakelockParameterizedRelease.invoke(proximityLock.get(), WAIT_FOR_PROXIMITY_NEGATIVE);
released = true;
} catch (IllegalAccessException e) {
Log.w(TAG, e);
} catch (InvocationTargetException e) {
Log.w(TAG, e);
}
}
if (!released) {
proximityLock.get().release();
}
}
Log.d(TAG, "Released proximity lock:" + proximityLock.get().isHeld());
}
private static Method getWakelockParamterizedReleaseMethod() {
try {
return PowerManager.WakeLock.class.getDeclaredMethod("release", Integer.TYPE);
} catch (NoSuchMethodException e) {
Log.d(TAG, "Parameterized WakeLock release not available on this device.");
}
return null;
}
}