Skip to content

Commit

Permalink
Changed: Refactor TermuxCreateShortcutActivity to move shortcut fun…
Browse files Browse the repository at this point in the history
…ctions to `ShortcutFile` and change shortcut path behaviour

The `ShortcutFile` will now store the path instead of the file object

This commit will also now pass absolute path of shortcuts when creating intents for shortcuts and widgets instead of canonical path. This will allow path expansion to be done during execution instead of at creation and if underlying symlink destination changes, new destination will be executed. There will still be a security check during execution to check if shortcut is under allowed directories. The shortcut label will now also be generated from absolute path instead of canonical path as mentioned in termux#59

Co-authored-by: Fabian Thomas <fabian@fabianthomas.de>
Co-authored-by: agnostic-apollo <agnosticapollo@gmail.com>
  • Loading branch information
fabian-thomas and agnostic-apollo committed Oct 30, 2022
1 parent 527caa6 commit b67203a
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 114 deletions.
97 changes: 75 additions & 22 deletions app/src/main/java/com/termux/widget/ShortcutFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
import android.os.Build;
import android.widget.RemoteViews;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import com.google.common.base.Joiner;
import com.termux.shared.data.DataUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.file.TermuxFileUtils;
import com.termux.shared.file.filesystem.FileType;
Expand All @@ -32,18 +34,57 @@ public final class ShortcutFile {

private static final String LOG_TAG = "ShortcutFile";

public final File file;
public final String label;
public final String mPath;
public String mLabel;

public ShortcutFile(File file, int depth) {
this.file = file;
this.label = (depth > 0 ? (file.getParentFile().getName() + "/") : "")
+ file.getName();
public ShortcutFile(@NonNull String path) {
this(path, null);
}

public ShortcutFile(@NonNull File file) {
this(file.getAbsolutePath(), null);
}

public ShortcutFile(@NonNull File file, int depth) {
this(file.getAbsolutePath(),
(depth > 0 && file.getParentFile() != null ? (file.getParentFile().getName() + "/") : "") + file.getName());
}

public ShortcutFile(@NonNull String path, @Nullable String defaultLabel) {
mPath = path;
mLabel = getLabelForShortcut(defaultLabel);
}

@NonNull
public String getPath() {
return mPath;
}

@NonNull
public String getCanonicalPath() {
return FileUtils.getCanonicalPath(getPath(), null);
}

@NonNull
public String getUnExpandedPath() {
return TermuxFileUtils.getUnExpandedTermuxPath(getCanonicalPath());
}

@NonNull
public String getLabel() {
return mLabel;
}

@NonNull
public String getLabelForShortcut(@Nullable String defaultLabel) {
if (!DataUtils.isNullOrEmpty(defaultLabel))
return defaultLabel;
else
return ShellUtils.getExecutableBasename(mPath);
}

public Intent getExecutionIntent(Context context) {
String path = file.getAbsolutePath();
Uri scriptUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(path).build();
Uri scriptUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(getPath()).build();
Intent executionIntent = new Intent(context, TermuxLaunchShortcutActivity.class);
executionIntent.setAction(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE); // Mandatory for pinned shortcuts
executionIntent.setData(scriptUri);
Expand All @@ -53,14 +94,12 @@ public Intent getExecutionIntent(Context context) {

@RequiresApi(api = Build.VERSION_CODES.N_MR1)
public ShortcutInfo getShortcutInfo(Context context) {
String path = file.getAbsolutePath();

ShortcutInfo.Builder builder = new ShortcutInfo.Builder(context, path);
builder.setIntent(this.getExecutionIntent(context));
builder.setShortLabel(this.label);
ShortcutInfo.Builder builder = new ShortcutInfo.Builder(context, getPath());
builder.setIntent(getExecutionIntent(context));
builder.setShortLabel(getLabel());

// Set icon if existent.
File shortcutIconFile = TermuxCreateShortcutActivity.getShortcutIconFile(context, ShellUtils.getExecutableBasename(path));
File shortcutIconFile = getIconFile(context);
if (shortcutIconFile != null)
builder.setIcon(Icon.createWithBitmap(((BitmapDrawable) Drawable.createFromPath(shortcutIconFile.getAbsolutePath())).getBitmap()));
else
Expand All @@ -69,26 +108,40 @@ public ShortcutInfo getShortcutInfo(Context context) {
return builder.build();
}

public Intent getStaticShortcutIntent(Context context) {
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, getExecutionIntent(context));
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, getLabel());

// Set icon if existent.
File shortcutIconFile = getIconFile(context);
if (shortcutIconFile != null)
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, ((BitmapDrawable) Drawable.createFromPath(shortcutIconFile.getAbsolutePath())).getBitmap());
else
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(context, R.drawable.ic_launcher));

return intent;
}

public RemoteViews getListWidgetView(Context context) {
// Position will always range from 0 to getCount() - 1.
// Construct remote views item based on the item xml file and set text based on position.
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_item);
rv.setTextViewText(R.id.widget_item, this.label);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_item);
remoteViews.setTextViewText(R.id.widget_item, getLabel());

