Skip to content

Commit

Permalink
Implement GUI based Termux settings manager and a centralized logging…
Browse files Browse the repository at this point in the history
… framework

The settings activity can be accessed by long pressing on terminal view and selecting "Settings" from the popup shown. It uses the Android's Preference framework. Currently only debugging preferences to set log level and enabling terminal view key logging are provided. The Preference framework by default uses the keys set in `app:key` attribute in the respective preferences XML file to store the values in the default `SharedPreferences` file of the app. However, since we rely on `TermuxPreferenceConstants` and `TermuxPropertyConstants` classes to define key names so that they can be easily shared between termux and its plugin apps, we provide our own `PreferenceDataStore` for storing key/value pairs. The key name in the XML file can optionally be the same. Check `DebuggingPreferencesFragment` class for a sample. Each new preference category fragment should be added to `app/settings/` with its data store.

This commit may allow support to be added for modifying `termux.properties` file directly from the UI but that requires more work, since writing to property files with comments require in-place modification.

The `Logger` class provides various static functions for logging that should be used from now on instead of directly calling android `Log.*` functions. The log level is automatically loaded from shared preferences at application startup via `TermuxApplication` and set in the static `Logger.CURRENT_LOG_LEVEL` variable. Changing the log level through the settings activity also changes the log level immediately.

The 4 supported log levels are:
- LOG_LEVEL_OFF which will log nothing.
- LOG_LEVEL_NORMAL which will start logging error, warn and info messages and stacktraces.
- LOG_LEVEL_DEBUG which will start logging debug messages.
- LOG_LEVEL_VERBOSE which will start logging verbose messages.

The default log level is `LOG_LEVEL_NORMAL` which will not log debug or verbose messages. Contributors can add useful log entries at those levels where ever they feel is appropriate so that it allows users and devs to more easily help solve issues or find bugs, specially without having to recompile termux after having to manually add general log entries to the source. DO NOT log data that may have private info of users like command arguments at log levels below debug, like `BackgroundJob` was doing previously.

Logging to file support may be added later, will require log file rotation support and storage permissions.
  • Loading branch information
agnostic-apollo committed Mar 13, 2021
1 parent 93b506a commit d39972b
Show file tree
Hide file tree
Showing 26 changed files with 854 additions and 178 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ android {
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference:1.1.1'
testImplementation 'junit:junit:4.13.1'
testImplementation 'org.robolectric:robolectric:4.4'
}
Expand Down
131 changes: 81 additions & 50 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.termux"
android:installLocation="internalOnly"
android:sharedUserId="${TERMUX_PACKAGE_NAME}"
android:sharedUserLabel="@string/shared_user_label" >
android:sharedUserLabel="@string/shared_user_label">

<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />

<permission android:name="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND"
android:label="@string/run_command_permission_label"
<permission
android:name="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND"
android:description="@string/run_command_permission_description"
android:icon="@mipmap/ic_launcher"
android:label="@string/run_command_permission_label"
android:protectionLevel="dangerous" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Expand All @@ -20,65 +26,95 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.DUMP" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

<application
android:label="@string/application_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:name=".app.TermuxApplication"
android:allowBackup="false"
android:banner="@drawable/banner"
android:theme="@style/Theme.Termux"

android:extractNativeLibs="true"
android:allowBackup="false"
android:supportsRtl="false" >
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="@style/Theme.Termux">

<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
mark the app with "This app is optimized to run in full screen." -->
<meta-data android:name="android.max_aspect" android:value="10.0" />
<!--
This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
mark the app with "This app is optimized to run in full screen."
-->
<meta-data
android:name="android.max_aspect"
android:value="10.0" />

<activity
android:name=".app.TermuxActivity"
android:label="@string/application_name"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
android:label="@string/application_name"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</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-alias
android:name=".HomeActivity"
android:targetActivity=".app.TermuxActivity">

<!-- Launch activity automatically on boot on Android Things devices -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.IOT_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity-alias>

<activity
android:name=".app.TermuxHelpActivity"
android:exported="false"
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
android:label="@string/application_name"
android:parentActivityName=".app.TermuxActivity"
android:resizeableActivity="true"
android:label="@string/application_name" />
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />

<activity
android:name=".app.TermuxSettingsActivity"
android:label="@string/title_activity_termux_settings"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />

<activity
android:name=".filepicker.TermuxFileReceiverActivity"
android:label="@string/application_name"
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver"
android:excludeFromRecents="true"
android:label="@string/application_name"
android:noHistory="true"
android:resizeableActivity="true"
android:noHistory="true">
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver">

<!-- Accept multiple file types when sending. -->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SEND" />

<category android:name="android.intent.category.DEFAULT" />

