Updpated linphone + using native remote provisioning procedure instead of java one + few improvements/fixes
This commit is contained in:
parent
a1d54584ae
commit
7d0405e4f3
9 changed files with 68 additions and 192 deletions
|
@ -8,6 +8,7 @@ register_only_when_network_is_up=1
|
||||||
auto_net_state_mon=0
|
auto_net_state_mon=0
|
||||||
auto_answer_replacing_calls=1
|
auto_answer_replacing_calls=1
|
||||||
media_encryption_mandatory=0
|
media_encryption_mandatory=0
|
||||||
|
root_ca=/data/data/org.linphone/files/rootca.pem
|
||||||
|
|
||||||
[rtp]
|
[rtp]
|
||||||
audio_rtp_port=7076
|
audio_rtp_port=7076
|
||||||
|
|
|
@ -9,6 +9,7 @@ auto_net_state_mon=0
|
||||||
auto_answer_replacing_calls=1
|
auto_answer_replacing_calls=1
|
||||||
media_encryption_mandatory=0
|
media_encryption_mandatory=0
|
||||||
ping_with_options=0
|
ping_with_options=0
|
||||||
|
root_ca=/data/data/org.linphone/files/rootca.pem
|
||||||
|
|
||||||
[rtp]
|
[rtp]
|
||||||
audio_rtp_port=7076
|
audio_rtp_port=7076
|
||||||
|
|
|
@ -341,8 +341,7 @@ public class LinphoneManager implements LinphoneCoreListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized static final LinphoneManager createAndStart(
|
public synchronized static final LinphoneManager createAndStart(Context c, LinphoneServiceListener listener) {
|
||||||
Context c, LinphoneServiceListener listener) {
|
|
||||||
if (instance != null)
|
if (instance != null)
|
||||||
throw new RuntimeException("Linphone Manager is already initialized");
|
throw new RuntimeException("Linphone Manager is already initialized");
|
||||||
|
|
||||||
|
@ -352,13 +351,11 @@ public class LinphoneManager implements LinphoneCoreListener {
|
||||||
boolean gsmIdle = tm.getCallState() == TelephonyManager.CALL_STATE_IDLE;
|
boolean gsmIdle = tm.getCallState() == TelephonyManager.CALL_STATE_IDLE;
|
||||||
setGsmIdle(gsmIdle);
|
setGsmIdle(gsmIdle);
|
||||||
|
|
||||||
getInstance().changeStatusToOnline();
|
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPresenceModelActivitySet() {
|
private boolean isPresenceModelActivitySet() {
|
||||||
return isInstanciated() && getLc().getPresenceModel() != null || getLc().getPresenceModel().getActivity() != null;
|
return getLc().getPresenceModel() != null || getLc().getPresenceModel().getActivity() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeStatusToOnline() {
|
public void changeStatusToOnline() {
|
||||||
|
@ -586,60 +583,49 @@ public class LinphoneManager implements LinphoneCoreListener {
|
||||||
boolean isDebugLogEnabled = !(mR.getBoolean(R.bool.disable_every_log));
|
boolean isDebugLogEnabled = !(mR.getBoolean(R.bool.disable_every_log));
|
||||||
LinphoneCoreFactory.instance().setDebugMode(isDebugLogEnabled, getString(R.string.app_name));
|
LinphoneCoreFactory.instance().setDebugMode(isDebugLogEnabled, getString(R.string.app_name));
|
||||||
|
|
||||||
// Try to get remote provisioning
|
mLc = LinphoneCoreFactory.instance().createLinphoneCore(this, mLinphoneConfigFile, mLinphoneFactoryConfigFile, null, c);
|
||||||
// First check if there is a remote provisioning url in the old preferences API
|
initLiblinphone();
|
||||||
|
|
||||||
String remote_provisioning = mPrefs.getRemoteProvisioningUrl();
|
TimerTask lTask = new TimerTask() {
|
||||||
if(remote_provisioning != null && remote_provisioning.length() > 0 && RemoteProvisioning.isAvailable()) {
|
@Override
|
||||||
RemoteProvisioning.download(remote_provisioning, mLinphoneConfigFile);
|
public void run() {
|
||||||
}
|
mLc.iterate();
|
||||||
|
}
|
||||||
initLiblinphone(c);
|
};
|
||||||
|
/*use schedule instead of scheduleAtFixedRate to avoid iterate from being call in burst after cpu wake up*/
|
||||||
PreferencesMigrator prefMigrator = new PreferencesMigrator(mServiceContext);
|
mTimer = new Timer("Linphone scheduler");
|
||||||
if (prefMigrator.isMigrationNeeded()) {
|
mTimer.schedule(lTask, 0, 20);
|
||||||
prefMigrator.doMigration();
|
|
||||||
}
|
|
||||||
|
|
||||||
int migrationResult = getLc().migrateToMultiTransport();
|
|
||||||
Log.d("Migration to multi transport result = " + migrationResult);
|
|
||||||
|
|
||||||
if (mServiceContext.getResources().getBoolean(R.bool.enable_push_id)) {
|
|
||||||
Compatibility.initPushNotificationService(mServiceContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
IntentFilter lFilter = new IntentFilter(Intent.ACTION_SCREEN_ON);
|
|
||||||
lFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
|
||||||
mServiceContext.registerReceiver(mKeepAliveReceiver, lFilter);
|
|
||||||
|
|
||||||
updateNetworkReachability();
|
|
||||||
|
|
||||||
startBluetooth();
|
|
||||||
resetCameraFromPreferences();
|
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
Log.e(e, "Cannot start linphone");
|
Log.e(e, "Cannot start linphone");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void initLiblinphone(Context c) throws LinphoneCoreException {
|
private synchronized void initLiblinphone() throws LinphoneCoreException {
|
||||||
boolean isDebugLogEnabled = !(mR.getBoolean(R.bool.disable_every_log)) && mPrefs.isDebugEnabled();
|
boolean isDebugLogEnabled = !(mR.getBoolean(R.bool.disable_every_log)) && mPrefs.isDebugEnabled();
|
||||||
LinphoneCoreFactory.instance().setDebugMode(isDebugLogEnabled, getString(R.string.app_name));
|
LinphoneCoreFactory.instance().setDebugMode(isDebugLogEnabled, getString(R.string.app_name));
|
||||||
|
|
||||||
mLc = LinphoneCoreFactory.instance().createLinphoneCore(this, mLinphoneConfigFile, mLinphoneFactoryConfigFile, null, c);
|
PreferencesMigrator prefMigrator = new PreferencesMigrator(mServiceContext);
|
||||||
mLc.setContext(c);
|
prefMigrator.migrateRemoteProvisioningUriIfNeeded();
|
||||||
|
|
||||||
|
if (prefMigrator.isMigrationNeeded()) {
|
||||||
|
prefMigrator.doMigration();
|
||||||
|
}
|
||||||
|
|
||||||
|
mLc.setContext(mServiceContext);
|
||||||
|
mLc.setZrtpSecretsCache(basePath + "/zrtp_secrets");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String versionName = c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionName;
|
String versionName = mServiceContext.getPackageManager().getPackageInfo(mServiceContext.getPackageName(), 0).versionName;
|
||||||
if (versionName == null) {
|
if (versionName == null) {
|
||||||
versionName = String.valueOf(c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionCode);
|
versionName = String.valueOf(mServiceContext.getPackageManager().getPackageInfo(mServiceContext.getPackageName(), 0).versionCode);
|
||||||
}
|
}
|
||||||
mLc.setUserAgent("LinphoneAndroid", versionName);
|
mLc.setUserAgent("LinphoneAndroid", versionName);
|
||||||
} catch (NameNotFoundException e) {
|
} catch (NameNotFoundException e) {
|
||||||
Log.e(e, "cannot get version name");
|
Log.e(e, "cannot get version name");
|
||||||
}
|
}
|
||||||
|
|
||||||
mLc.setZrtpSecretsCache(basePath + "/zrtp_secrets");
|
|
||||||
|
|
||||||
mLc.setRing(null);
|
mLc.setRing(null);
|
||||||
mLc.setRootCA(mLinphoneRootCaFile);
|
mLc.setRootCA(mLinphoneRootCaFile);
|
||||||
mLc.setPlayFile(mPauseSoundFile);
|
mLc.setPlayFile(mPauseSoundFile);
|
||||||
|
@ -649,25 +635,23 @@ public class LinphoneManager implements LinphoneCoreListener {
|
||||||
Log.w("MediaStreamer : " + availableCores + " cores detected and configured");
|
Log.w("MediaStreamer : " + availableCores + " cores detected and configured");
|
||||||
mLc.setCpuCount(availableCores);
|
mLc.setCpuCount(availableCores);
|
||||||
|
|
||||||
int camId = 0;
|
|
||||||
AndroidCamera[] cameras = AndroidCameraConfiguration.retrieveCameras();
|
|
||||||
for (AndroidCamera androidCamera : cameras) {
|
|
||||||
if (androidCamera.frontFacing == mPrefs.useFrontCam())
|
|
||||||
camId = androidCamera.id;
|
|
||||||
}
|
|
||||||
LinphoneManager.getLc().setVideoDevice(camId);
|
|
||||||
|
|
||||||
initTunnelFromConf();
|
initTunnelFromConf();
|
||||||
|
|
||||||
TimerTask lTask = new TimerTask() {
|
int migrationResult = getLc().migrateToMultiTransport();
|
||||||
@Override
|
Log.d("Migration to multi transport result = " + migrationResult);
|
||||||
public void run() {
|
|
||||||
mLc.iterate();
|
if (mServiceContext.getResources().getBoolean(R.bool.enable_push_id)) {
|
||||||
}
|
Compatibility.initPushNotificationService(mServiceContext);
|
||||||
};
|
}
|
||||||
/*use schedule instead of scheduleAtFixedRate to avoid iterate from being call in burst after cpu wake up*/
|
|
||||||
mTimer = new Timer("Linphone scheduler");
|
IntentFilter lFilter = new IntentFilter(Intent.ACTION_SCREEN_ON);
|
||||||
mTimer.schedule(lTask, 0, 20);
|
lFilter.addAction(Intent.ACTION_SCREEN_OFF);
|
||||||
|
mServiceContext.registerReceiver(mKeepAliveReceiver, lFilter);
|
||||||
|
|
||||||
|
updateNetworkReachability();
|
||||||
|
|
||||||
|
startBluetooth();
|
||||||
|
resetCameraFromPreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copyAssetsFromPackage() throws IOException {
|
private void copyAssetsFromPackage() throws IOException {
|
||||||
|
@ -890,20 +874,18 @@ public class LinphoneManager implements LinphoneCoreListener {
|
||||||
public String getLastLcStatusMessage() {
|
public String getLastLcStatusMessage() {
|
||||||
return lastLcStatusMessage;
|
return lastLcStatusMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void displayStatus(final LinphoneCore lc, final String message) {
|
public void displayStatus(final LinphoneCore lc, final String message) {
|
||||||
Log.i(message);
|
Log.i(message);
|
||||||
lastLcStatusMessage=message;
|
lastLcStatusMessage=message;
|
||||||
mListenerDispatcher.onDisplayStatus(message);
|
mListenerDispatcher.onDisplayStatus(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void globalState(final LinphoneCore lc, final LinphoneCore.GlobalState state, final String message) {
|
public void globalState(final LinphoneCore lc, final LinphoneCore.GlobalState state, final String message) {
|
||||||
Log.i("new state [",state,"]");
|
Log.i("new state [",state,"]");
|
||||||
mListenerDispatcher.onGlobalStateChanged(state, message);
|
mListenerDispatcher.onGlobalStateChanged(state, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void registrationState(final LinphoneCore lc, final LinphoneProxyConfig cfg,final LinphoneCore.RegistrationState state,final String message) {
|
public void registrationState(final LinphoneCore lc, final LinphoneProxyConfig cfg,final LinphoneCore.RegistrationState state,final String message) {
|
||||||
Log.i("new state ["+state+"]");
|
Log.i("new state ["+state+"]");
|
||||||
mListenerDispatcher.onRegistrationStateChanged(state, message);
|
mListenerDispatcher.onRegistrationStateChanged(state, message);
|
||||||
|
@ -1465,8 +1447,7 @@ public class LinphoneManager implements LinphoneCoreListener {
|
||||||
@Override
|
@Override
|
||||||
public void publishStateChanged(LinphoneCore lc, LinphoneEvent ev,
|
public void publishStateChanged(LinphoneCore lc, LinphoneEvent ev,
|
||||||
PublishState state) {
|
PublishState state) {
|
||||||
// TODO Auto-generated method stub
|
Log.d("Publish state changed to " + state + " for event name " + ev.getEventName());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private LinphoneOnComposingReceivedListener composingReceivedListener;
|
private LinphoneOnComposingReceivedListener composingReceivedListener;
|
||||||
|
@ -1482,6 +1463,6 @@ public class LinphoneManager implements LinphoneCoreListener {
|
||||||
@Override
|
@Override
|
||||||
public void configuringStatus(LinphoneCore lc,
|
public void configuringStatus(LinphoneCore lc,
|
||||||
RemoteProvisioningState state, String message) {
|
RemoteProvisioningState state, String message) {
|
||||||
Log.d("Remote provisioning status = " + state.toString());
|
Log.d("Remote provisioning status = " + state.toString() + " (" + message + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -479,6 +479,9 @@ public class LinphonePreferences {
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getAccountCount() {
|
public int getAccountCount() {
|
||||||
|
if (getLc() == null || getLc().getProxyConfigList() == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
return getLc().getProxyConfigList().length;
|
return getLc().getProxyConfigList().length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -776,11 +779,13 @@ public class LinphonePreferences {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRemoteProvisioningUrl(String url) {
|
public void setRemoteProvisioningUrl(String url) {
|
||||||
getConfig().setString("app", "remote_provisioning", url);
|
if (url != null && url.length() == 0)
|
||||||
|
url = null;
|
||||||
|
getConfig().setString("misc", "config-uri", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRemoteProvisioningUrl() {
|
public String getRemoteProvisioningUrl() {
|
||||||
return getConfig().getString("app", "remote_provisioning", null);
|
return getConfig().getString("misc", "config-uri", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDefaultDisplayName(String displayName) {
|
public void setDefaultDisplayName(String displayName) {
|
||||||
|
|
|
@ -85,7 +85,7 @@ public final class LinphoneService extends Service implements LinphoneServiceLis
|
||||||
private WifiManager mWifiManager ;
|
private WifiManager mWifiManager ;
|
||||||
private WifiLock mWifiLock ;
|
private WifiLock mWifiLock ;
|
||||||
public static boolean isReady() {
|
public static boolean isReady() {
|
||||||
return instance!=null && instance.mTestDelayElapsed;
|
return instance != null && instance.mTestDelayElapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -44,8 +44,7 @@ public class PreferencesMigrator {
|
||||||
|
|
||||||
public boolean isMigrationNeeded() {
|
public boolean isMigrationNeeded() {
|
||||||
int accountNumber = mOldPrefs.getInt(getString(R.string.pref_extra_accounts), -1);
|
int accountNumber = mOldPrefs.getInt(getString(R.string.pref_extra_accounts), -1);
|
||||||
boolean migrationNeeded = accountNumber != -1;
|
return accountNumber != -1;
|
||||||
return migrationNeeded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void doMigration() {
|
public void doMigration() {
|
||||||
|
@ -68,6 +67,16 @@ public class PreferencesMigrator {
|
||||||
deleteAllOldPreferences();
|
deleteAllOldPreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void migrateRemoteProvisioningUriIfNeeded() {
|
||||||
|
String oldUri = mNewPrefs.getConfig().getString("app", "remote_provisioning", null);
|
||||||
|
String currentUri = mNewPrefs.getRemoteProvisioningUrl();
|
||||||
|
if (oldUri != null && oldUri.length() > 0 && currentUri == null) {
|
||||||
|
mNewPrefs.setRemoteProvisioningUrl(oldUri);
|
||||||
|
mNewPrefs.getConfig().setString("app", "remote_provisioning", null);
|
||||||
|
mNewPrefs.getConfig().sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void doAccountsMigration() {
|
private void doAccountsMigration() {
|
||||||
LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
|
LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
|
||||||
lc.clearAuthInfos();
|
lc.clearAuthInfos();
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
package org.linphone;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLConnection;
|
|
||||||
|
|
||||||
import org.linphone.core.LinphoneCoreFactory;
|
|
||||||
import org.linphone.core.LpConfig;
|
|
||||||
import org.linphone.mediastream.Log;
|
|
||||||
import org.linphone.tools.Xml2Lpc;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public class RemoteProvisioning {
|
|
||||||
static private class RemoteProvisioningThread extends Thread {
|
|
||||||
String mRPAddress;
|
|
||||||
String mSchema;
|
|
||||||
String mLocalLP;
|
|
||||||
boolean value;
|
|
||||||
|
|
||||||
public RemoteProvisioningThread(final String RPAddress, final String LocalLP, final String schema) {
|
|
||||||
this.mRPAddress = RPAddress;
|
|
||||||
this.mLocalLP = LocalLP;
|
|
||||||
this.mSchema = schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
value = false;
|
|
||||||
Log.i("Download remote provisioning file from " + mRPAddress);
|
|
||||||
URL url = new URL(mRPAddress);
|
|
||||||
URLConnection ucon = url.openConnection();
|
|
||||||
InputStream is = ucon.getInputStream();
|
|
||||||
BufferedInputStream bis = new BufferedInputStream(is);
|
|
||||||
byte[] contents = new byte[1024];
|
|
||||||
|
|
||||||
int bytesRead = 0;
|
|
||||||
ByteArrayOutputStream fileContents = new ByteArrayOutputStream();
|
|
||||||
while( (bytesRead = bis.read(contents)) != -1) {
|
|
||||||
fileContents.write(contents, 0, bytesRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
String strFileContents = fileContents.toString();
|
|
||||||
Log.i("Remote provisioning download successful");
|
|
||||||
|
|
||||||
// Initialize converter
|
|
||||||
LpConfig lp = LinphoneCoreFactory.instance().createLpConfig(mLocalLP);
|
|
||||||
Xml2Lpc x2l = new Xml2Lpc();
|
|
||||||
if(x2l.setXmlString(strFileContents) != 0) {
|
|
||||||
Log.e("Error during remote provisioning file parsing");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if needed
|
|
||||||
if(mSchema != null) {
|
|
||||||
if(x2l.setXsdFile(mSchema) != 0) {
|
|
||||||
Log.e("Error during schema file parsing");
|
|
||||||
}
|
|
||||||
if(x2l.validate() != 0) {
|
|
||||||
Log.e("Can't validate the schema of remote provisioning file");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert
|
|
||||||
if(x2l.convert(lp) != 0) {
|
|
||||||
Log.e("Can't convert remote provisioning file to LinphoneConfig");
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
lp.sync();
|
|
||||||
}
|
|
||||||
value = true;
|
|
||||||
Log.i("Remote provisioning ok");
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
Log.e("Invalid remote provisioning url: " + e.getLocalizedMessage());
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(e);
|
|
||||||
} finally {
|
|
||||||
synchronized(this) {
|
|
||||||
this.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static boolean download(String address, String lpfile, boolean check) {
|
|
||||||
try {
|
|
||||||
String schema = null;
|
|
||||||
if(check) {
|
|
||||||
schema = LinphoneManager.getInstance().getLPConfigXsdPath();
|
|
||||||
}
|
|
||||||
RemoteProvisioningThread thread = new RemoteProvisioningThread(address, lpfile, schema);
|
|
||||||
synchronized(thread) {
|
|
||||||
thread.start();
|
|
||||||
thread.wait();
|
|
||||||
}
|
|
||||||
return thread.value;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Log.e(e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean download(String address, String lpfile) {
|
|
||||||
return download(address, lpfile, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isAvailable() {
|
|
||||||
if(Xml2Lpc.isAvailable()) {
|
|
||||||
Log.i("RemoteProvisioning is available");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
Log.i("RemoteProvisioning is NOT available");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -42,6 +42,7 @@ public class RemoteProvisioningFragment extends Fragment implements OnClickListe
|
||||||
// Restart Linphone
|
// Restart Linphone
|
||||||
Intent intent = new Intent();
|
Intent intent = new Intent();
|
||||||
intent.setClass(getActivity(), LinphoneLauncherActivity.class);
|
intent.setClass(getActivity(), LinphoneLauncherActivity.class);
|
||||||
|
getActivity().finish();
|
||||||
LinphoneActivity.instance().exit();
|
LinphoneActivity.instance().exit();
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit e9b444c3c61b543eb5c69272486d362e00f91345
|
Subproject commit ce7404581abd927361fcd6b73b40255a3081f904
|
Loading…
Reference in a new issue