Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5f9cf85c9 | ||
|
|
549f09573f | ||
|
|
94e5bc86fb | ||
|
|
370ac2bd89 | ||
|
|
7041f41981 | ||
|
|
dd6a21257b | ||
|
|
445da0c4ea | ||
|
|
92980824b1 | ||
|
|
7d42b07a32 | ||
|
|
e502b9169f | ||
|
|
9f2d887ca0 | ||
|
|
e6aacc5f08 | ||
|
|
ff935be54a | ||
|
|
4e76162346 | ||
|
|
5fa4f2647b | ||
|
|
81b5889a26 | ||
|
|
514f59258a | ||
|
|
9f79393aa5 | ||
|
|
e49d514236 | ||
|
|
4e1462326c |
@@ -14,8 +14,8 @@ android {
|
|||||||
applicationId "com.termux"
|
applicationId "com.termux"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 70
|
versionCode 74
|
||||||
versionName "0.70"
|
versionName "0.74"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -35,3 +35,9 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task versionName {
|
||||||
|
doLast {
|
||||||
|
print android.defaultConfig.versionName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:extractNativeLibs="true"
|
android:extractNativeLibs="true"
|
||||||
@@ -38,12 +39,10 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
|
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|||||||
@@ -93,6 +93,13 @@ public final class BackgroundJob {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addToEnvIfPresent(List<String> environment, String name) {
|
||||||
|
String value = System.getenv(name);
|
||||||
|
if (value != null) {
|
||||||
|
environment.add(name + "=" + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static String[] buildEnvironment(boolean failSafe, String cwd) {
|
static String[] buildEnvironment(boolean failSafe, String cwd) {
|
||||||
new File(TermuxService.HOME_PATH).mkdirs();
|
new File(TermuxService.HOME_PATH).mkdirs();
|
||||||
|
|
||||||
@@ -103,16 +110,15 @@ public final class BackgroundJob {
|
|||||||
environment.add("TERM=xterm-256color");
|
environment.add("TERM=xterm-256color");
|
||||||
environment.add("HOME=" + TermuxService.HOME_PATH);
|
environment.add("HOME=" + TermuxService.HOME_PATH);
|
||||||
environment.add("PREFIX=" + TermuxService.PREFIX_PATH);
|
environment.add("PREFIX=" + TermuxService.PREFIX_PATH);
|
||||||
|
environment.add("BOOTCLASSPATH" + System.getenv("BOOTCLASSPATH"));
|
||||||
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
|
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
|
||||||
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
||||||
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
||||||
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
||||||
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
||||||
String androidRuntimeRoot = System.getenv("ANDROID_RUNTIME_ROOT");
|
// ANDROID_RUNTIME_ROOT and ANDROID_TZDATA_ROOT are required for `am` to run on Android Q
|
||||||
// ANDROID_RUNTIME_ROOT is required for `am` to run on Android Q
|
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
|
||||||
if (androidRuntimeRoot != null) {
|
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
|
||||||
environment.add("ANDROID_RUNTIME_ROOT=" + androidRuntimeRoot);
|
|
||||||
}
|
|
||||||
if (failSafe) {
|
if (failSafe) {
|
||||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||||
environment.add("PATH= " + System.getenv("PATH"));
|
environment.add("PATH= " + System.getenv("PATH"));
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public class BellUtil {
|
|||||||
private static BellUtil instance = null;
|
private static BellUtil instance = null;
|
||||||
private static final Object lock = new Object();
|
private static final Object lock = new Object();
|
||||||
|
|
||||||
public static BellUtil with(Context context) {
|
public static BellUtil getInstance(Context context) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
|
|||||||
@@ -73,6 +73,18 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||||
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
||||||
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
||||||
|
put("F1", KeyEvent.KEYCODE_F1);
|
||||||
|
put("F2", KeyEvent.KEYCODE_F2);
|
||||||
|
put("F3", KeyEvent.KEYCODE_F3);
|
||||||
|
put("F4", KeyEvent.KEYCODE_F4);
|
||||||
|
put("F5", KeyEvent.KEYCODE_F5);
|
||||||
|
put("F6", KeyEvent.KEYCODE_F6);
|
||||||
|
put("F7", KeyEvent.KEYCODE_F7);
|
||||||
|
put("F8", KeyEvent.KEYCODE_F8);
|
||||||
|
put("F9", KeyEvent.KEYCODE_F9);
|
||||||
|
put("F10", KeyEvent.KEYCODE_F10);
|
||||||
|
put("F11", KeyEvent.KEYCODE_F11);
|
||||||
|
put("F12", KeyEvent.KEYCODE_F12);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
static void sendKey(View view, String keyName) {
|
static void sendKey(View view, String keyName) {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.net.Uri;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.Vibrator;
|
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@@ -402,7 +401,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||||
break;
|
break;
|
||||||
case TermuxPreferences.BELL_VIBRATE:
|
case TermuxPreferences.BELL_VIBRATE:
|
||||||
BellUtil.with(TermuxActivity.this).doBell();
|
BellUtil.getInstance(TermuxActivity.this).doBell();
|
||||||
break;
|
break;
|
||||||
case TermuxPreferences.BELL_IGNORE:
|
case TermuxPreferences.BELL_IGNORE:
|
||||||
// Ignore the bell character.
|
// Ignore the bell character.
|
||||||
@@ -482,7 +481,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (bundle != null) {
|
if (bundle != null) {
|
||||||
launchFailsafe = bundle.getBoolean(TERMUX_FAILSAFE_SESSION_ACTION, false);
|
launchFailsafe = bundle.getBoolean(TERMUX_FAILSAFE_SESSION_ACTION, false);
|
||||||
}
|
}
|
||||||
clearTemporaryDirectory();
|
|
||||||
addNewSession(launchFailsafe, null);
|
addNewSession(launchFailsafe, null);
|
||||||
} catch (WindowManager.BadTokenException e) {
|
} catch (WindowManager.BadTokenException e) {
|
||||||
// Activity finished - ignore.
|
// Activity finished - ignore.
|
||||||
@@ -496,7 +494,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
Intent i = getIntent();
|
Intent i = getIntent();
|
||||||
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
|
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
|
||||||
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
|
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
|
||||||
clearTemporaryDirectory();
|
|
||||||
boolean failSafe = i.getBooleanExtra(TERMUX_FAILSAFE_SESSION_ACTION, false);
|
boolean failSafe = i.getBooleanExtra(TERMUX_FAILSAFE_SESSION_ACTION, false);
|
||||||
addNewSession(failSafe, null);
|
addNewSession(failSafe, null);
|
||||||
} else {
|
} else {
|
||||||
@@ -592,8 +589,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
|
new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
|
||||||
.setPositiveButton(android.R.string.ok, null).show();
|
.setPositiveButton(android.R.string.ok, null).show();
|
||||||
} else {
|
} else {
|
||||||
String executablePath = (failSafe ? "/system/bin/sh" : null);
|
TerminalSession newSession = mTermService.createTermSession(null, null, null, failSafe);
|
||||||
TerminalSession newSession = mTermService.createTermSession(executablePath, null, null, failSafe);
|
|
||||||
if (sessionName != null) {
|
if (sessionName != null) {
|
||||||
newSession.mSessionName = sessionName;
|
newSession.mSessionName = sessionName;
|
||||||
}
|
}
|
||||||
@@ -725,7 +721,18 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (session != null) {
|
if (session != null) {
|
||||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
intent.setType("text/plain");
|
intent.setType("text/plain");
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
|
String transcriptText = session.getEmulator().getScreen().getTranscriptTextWithoutJoinedLines().trim();
|
||||||
|
// See https://github.com/termux/termux-app/issues/1166.
|
||||||
|
final int MAX_LENGTH = 100_000;
|
||||||
|
if (transcriptText.length() > MAX_LENGTH) {
|
||||||
|
int cutOffIndex = transcriptText.length() - MAX_LENGTH;
|
||||||
|
int nextNewlineIndex = transcriptText.indexOf('\n', cutOffIndex);
|
||||||
|
if (nextNewlineIndex != -1 && nextNewlineIndex != transcriptText.length() - 1) {
|
||||||
|
cutOffIndex = nextNewlineIndex + 1;
|
||||||
|
}
|
||||||
|
transcriptText = transcriptText.substring(cutOffIndex).trim();
|
||||||
|
}
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||||
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
|
||||||
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
|
||||||
}
|
}
|
||||||
@@ -836,18 +843,4 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearTemporaryDirectory() {
|
|
||||||
if (mTermService.getSessions().size() == 0 && !mTermService.isWakelockEnabled()) {
|
|
||||||
File termuxTmpDir = new File(TermuxService.PREFIX_PATH + "/tmp");
|
|
||||||
if (termuxTmpDir.exists()) {
|
|
||||||
try {
|
|
||||||
TermuxInstaller.deleteFolder(termuxTmpDir);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
termuxTmpDir.mkdirs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import android.os.Build;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
|
|
||||||
@@ -112,6 +113,17 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
|
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
|
||||||
mWifiLock.acquire();
|
mWifiLock.acquire();
|
||||||
|
|
||||||
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
String packageName = getPackageName();
|
||||||
|
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||||
|
Intent whitelist = new Intent();
|
||||||
|
whitelist.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||||
|
whitelist.setData(Uri.parse("package:" + packageName));
|
||||||
|
whitelist.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(whitelist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateNotification();
|
updateNotification();
|
||||||
}
|
}
|
||||||
} else if (ACTION_UNLOCK_WAKE.equals(action)) {
|
} else if (ACTION_UNLOCK_WAKE.equals(action)) {
|
||||||
@@ -136,7 +148,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
mBackgroundTasks.add(task);
|
mBackgroundTasks.add(task);
|
||||||
updateNotification();
|
updateNotification();
|
||||||
} else {
|
} else {
|
||||||
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
|
boolean failsafe = intent.getBooleanExtra(TermuxActivity.TERMUX_FAILSAFE_SESSION_ACTION, false);
|
||||||
|
TerminalSession newSession = createTermSession(executablePath, arguments, cwd, failsafe);
|
||||||
|
|
||||||
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
// Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh".
|
||||||
if (executablePath != null) {
|
if (executablePath != null) {
|
||||||
@@ -237,6 +250,18 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
|
File termuxTmpDir = new File(TermuxService.PREFIX_PATH + "/tmp");
|
||||||
|
|
||||||
|
if (termuxTmpDir.exists()) {
|
||||||
|
try {
|
||||||
|
TermuxInstaller.deleteFolder(termuxTmpDir.getCanonicalFile());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(EmulatorDebug.LOG_TAG, "Error while removing file at " + termuxTmpDir.getAbsolutePath(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
termuxTmpDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
if (mWakeLock != null) mWakeLock.release();
|
if (mWakeLock != null) mWakeLock.release();
|
||||||
if (mWifiLock != null) mWifiLock.release();
|
if (mWifiLock != null) mWifiLock.release();
|
||||||
|
|
||||||
@@ -250,14 +275,6 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
return mTerminalSessions;
|
return mTerminalSessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isWakelockEnabled() {
|
|
||||||
if (mWakeLock == null) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return mWakeLock.isHeld();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
|
TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
|
||||||
new File(HOME_PATH).mkdirs();
|
new File(HOME_PATH).mkdirs();
|
||||||
|
|
||||||
@@ -267,11 +284,13 @@ public final class TermuxService extends Service implements SessionChangedCallba
|
|||||||
boolean isLoginShell = false;
|
boolean isLoginShell = false;
|
||||||
|
|
||||||
if (executablePath == null) {
|
if (executablePath == null) {
|
||||||
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
if (!failSafe) {
|
||||||
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
|
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
||||||
if (shellFile.canExecute()) {
|
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
|
||||||
executablePath = shellFile.getAbsolutePath();
|
if (shellFile.canExecute()) {
|
||||||
break;
|
executablePath = shellFile.getAbsolutePath();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.4.0'
|
classpath 'com.android.tools.build:gradle:3.4.2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,15 @@ public final class TerminalBuffer {
|
|||||||
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
|
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTranscriptTextWithoutJoinedLines() {
|
||||||
|
return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows, false).trim();
|
||||||
|
}
|
||||||
|
|
||||||
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
|
public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
|
||||||
|
return getSelectedText(selX1, selY1, selX2, selY2, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines) {
|
||||||
final StringBuilder builder = new StringBuilder();
|
final StringBuilder builder = new StringBuilder();
|
||||||
final int columns = mColumns;
|
final int columns = mColumns;
|
||||||
|
|
||||||
@@ -79,7 +87,8 @@ public final class TerminalBuffer {
|
|||||||
}
|
}
|
||||||
if (lastPrintingCharIndex != -1)
|
if (lastPrintingCharIndex != -1)
|
||||||
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
|
||||||
if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
if ((!joinBackLines || !rowLineWrap)
|
||||||
|
&& row < selY2 && row < mScreenRows - 1) builder.append('\n');
|
||||||
}
|
}
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user