<data android:mimeType="application/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="image/*" />
Expand All @@ -89,8 +125,10 @@
</intent-filter>
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />

<data android:mimeType="application/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="image/*" />
Expand All @@ -99,23 +137,11 @@
</intent-filter>
</activity>

<activity-alias
android:name=".HomeActivity"
android:targetActivity=".app.TermuxActivity">

<!-- Launch activity automatically on boot on Android Things devices -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.IOT_LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity-alias>

<provider
android:name=".filepicker.TermuxDocumentsProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.documents"
android:grantUriPermissions="true"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
Expand All @@ -125,25 +151,30 @@
<service
android:name=".app.TermuxService"
android:exported="false" />

<service
android:name=".app.RunCommandService"
android:exported="true"
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND" >
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND">
<intent-filter>
<action android:name="${TERMUX_PACKAGE_NAME}.RUN_COMMAND" />
</intent-filter>
</service>

<receiver android:name=".app.TermuxOpenReceiver" />

<provider android:authorities="${TERMUX_PACKAGE_NAME}.files"
android:readPermission="android.permission.permRead"
android:exported="true"
android:grantUriPermissions="true"
android:name=".app.TermuxOpenReceiver$ContentProvider" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
<provider
android:name=".app.TermuxOpenReceiver$ContentProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.files"
android:exported="true"
android:grantUriPermissions="true"
android:readPermission="android.permission.permRead" />

<meta-data
android:name="com.sec.android.support.multiwindow"
android:value="true" />
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
</application>

</manifest>
20 changes: 10 additions & 10 deletions app/src/main/java/com/termux/app/BackgroundJob.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

import com.termux.BuildConfig;
import com.termux.app.utils.Logger;

import java.io.BufferedReader;
import java.io.File;
Expand All @@ -26,10 +26,10 @@
*/
public final class BackgroundJob {

private static final String LOG_TAG = "termux-task";

final Process mProcess;

private static final String LOG_TAG = "BackgroundJob";

public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service){
this(cwd, fileToExecute, args, service, null);
}
Expand All @@ -47,7 +47,7 @@ public BackgroundJob(String cwd, String fileToExecute, final String[] args, fina
} catch (IOException e) {
mProcess = null;
// TODO: Visible error message?
Log.e(LOG_TAG, "Failed running background job: " + processDescription, e);
Logger.logStackTraceWithMessage(LOG_TAG, "Failed running background job: " + processDescription, e);
return;
}

Expand All @@ -67,7 +67,7 @@ public void run() {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
errResult.append(line).append('\n');
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
Logger.logDebug(LOG_TAG, "[" + pid + "] stderr: " + line);
}
} catch (IOException e) {
// Ignore.
Expand All @@ -79,28 +79,28 @@ public void run() {
new Thread() {
@Override
public void run() {
Log.i(LOG_TAG, "[" + pid + "] starting: " + processDescription);
Logger.logDebug(LOG_TAG, "[" + pid + "] starting: " + processDescription);
InputStream stdout = mProcess.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));

String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
Log.i(LOG_TAG, "[" + pid + "] stdout: " + line);
Logger.logDebug(LOG_TAG, "[" + pid + "] stdout: " + line);
outResult.append(line).append('\n');
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error reading output", e);
Logger.logStackTraceWithMessage(LOG_TAG, "Error reading output", e);
}

try {
int exitCode = mProcess.waitFor();
service.onBackgroundJobExited(BackgroundJob.this);
if (exitCode == 0) {
Log.i(LOG_TAG, "[" + pid + "] exited normally");
Logger.logDebug(LOG_TAG, "[" + pid + "] exited normally");
} else {
Log.w(LOG_TAG, "[" + pid + "] exited with code: " + exitCode);
Logger.logDebug(LOG_TAG, "[" + pid + "] exited with code: " + exitCode);
}

result.putString("stdout", outResult.toString());
Expand Down
12 changes: 7 additions & 5 deletions app/src/main/java/com/termux/app/RunCommandService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;

import com.termux.R;
import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.app.settings.properties.TermuxPropertyConstants;
import com.termux.app.settings.properties.TermuxSharedProperties;
import com.termux.app.utils.Logger;

import java.io.File;
import java.io.FileInputStream;
Expand Down Expand Up @@ -84,6 +84,8 @@ public class RunCommandService extends Service {
private static final String NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel";
private static final int NOTIFICATION_ID = 1338;

private static final String LOG_TAG = "RunCommandService";

class LocalBinder extends Binder {
public final RunCommandService service = RunCommandService.this;
}
Expand All @@ -107,13 +109,13 @@ public int onStartCommand(Intent intent, int flags, int startId) {

// If wrong action passed, then just return
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
Log.e("termux", "Unexpected intent action to RunCommandService: " + intent.getAction());
Logger.logError(LOG_TAG, "Unexpected intent action to RunCommandService: " + intent.getAction());
return Service.START_NOT_STICKY;
}

// If allow-external-apps property is not set to "true"
if (!TermuxSharedProperties.isPropertyValueTrue(this, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) {
Log.e("termux", "RunCommandService requires allow-external-apps property to be set to \"true\" in \"" + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH + "\" file");
Logger.logError(LOG_TAG, "RunCommandService requires allow-external-apps property to be set to \"true\" in \"" + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH + "\" file");
return Service.START_NOT_STICKY;
}

Expand Down Expand Up @@ -155,7 +157,7 @@ private void runStopForeground() {

private Notification buildNotification() {
Notification.Builder builder = new Notification.Builder(this);
builder.setContentTitle(getText(R.string.application_name) + " Run Command");
builder.setContentTitle(TermuxConstants.TERMUX_APP_NAME + " Run Command");
builder.setSmallIcon(R.drawable.ic_service_notification);

// Use a low priority:
Expand All @@ -177,7 +179,7 @@ private Notification buildNotification() {
private void setupNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;

String channelName = "Termux Run Command";
String channelName = TermuxConstants.TERMUX_APP_NAME + " Run Command";
int importance = NotificationManager.IMPORTANCE_LOW;

NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
Expand Down
Loading

0 comments on commit d39972b

Please sign in to comment.