// Next, we set a fill-intent which will be used to fill-in the pending intent template
// which is set on the collection view in TermuxAppWidgetProvider.
Intent fillInIntent = new Intent().putExtra(TERMUX_WIDGET_PROVIDER.EXTRA_FILE_CLICKED, this.file.getAbsolutePath());
rv.setOnClickFillInIntent(R.id.widget_item_layout, fillInIntent);
Intent fillInIntent = new Intent().putExtra(TERMUX_WIDGET_PROVIDER.EXTRA_FILE_CLICKED, getPath());
remoteViews.setOnClickFillInIntent(R.id.widget_item_layout, fillInIntent);

return rv;
return remoteViews;
}

@Nullable
private File getIconFile(Context context) {
String errmsg;
String shortcutIconFilePath = FileUtils.getCanonicalPath(
TermuxConstants.TERMUX_SHORTCUT_SCRIPT_ICONS_DIR_PATH +
"/" + ShellUtils.getExecutableBasename(file.getAbsolutePath()) + ".png", null);
String shortcutIconFilePath = TermuxConstants.TERMUX_SHORTCUT_SCRIPT_ICONS_DIR_PATH +
"/" + ShellUtils.getExecutableBasename(getPath()) + ".png";

FileType fileType = FileUtils.getFileType(shortcutIconFilePath, true);
// Ensure file or symlink points to a regular file that exists
Expand Down
102 changes: 10 additions & 92 deletions app/src/main/java/com/termux/widget/TermuxCreateShortcutActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,16 @@
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.MenuItem;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import androidx.annotation.Nullable;

import com.google.common.base.Joiner;
import com.termux.shared.data.DataUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.file.TermuxFileUtils;
import com.termux.shared.file.filesystem.FileType;
import com.termux.shared.logger.Logger;
import com.termux.shared.settings.preferences.TermuxWidgetAppSharedPreferences;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.shared.termux.TermuxConstants.TERMUX_WIDGET;
import com.termux.shared.termux.TermuxUtils;
import com.termux.widget.utils.ShortcutUtils;

Expand Down Expand Up @@ -124,102 +108,36 @@ private void createShortcut(Context context, File clickedFile) {
isPinnedShortcutSupported = true;
}

String shortcutFilePath = FileUtils.getCanonicalPath(clickedFile.getAbsolutePath(), null);
ShortcutFile shortcutFile = new ShortcutFile(clickedFile);

try {
if (isPinnedShortcutSupported)
createPinnedShortcut(context, shortcutFilePath);
createPinnedShortcut(context, shortcutFile);
else
createStaticShortcut(context, shortcutFilePath);
createStaticShortcut(context, shortcutFile);
} catch (Exception e) {
String message = context.getString(
isPinnedShortcutSupported ? R.string.error_create_pinned_shortcut_failed : R.string.error_create_static_shortcut_failed,
TermuxFileUtils.getUnExpandedTermuxPath(shortcutFilePath));
shortcutFile.getUnExpandedPath());
Logger.logErrorAndShowToast(context, LOG_TAG, message + ": " + e.getMessage());
Logger.logStackTraceWithMessage(LOG_TAG, message, e);
}
}

@TargetApi(Build.VERSION_CODES.O)
private void createPinnedShortcut(Context context, String shortcutFilePath) {
private void createPinnedShortcut(Context context, ShortcutFile shortcutFile) {
ShortcutManager shortcutManager = (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
if (shortcutManager == null) return;

String shortcutFileName = ShellUtils.getExecutableBasename(shortcutFilePath);

ShortcutInfo.Builder builder = new ShortcutInfo.Builder(context, shortcutFilePath);
builder.setIntent(TermuxCreateShortcutActivity.getExecutionIntent(context, shortcutFilePath));
builder.setShortLabel(shortcutFileName);

File shortcutIconFile = TermuxCreateShortcutActivity.getShortcutIconFile(context, shortcutFileName);
if (shortcutIconFile != null)
builder.setIcon(Icon.createWithBitmap(((BitmapDrawable) Drawable.createFromPath(shortcutIconFile.getAbsolutePath())).getBitmap()));
else
builder.setIcon(Icon.createWithResource(context, R.drawable.ic_launcher));

Logger.showToast(context, context.getString(R.string.msg_request_create_pinned_shortcut,
TermuxFileUtils.getUnExpandedTermuxPath(shortcutFilePath)), true);
shortcutManager.requestPinShortcut(builder.build(), null);
shortcutFile.getUnExpandedPath()), true);
shortcutManager.requestPinShortcut(shortcutFile.getShortcutInfo(context), null);
}

private void createStaticShortcut(Context context, String shortcutFilePath) {
String shortcutFileName = ShellUtils.getExecutableBasename(shortcutFilePath);

Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, TermuxCreateShortcutActivity.getExecutionIntent(context, shortcutFilePath));
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, shortcutFileName);

File shortcutIconFile = TermuxCreateShortcutActivity.getShortcutIconFile(context, shortcutFileName);
if (shortcutIconFile != null)
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, ((BitmapDrawable) Drawable.createFromPath(shortcutIconFile.getAbsolutePath())).getBitmap());
else
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(context, R.drawable.ic_launcher));

private void createStaticShortcut(Context context, ShortcutFile shortcutFile) {
Logger.showToast(context, context.getString(R.string.msg_request_create_static_shortcut,
TermuxFileUtils.getUnExpandedTermuxPath(shortcutFilePath)), true);
setResult(RESULT_OK, intent);
}

public static Intent getExecutionIntent(Context context, String shortcutFilePath) {
Uri scriptUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(shortcutFilePath).build();
Intent executionIntent = new Intent(context, TermuxLaunchShortcutActivity.class);
executionIntent.setAction(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE); // Mandatory for pinned shortcuts
executionIntent.setData(scriptUri);
executionIntent.putExtra(TERMUX_WIDGET.EXTRA_TOKEN_NAME, TermuxWidgetAppSharedPreferences.getGeneratedToken(context));
return executionIntent;
}

@Nullable
public static File getShortcutIconFile(Context context, String shortcutFileName) {
String errmsg;
String shortcutIconFilePath = FileUtils.getCanonicalPath(
TermuxConstants.TERMUX_SHORTCUT_SCRIPT_ICONS_DIR_PATH +
"/" + shortcutFileName + ".png", null);

FileType fileType = FileUtils.getFileType(shortcutIconFilePath, true);
// Ensure file or symlink points to a regular file that exists
if (fileType != FileType.REGULAR) {
if (fileType != FileType.NO_EXIST) {
errmsg = context.getString(R.string.error_icon_not_a_regular_file, fileType.getName()) +
"\n" + context.getString(R.string.msg_icon_absolute_path, shortcutIconFilePath);
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
}
return null;
}

// Do not allow shortcut icons files not under SHORTCUT_ICONS_FILES_ALLOWED_PATHS_LIST
if (!FileUtils.isPathInDirPaths(shortcutIconFilePath, ShortcutUtils.SHORTCUT_ICONS_FILES_ALLOWED_PATHS_LIST, true)) {
errmsg = context.getString(R.string.error_icon_not_under_shortcut_icons_directories,
Joiner.on(", ").skipNulls().join(TermuxFileUtils.getUnExpandedTermuxPaths(ShortcutUtils.SHORTCUT_ICONS_FILES_ALLOWED_PATHS_LIST))) +
"\n" + context.getString(R.string.msg_icon_absolute_path, shortcutIconFilePath);
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
return null;
}

Logger.logInfo(LOG_TAG, "Using file at \"" + shortcutIconFilePath + "\" as shortcut icon file");
Logger.showToast(context, "Using file at \"" + shortcutIconFilePath + "\" as shortcut icon file", true);

return new File(shortcutIconFilePath);
shortcutFile.getUnExpandedPath()), true);
setResult(RESULT_OK, shortcutFile.getStaticShortcutIntent(context));
}

}

0 comments on commit b67203a

Please sign in to comment.