isolated permission request
This commit is contained in:
parent
8585466c96
commit
8c7b897cd8
|
@ -57,8 +57,6 @@ import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.support.design.widget.Snackbar;
|
|
||||||
import android.support.v4.app.ActivityCompat;
|
|
||||||
import android.support.v4.content.LocalBroadcastManager;
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
import android.support.v4.widget.DrawerLayout;
|
import android.support.v4.widget.DrawerLayout;
|
||||||
import android.support.v7.app.ActionBarDrawerToggle;
|
import android.support.v7.app.ActionBarDrawerToggle;
|
||||||
|
@ -92,7 +90,6 @@ import com.google.zxing.integration.android.IntentResult;
|
||||||
|
|
||||||
public class OrbotMainActivity extends AppCompatActivity
|
public class OrbotMainActivity extends AppCompatActivity
|
||||||
implements OrbotConstants, OnLongClickListener, OnTouchListener {
|
implements OrbotConstants, OnLongClickListener, OnTouchListener {
|
||||||
private GrantedPermissionsAction postPermissionsAction = null;
|
|
||||||
|
|
||||||
/* Useful UI bits */
|
/* Useful UI bits */
|
||||||
private TextView lblStatus = null; //the main text display widget
|
private TextView lblStatus = null; //the main text display widget
|
||||||
|
@ -134,8 +131,6 @@ public class OrbotMainActivity extends AppCompatActivity
|
||||||
public final static String INTENT_ACTION_REQUEST_HIDDEN_SERVICE = "org.torproject.android.REQUEST_HS_PORT";
|
public final static String INTENT_ACTION_REQUEST_HIDDEN_SERVICE = "org.torproject.android.REQUEST_HS_PORT";
|
||||||
public final static String INTENT_ACTION_REQUEST_START_TOR = "org.torproject.android.START_TOR";
|
public final static String INTENT_ACTION_REQUEST_START_TOR = "org.torproject.android.START_TOR";
|
||||||
|
|
||||||
public final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
|
|
||||||
|
|
||||||
// for bridge loading from the assets default bridges.txt file
|
// for bridge loading from the assets default bridges.txt file
|
||||||
class Bridge
|
class Bridge
|
||||||
{
|
{
|
||||||
|
@ -525,18 +520,7 @@ public class OrbotMainActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
else if (item.getItemId() == R.id.menu_hidden_services)
|
else if (item.getItemId() == R.id.menu_hidden_services)
|
||||||
{
|
{
|
||||||
if(usesRuntimePermissions() && !hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)){
|
startActivity(new Intent(this, HiddenServicesActivity.class));
|
||||||
postPermissionsAction = new GrantedPermissionsAction() {
|
|
||||||
@Override
|
|
||||||
public void run(Context context, boolean granted) {
|
|
||||||
startActivity(new Intent(context, HiddenServicesActivity.class));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkPermissions();
|
|
||||||
} else {
|
|
||||||
startActivity(new Intent(this, HiddenServicesActivity.class));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
|
@ -671,19 +655,23 @@ public class OrbotMainActivity extends AppCompatActivity
|
||||||
requestTorRereadConfig();
|
requestTorRereadConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TODO
|
||||||
if(doBackup)
|
if(doBackup)
|
||||||
{
|
{
|
||||||
backupPath = hsutils.createOnionBackup(hsPort);
|
backupPath = hsutils.createOnionBackup(hsPort);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
onion.close();
|
onion.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent nResult = new Intent();
|
Intent nResult = new Intent();
|
||||||
nResult.putExtra("hs_host", hostname);
|
nResult.putExtra("hs_host", hostname);
|
||||||
|
/* TODO
|
||||||
if(doBackup && backupPath != null) {
|
if(doBackup && backupPath != null) {
|
||||||
nResult.putExtra("hs_backup_path", backupPath);
|
nResult.putExtra("hs_backup_path", backupPath);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
setResult(RESULT_OK, nResult);
|
setResult(RESULT_OK, nResult);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
@ -712,120 +700,90 @@ public class OrbotMainActivity extends AppCompatActivity
|
||||||
|
|
||||||
if (action == null)
|
if (action == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (action.equals(INTENT_ACTION_REQUEST_HIDDEN_SERVICE))
|
|
||||||
{
|
|
||||||
final int hiddenServicePort = intent.getIntExtra("hs_port", -1);
|
|
||||||
final int hiddenServiceRemotePort = intent.getIntExtra("hs_onion_port", -1);
|
|
||||||
final String hiddenServiceName = intent.getStringExtra("hs_name");
|
|
||||||
final Boolean createBackup = intent.getBooleanExtra("hs_backup",false);
|
|
||||||
final String keyZipPath = intent.getStringExtra("hs_key_zip_path");
|
|
||||||
|
|
||||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
switch (action) {
|
||||||
|
case INTENT_ACTION_REQUEST_HIDDEN_SERVICE:
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
final int hiddenServicePort = intent.getIntExtra("hs_port", -1);
|
||||||
switch (which){
|
final int hiddenServiceRemotePort = intent.getIntExtra("hs_onion_port", -1);
|
||||||
case DialogInterface.BUTTON_POSITIVE:
|
final String hiddenServiceName = intent.getStringExtra("hs_name");
|
||||||
if(createBackup && usesRuntimePermissions()
|
final Boolean createBackup = intent.getBooleanExtra("hs_backup", false);
|
||||||
&& !hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)){
|
final String keyZipPath = intent.getStringExtra("hs_key_zip_path");
|
||||||
postPermissionsAction = new GrantedPermissionsAction() {
|
|
||||||
@Override
|
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||||
public void run(Context context, boolean granted) {
|
|
||||||
try {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
enableHiddenServicePort (
|
switch (which) {
|
||||||
hiddenServiceName, hiddenServicePort,
|
case DialogInterface.BUTTON_POSITIVE:
|
||||||
hiddenServiceRemotePort, createBackup, keyZipPath
|
try {
|
||||||
);
|
enableHiddenServicePort(
|
||||||
} catch (RemoteException e) {
|
hiddenServiceName, hiddenServicePort,
|
||||||
// TODO Auto-generated catch block
|
hiddenServiceRemotePort, createBackup, keyZipPath
|
||||||
e.printStackTrace();
|
);
|
||||||
} catch (InterruptedException e) {
|
} catch (RemoteException e) {
|
||||||
// TODO Auto-generated catch block
|
// TODO Auto-generated catch block
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
} catch (InterruptedException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
checkPermissions();
|
break;
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
enableHiddenServicePort (
|
|
||||||
hiddenServiceName, hiddenServicePort,
|
|
||||||
hiddenServiceRemotePort, createBackup, keyZipPath
|
|
||||||
);
|
|
||||||
} catch (RemoteException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
};
|
||||||
|
|
||||||
case DialogInterface.BUTTON_NEGATIVE:
|
String requestMsg = getString(R.string.hidden_service_request, hiddenServicePort);
|
||||||
//No button clicked
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
finish();
|
builder.setMessage(requestMsg).setPositiveButton("Allow", dialogClickListener)
|
||||||
break;
|
.setNegativeButton("Deny", dialogClickListener).show();
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
String requestMsg = getString(R.string.hidden_service_request, hiddenServicePort);
|
return; //don't null the setIntent() as we need it later
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
|
||||||
builder.setMessage(requestMsg).setPositiveButton("Allow", dialogClickListener)
|
|
||||||
.setNegativeButton("Deny", dialogClickListener).show();
|
|
||||||
|
|
||||||
return; //don't null the setIntent() as we need it later
|
|
||||||
}
|
|
||||||
else if (action.equals(INTENT_ACTION_REQUEST_START_TOR))
|
|
||||||
{
|
|
||||||
autoStartFromIntent = true;
|
|
||||||
|
|
||||||
startTor();
|
|
||||||
|
|
||||||
//never allow backgrounds start from this type of intent start
|
case INTENT_ACTION_REQUEST_START_TOR:
|
||||||
//app devs who want background starts, can use the service intents
|
autoStartFromIntent = true;
|
||||||
/**
|
|
||||||
if (Prefs.allowBackgroundStarts())
|
|
||||||
{
|
|
||||||
Intent resultIntent;
|
|
||||||
if (lastStatusIntent == null) {
|
|
||||||
resultIntent = new Intent(intent);
|
|
||||||
} else {
|
|
||||||
resultIntent = lastStatusIntent;
|
|
||||||
}
|
|
||||||
resultIntent.putExtra(TorServiceConstants.EXTRA_STATUS, torStatus);
|
|
||||||
setResult(RESULT_OK, resultIntent);
|
|
||||||
finish();
|
|
||||||
}*/
|
|
||||||
|
|
||||||
}
|
|
||||||
else if (action.equals(Intent.ACTION_VIEW))
|
|
||||||
{
|
|
||||||
String urlString = intent.getDataString();
|
|
||||||
|
|
||||||
if (urlString != null)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (urlString.toLowerCase().startsWith("bridge://"))
|
|
||||||
|
|
||||||
{
|
startTor();
|
||||||
String newBridgeValue = urlString.substring(9); //remove the bridge protocol piece
|
|
||||||
newBridgeValue = URLDecoder.decode(newBridgeValue); //decode the value here
|
|
||||||
|
|
||||||
showAlert(getString(R.string.bridges_updated),getString(R.string.restart_orbot_to_use_this_bridge_) + newBridgeValue,false);
|
//never allow backgrounds start from this type of intent start
|
||||||
|
//app devs who want background starts, can use the service intents
|
||||||
setNewBridges(newBridgeValue);
|
/**
|
||||||
|
if (Prefs.allowBackgroundStarts())
|
||||||
|
{
|
||||||
|
Intent resultIntent;
|
||||||
|
if (lastStatusIntent == null) {
|
||||||
|
resultIntent = new Intent(intent);
|
||||||
|
} else {
|
||||||
|
resultIntent = lastStatusIntent;
|
||||||
|
}
|
||||||
|
resultIntent.putExtra(TorServiceConstants.EXTRA_STATUS, torStatus);
|
||||||
|
setResult(RESULT_OK, resultIntent);
|
||||||
|
finish();
|
||||||
|
}*/
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Intent.ACTION_VIEW:
|
||||||
|
String urlString = intent.getDataString();
|
||||||
|
|
||||||
|
if (urlString != null) {
|
||||||
|
|
||||||
|
if (urlString.toLowerCase().startsWith("bridge://"))
|
||||||
|
|
||||||
|
{
|
||||||
|
String newBridgeValue = urlString.substring(9); //remove the bridge protocol piece
|
||||||
|
newBridgeValue = URLDecoder.decode(newBridgeValue); //decode the value here
|
||||||
|
|
||||||
|
showAlert(getString(R.string.bridges_updated), getString(R.string.restart_orbot_to_use_this_bridge_) + newBridgeValue, false);
|
||||||
|
|
||||||
|
setNewBridges(newBridgeValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(null);
|
updateStatus(null);
|
||||||
|
|
||||||
setIntent(null);
|
setIntent(null);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setNewBridges (String newBridgeValue)
|
private void setNewBridges (String newBridgeValue)
|
||||||
|
@ -904,10 +862,6 @@ public class OrbotMainActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void startIntent (String pkg, String action, Uri data)
|
private void startIntent (String pkg, String action, Uri data)
|
||||||
{
|
{
|
||||||
|
@ -1598,52 +1552,4 @@ public class OrbotMainActivity extends AppCompatActivity
|
||||||
|
|
||||||
setNewBridges(sbConfig.toString());
|
setNewBridges(sbConfig.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean usesRuntimePermissions() {
|
|
||||||
return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
private boolean hasPermission(String permission) {
|
|
||||||
return (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkPermissions() {
|
|
||||||
if (ActivityCompat.shouldShowRequestPermissionRationale
|
|
||||||
(OrbotMainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
|
||||||
Snackbar.make(findViewById(android.R.id.content),
|
|
||||||
R.string.please_grant_permissions_for_external_storage,
|
|
||||||
Snackbar.LENGTH_INDEFINITE).setAction("ENABLE",
|
|
||||||
new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
ActivityCompat.requestPermissions(OrbotMainActivity.this,
|
|
||||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
|
||||||
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
|
|
||||||
}
|
|
||||||
}).show();
|
|
||||||
} else {
|
|
||||||
ActivityCompat.requestPermissions(OrbotMainActivity.this,
|
|
||||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
|
||||||
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode,
|
|
||||||
String permissions[], int[] grantResults) {
|
|
||||||
switch (requestCode) {
|
|
||||||
case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: {
|
|
||||||
// If request is cancelled, the result arrays are empty.
|
|
||||||
boolean granted = (grantResults.length > 0
|
|
||||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
|
||||||
|
|
||||||
postPermissionsAction.run(this, granted);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
package org.torproject.android.ui.hs;
|
package org.torproject.android.ui.hs;
|
||||||
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.design.widget.FloatingActionButton;
|
import android.support.design.widget.FloatingActionButton;
|
||||||
|
@ -79,10 +75,6 @@ public class HiddenServicesActivity extends AppCompatActivity {
|
||||||
Bundle arguments = new Bundle();
|
Bundle arguments = new Bundle();
|
||||||
arguments.putString("port", port.getText().toString());
|
arguments.putString("port", port.getText().toString());
|
||||||
arguments.putString("onion", onion.getText().toString());
|
arguments.putString("onion", onion.getText().toString());
|
||||||
boolean has_write_permission = true;
|
|
||||||
if (usesRuntimePermissions())
|
|
||||||
has_write_permission = hasPermission();
|
|
||||||
arguments.putBoolean("has_write_permissions", has_write_permission);
|
|
||||||
|
|
||||||
HSActionsDialog dialog = new HSActionsDialog();
|
HSActionsDialog dialog = new HSActionsDialog();
|
||||||
dialog.setArguments(arguments);
|
dialog.setArguments(arguments);
|
||||||
|
@ -91,15 +83,6 @@ public class HiddenServicesActivity extends AppCompatActivity {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean usesRuntimePermissions() {
|
|
||||||
return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
private boolean hasPermission() {
|
|
||||||
return (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
class HSObserver extends ContentObserver {
|
class HSObserver extends ContentObserver {
|
||||||
HSObserver(Handler handler) {
|
HSObserver(Handler handler) {
|
||||||
super(handler);
|
super(handler);
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
package org.torproject.android.ui.hs.dialogs;
|
package org.torproject.android.ui.hs.dialogs;
|
||||||
|
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.app.DialogFragment;
|
import android.support.v4.app.DialogFragment;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -20,6 +26,7 @@ import org.torproject.android.hsutils.HiddenServiceUtils;
|
||||||
import org.torproject.android.ui.hs.providers.HSContentProvider;
|
import org.torproject.android.ui.hs.providers.HSContentProvider;
|
||||||
|
|
||||||
public class HSActionsDialog extends DialogFragment {
|
public class HSActionsDialog extends DialogFragment {
|
||||||
|
public final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
|
@ -37,8 +44,8 @@ public class HSActionsDialog extends DialogFragment {
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
Context mContext = v.getContext();
|
Context mContext = v.getContext();
|
||||||
|
|
||||||
if (!arguments.getBoolean("has_write_permissions")) {
|
if (usesRuntimePermissions() && !hasExternalWritePermission(mContext)) {
|
||||||
Toast.makeText(mContext, R.string.please_grant_permissions_for_external_storage, Toast.LENGTH_LONG).show();
|
requestPermissions();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,4 +104,34 @@ public class HSActionsDialog extends DialogFragment {
|
||||||
|
|
||||||
return actionDialog;
|
return actionDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean usesRuntimePermissions() {
|
||||||
|
return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
private boolean hasExternalWritePermission(Context context) {
|
||||||
|
return (context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestPermissions() {
|
||||||
|
if (ActivityCompat.shouldShowRequestPermissionRationale
|
||||||
|
(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
|
Snackbar.make(getActivity().findViewById(android.R.id.content),
|
||||||
|
R.string.please_grant_permissions_for_external_storage,
|
||||||
|
Snackbar.LENGTH_INDEFINITE).setAction("ENABLE",
|
||||||
|
new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
ActivityCompat.requestPermissions(getActivity(),
|
||||||
|
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||||
|
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
|
||||||
|
}
|
||||||
|
}).show();
|
||||||
|
} else {
|
||||||
|
ActivityCompat.requestPermissions(getActivity(),
|
||||||
|
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||||
|
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue