Skip to content

Commit 6253df5

Browse files
Added: Add dynamic shortcuts
Co-authored-by: Fabian Thomas <fabian@fabianthomas.de> Co-authored-by: agnostic-apollo <agnosticapollo@gmail.com>
1 parent 56bf254 commit 6253df5

File tree

4 files changed

+309
-9
lines changed

4 files changed

+309
-9
lines changed

app/src/main/java/com/termux/widget/TermuxCreateShortcutActivity.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,10 @@ private void createPinnedShortcut(Context context, String shortcutFilePath) {
147147
String shortcutFileName = ShellUtils.getExecutableBasename(shortcutFilePath);
148148

149149
ShortcutInfo.Builder builder = new ShortcutInfo.Builder(context, shortcutFilePath);
150-
builder.setIntent(getExecutionIntent(context, shortcutFilePath));
150+
builder.setIntent(TermuxCreateShortcutActivity.getExecutionIntent(context, shortcutFilePath));
151151
builder.setShortLabel(shortcutFileName);
152152

153-
File shortcutIconFile = getShortcutIconFile(context, shortcutFileName);
153+
File shortcutIconFile = TermuxCreateShortcutActivity.getShortcutIconFile(context, shortcutFileName);
154154
if (shortcutIconFile != null)
155155
builder.setIcon(Icon.createWithBitmap(((BitmapDrawable) Drawable.createFromPath(shortcutIconFile.getAbsolutePath())).getBitmap()));
156156
else
@@ -165,10 +165,10 @@ private void createStaticShortcut(Context context, String shortcutFilePath) {
165165
String shortcutFileName = ShellUtils.getExecutableBasename(shortcutFilePath);
166166

167167
Intent intent = new Intent();
168-
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, getExecutionIntent(context, shortcutFilePath));
168+
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, TermuxCreateShortcutActivity.getExecutionIntent(context, shortcutFilePath));
169169
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, shortcutFileName);
170170

171-
File shortcutIconFile = getShortcutIconFile(context, shortcutFileName);
171+
File shortcutIconFile = TermuxCreateShortcutActivity.getShortcutIconFile(context, shortcutFileName);
172172
if (shortcutIconFile != null)
173173
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, ((BitmapDrawable) Drawable.createFromPath(shortcutIconFile.getAbsolutePath())).getBitmap());
174174
else
@@ -179,7 +179,7 @@ private void createStaticShortcut(Context context, String shortcutFilePath) {
179179
setResult(RESULT_OK, intent);
180180
}
181181

182-
private Intent getExecutionIntent(Context context, String shortcutFilePath) {
182+
public static Intent getExecutionIntent(Context context, String shortcutFilePath) {
183183
Uri scriptUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(shortcutFilePath).build();
184184
Intent executionIntent = new Intent(context, TermuxLaunchShortcutActivity.class);
185185
executionIntent.setAction(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE); // Mandatory for pinned shortcuts
@@ -189,7 +189,7 @@ private Intent getExecutionIntent(Context context, String shortcutFilePath) {
189189
}
190190

191191
@Nullable
192-
private File getShortcutIconFile(Context context, String shortcutFileName) {
192+
public static File getShortcutIconFile(Context context, String shortcutFileName) {
193193
String errmsg;
194194
String shortcutIconFilePath = FileUtils.getCanonicalPath(
195195
TermuxConstants.TERMUX_SHORTCUT_SCRIPT_ICONS_DIR_PATH +

app/src/main/java/com/termux/widget/activities/TermuxWidgetActivity.java

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,93 @@
11
package com.termux.widget.activities;
22

33
import android.os.Bundle;
4+
import android.view.View;
45
import android.widget.Button;
6+
import android.widget.LinearLayout;
57
import android.widget.TextView;
8+
import android.content.Context;
9+
import android.content.Intent;
10+
import android.content.pm.ShortcutInfo;
11+
import android.content.pm.ShortcutManager;
12+
import android.graphics.drawable.BitmapDrawable;
13+
import android.graphics.drawable.Drawable;
14+
import android.graphics.drawable.Icon;
15+
import android.os.Build;
616

17+
import androidx.annotation.NonNull;
18+
import androidx.annotation.RequiresApi;
719
import androidx.appcompat.app.AppCompatActivity;
820

21+
import com.termux.shared.file.FileUtils;
22+
import com.termux.shared.file.TermuxFileUtils;
923
import com.termux.shared.logger.Logger;
24+
import com.termux.shared.models.errors.Error;
1025
import com.termux.shared.packages.PackageUtils;
1126
import com.termux.shared.termux.TermuxConstants;
1227
import com.termux.widget.R;
28+
import com.termux.shared.shell.ShellUtils;
29+
import com.termux.widget.TermuxWidgetService;
30+
import com.termux.widget.TermuxCreateShortcutActivity;
31+
import com.termux.widget.NaturalOrderComparator;
32+
33+
import java.io.File;
34+
import java.util.Arrays;
35+
import java.util.ArrayList;
36+
import java.util.List;
37+
import java.util.regex.Pattern;
1338

1439
public class TermuxWidgetActivity extends AppCompatActivity {
1540

1641
private static final String LOG_TAG = "TermuxWidgetActivity";
1742

43+
/** Termux:Widget app data home directory path. */
44+
public static final String TERMUX_WIDGET_DATA_HOME_DIR_PATH = TermuxConstants.TERMUX_DATA_HOME_DIR_PATH + "/widget"; // Default: "/data/data/com.termux/files/home/.termux/widget"
45+
46+
/** Termux:Widget app directory path to store scripts/binaries to be used as dynamic shortcuts. */
47+
public static final String TERMUX_WIDGET_DYNAMIC_SHORTCUTS_DIR_PATH = TERMUX_WIDGET_DATA_HOME_DIR_PATH + "/dynamic_shortcuts"; // Default: "/data/data/com.termux/files/home/.termux/widget/dynamic_shortcuts"
48+
49+
public static final String MAX_SHORTCUTS_LIMIT_DOCS_URL = TermuxConstants.TERMUX_WIDGET_GITHUB_REPO_URL + "#max-shortcuts-limit-optional"; // Default: "https://github.com/termux/termux-widget#max-shortcuts-limit-optional"
50+
1851
@Override
1952
protected void onCreate(Bundle savedInstanceState) {
2053
super.onCreate(savedInstanceState);
54+
55+
Logger.logDebug(LOG_TAG, "onCreate");
56+
2157
setContentView(R.layout.activity_termux_widget);
2258

2359
TextView pluginInfo = findViewById(R.id.textview_plugin_info);
2460
pluginInfo.setText(getString(R.string.plugin_info, TermuxConstants.TERMUX_GITHUB_REPO_URL,
2561
TermuxConstants.TERMUX_WIDGET_GITHUB_REPO_URL));
2662

63+
setDisableLauncherIconViews();
64+
setDynamicShortcutsViews();
65+
}
66+
67+
@Override
68+
protected void onResume() {
69+
super.onResume();
70+
71+
Logger.logVerbose(LOG_TAG, "onResume");
72+
73+
setMaxShortcutsLimitView();
74+
}
75+
76+
private void setMaxShortcutsLimitView() {
77+
LinearLayout maxShortcutsInfoLinearLayout = findViewById(R.id.linearlayout_max_shortcuts_limit_info);
78+
maxShortcutsInfoLinearLayout.setVisibility(View.GONE);
79+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
80+
ShortcutManager shortcutManager = getShortcutManager(this, true);
81+
TextView maxShortcutsInfoTextView = findViewById(R.id.textview_max_shortcuts_limit_info);
82+
if (shortcutManager != null) {
83+
maxShortcutsInfoLinearLayout.setVisibility(View.VISIBLE);
84+
maxShortcutsInfoTextView.setText(getString(R.string.msg_max_shortcuts_limit_info,
85+
shortcutManager.getMaxShortcutCountPerActivity(), MAX_SHORTCUTS_LIMIT_DOCS_URL));
86+
}
87+
}
88+
}
89+
90+
private void setDisableLauncherIconViews() {
2791
Button disableLauncherIconButton = findViewById(R.id.btn_disable_launcher_icon);
2892
disableLauncherIconButton.setOnClickListener(v -> {
2993
String message = getString(R.string.msg_disabling_launcher_icon, TermuxConstants.TERMUX_WIDGET_APP_NAME);
@@ -34,4 +98,135 @@ protected void onCreate(Bundle savedInstanceState) {
3498
});
3599
}
36100

101+
private void setDynamicShortcutsViews() {
102+
LinearLayout dynamicShortcutsLinearLayout = findViewById(R.id.linearlayout_dynamic_shortcuts);
103+
104+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
105+
dynamicShortcutsLinearLayout.setVisibility(View.VISIBLE);
106+
107+
TextView dynamicShortcutsInfoTextView = findViewById(R.id.textview_dynamic_shortcuts_info);
108+
dynamicShortcutsInfoTextView.setText(getString(R.string.msg_dynamic_shortcuts_info,
109+
TermuxFileUtils.getUnExpandedTermuxPath(TERMUX_WIDGET_DYNAMIC_SHORTCUTS_DIR_PATH)));
110+
111+
Button createDynamicShortcutsButton = findViewById(R.id.btn_create_dynamic_shortcuts);
112+
createDynamicShortcutsButton.setOnClickListener(v -> createDynamicShortcuts(TermuxWidgetActivity.this));
113+
114+
Button removeDynamicShortcutsButton = findViewById(R.id.btn_remove_dynamic_shortcuts);
115+
removeDynamicShortcutsButton.setOnClickListener(v -> removeDynamicShortcuts(TermuxWidgetActivity.this));
116+
} else {
117+
dynamicShortcutsLinearLayout.setVisibility(View.GONE);
118+
}
119+
}
120+
121+
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
122+
public static ShortcutManager getShortcutManager(@NonNull Context context, boolean showToast) {
123+
ShortcutManager shortcutManager = (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
124+
if (shortcutManager == null) {
125+
Logger.logErrorAndShowToast(showToast ? context : null, LOG_TAG, "Failed to get shortcut manager");
126+
return null;
127+
}
128+
return shortcutManager;
129+
}
130+
131+
private static void enumerateShortcutFiles(List<TermuxWidgetService.TermuxWidgetItem> items, File dir, boolean sorted) {
132+
enumerateShortcutFiles(items, dir, sorted, 0);
133+
}
134+
135+
private static void enumerateShortcutFiles(List<TermuxWidgetService.TermuxWidgetItem> items, File dir, boolean sorted, int depth) {
136+
if (depth > 5) return;
137+
138+
File[] files = dir.listFiles(TermuxWidgetService.SHORTCUT_FILES_FILTER);
139+
140+
if (files == null) return;
141+
142+
if (sorted) {
143+
Arrays.sort(files, (lhs, rhs) -> {
144+
if (lhs.isDirectory() != rhs.isDirectory()) {
145+
return lhs.isDirectory() ? 1 : -1;
146+
}
147+
return NaturalOrderComparator.compare(lhs.getName(), rhs.getName());
148+
});
149+
}
150+
151+
for (File file : files) {
152+
if (file.isDirectory()) {
153+
enumerateShortcutFiles(items, file, sorted, depth + 1);
154+
} else {
155+
items.add(new TermuxWidgetService.TermuxWidgetItem(file, depth));
156+
}
157+
}
158+
159+
}
160+
161+
@RequiresApi(Build.VERSION_CODES.N_MR1)
162+
private void createDynamicShortcuts(@NonNull Context context) {
163+
ShortcutManager shortcutManager = getShortcutManager(context, true);
164+
if (shortcutManager == null) return;
165+
166+
// Create directory if necessary so user more easily finds where to put shortcuts
167+
Error error = FileUtils.createDirectoryFile(TERMUX_WIDGET_DYNAMIC_SHORTCUTS_DIR_PATH);
168+
if (error != null) {
169+
Logger.logError(LOG_TAG, error.toString());
170+
Logger.showToast(this, Error.getMinimalErrorLogString(error), true);
171+
}
172+
173+
List<TermuxWidgetService.TermuxWidgetItem> items = new ArrayList<>();
174+
enumerateShortcutFiles(items, new File(TERMUX_WIDGET_DYNAMIC_SHORTCUTS_DIR_PATH), false);
175+
176+
if (items.size() == 0) {
177+
Logger.showToast(context, getString(R.string.msg_no_shortcut_files_found_in_directory,
178+
TermuxFileUtils.getUnExpandedTermuxPath(TERMUX_WIDGET_DYNAMIC_SHORTCUTS_DIR_PATH)), true);
179+
return;
180+
}
181+
182+
List<ShortcutInfo> shortcuts = new ArrayList<>();
183+
for (TermuxWidgetService.TermuxWidgetItem item : items) {
184+
ShortcutInfo.Builder builder = new ShortcutInfo.Builder(context, item.mFile);
185+
builder.setIntent(TermuxCreateShortcutActivity.getExecutionIntent(context, item.mFile));
186+
builder.setShortLabel(item.mLabel);
187+
188+
File shortcutIconFile = TermuxCreateShortcutActivity.getShortcutIconFile(context, ShellUtils.getExecutableBasename(item.mFile));
189+
if (shortcutIconFile != null)
190+
builder.setIcon(Icon.createWithBitmap(((BitmapDrawable) Drawable.createFromPath(shortcutIconFile.getAbsolutePath())).getBitmap()));
191+
else
192+
builder.setIcon(Icon.createWithResource(context, R.drawable.ic_launcher));
193+
194+
shortcuts.add(builder.build());
195+
}
196+
197+
// Remove shortcuts that can not be added.
198+
int maxShortcuts = shortcutManager.getMaxShortcutCountPerActivity();
199+
Logger.logDebug(LOG_TAG, "Found " + items.size() + " shortcuts and max shortcuts limit is " + maxShortcuts);
200+
if (shortcuts.size() > maxShortcuts) {
201+
Logger.logErrorAndShowToast(context, LOG_TAG, getString(R.string.msg_dynamic_shortcuts_limit_reached, maxShortcuts));
202+
while (shortcuts.size() > maxShortcuts) {
203+
String message = getString(R.string.msg_skipping_shortcut,
204+
shortcuts.get(shortcuts.size() - 1).getId().replaceAll(
205+
"^" + Pattern.quote(TERMUX_WIDGET_DYNAMIC_SHORTCUTS_DIR_PATH + "/"), ""));
206+
Logger.showToast(context, message, false);
207+
Logger.logDebug(LOG_TAG, message);
208+
shortcuts.remove(shortcuts.size() - 1);
209+
}
210+
}
211+
212+
shortcutManager.removeAllDynamicShortcuts();
213+
shortcutManager.addDynamicShortcuts(shortcuts);
214+
Logger.logDebugAndShowToast(context, LOG_TAG, getString(R.string.msg_created_dynamic_shortcuts_successfully, shortcuts.size()));
215+
}
216+
217+
@RequiresApi(Build.VERSION_CODES.N_MR1)
218+
private void removeDynamicShortcuts(@NonNull Context context) {
219+
ShortcutManager shortcutManager = getShortcutManager(context, true);
220+
if (shortcutManager == null) return;
221+
222+
List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
223+
if (shortcuts != null && shortcuts.size() == 0) {
224+
Logger.logDebugAndShowToast(context, LOG_TAG, getString(R.string.msg_no_dynamic_shortcuts_currently_created));
225+
return;
226+
}
227+
228+
shortcutManager.removeAllDynamicShortcuts();
229+
Logger.logDebugAndShowToast(context, LOG_TAG, getString(R.string.msg_removed_dynamic_shortcuts_successfully));
230+
}
231+
37232
}

app/src/main/res/layout/activity_termux_widget.xml

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,32 @@
2727
android:textColorLink="?android:textColorLink"
2828
android:autoLink="web"/>
2929

30+
31+
32+
<LinearLayout
33+
android:id="@+id/linearlayout_max_shortcuts_limit_info"
34+
android:layout_width="match_parent"
35+
android:layout_height="wrap_content"
36+
android:orientation="vertical">
37+
<View style="@style/ViewDivider"/>
38+
39+
<com.google.android.material.textview.MaterialTextView
40+
android:id="@+id/textview_max_shortcuts_limit_info"
41+
android:layout_width="match_parent"
42+
android:layout_height="wrap_content"
43+
android:paddingTop="@dimen/activity_vertical_margin"
44+
android:paddingBottom="@dimen/activity_vertical_margin"
45+
android:gravity="start|center_vertical"
46+
android:textSize="14sp"
47+
android:textStyle="normal"
48+
android:textColor="?android:textColorPrimary"
49+
android:textColorLink="?android:textColorLink"
50+
android:autoLink="web"/>
51+
52+
</LinearLayout>
53+
54+
55+
3056
<View style="@style/ViewDivider"/>
3157

3258
<com.google.android.material.textview.MaterialTextView
@@ -35,7 +61,7 @@
3561
android:layout_height="wrap_content"
3662
android:paddingBottom="@dimen/activity_vertical_margin"
3763
android:gravity="start|center_vertical"
38-
android:text="@string/msg_disable_launcher_icon_details"
64+
android:text="@string/msg_disable_launcher_icon_info"
3965
android:textSize="14sp"
4066
android:textStyle="normal"
4167
android:textColor="?android:textColorPrimary"
@@ -54,6 +80,66 @@
5480
android:textColor="?android:textColorPrimary"
5581
app:strokeColor="?android:textColorPrimary"
5682
app:strokeWidth="2dp"/>
83+
84+
85+
86+
<LinearLayout
87+
android:id="@+id/linearlayout_dynamic_shortcuts"
88+
android:layout_width="match_parent"
89+
android:layout_height="wrap_content"
90+
android:orientation="vertical">
91+
92+
<View style="@style/ViewDivider"/>
93+
94+
<com.google.android.material.textview.MaterialTextView
95+
android:id="@+id/textview_dynamic_shortcuts_info"
96+
android:layout_width="match_parent"
97+
android:layout_height="wrap_content"
98+
android:paddingBottom="@dimen/activity_vertical_margin"
99+
android:gravity="start|center_vertical"
100+
android:textSize="14sp"
101+
android:textStyle="normal"
102+
android:textColor="?android:textColorPrimary"
103+
android:textColorLink="?android:textColorLink"
104+
android:autoLink="web"/>
105+
106+
<LinearLayout
107+
android:layout_width="match_parent"
108+
android:layout_height="wrap_content"
109+
android:orientation="horizontal">
110+
111+
<com.google.android.material.button.MaterialButton
112+
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
113+
android:id="@+id/btn_remove_dynamic_shortcuts"
114+
android:layout_width="0dp"
115+
android:layout_height="wrap_content"
116+
android:layout_weight="1"
117+
android:gravity="center"
118+
android:layout_marginStart="@dimen/content_padding_half"
119+
android:layout_marginEnd="@dimen/content_padding_half"
120+
android:text="@string/action_remove_dynamic_shortcuts"
121+
android:textSize="12sp"
122+
android:textColor="?android:textColorPrimary"
123+
app:strokeColor="?android:textColorPrimary"
124+
app:strokeWidth="2dp"/>
125+
126+
<com.google.android.material.button.MaterialButton
127+
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
128+
android:id="@+id/btn_create_dynamic_shortcuts"
129+
android:layout_width="0dp"
130+
android:layout_height="wrap_content"
131+
android:layout_weight="1"
132+
android:gravity="center"
133+
android:layout_marginStart="@dimen/content_padding_half"
134+
android:layout_marginEnd="@dimen/content_padding_half"
135+
android:text="@string/action_create_dynamic_shortcuts"
136+
android:textSize="12sp"
137+
android:textColor="?android:textColorPrimary"
138+
app:strokeColor="?android:textColorPrimary"
139+
app:strokeWidth="2dp"/>
140+
</LinearLayout>
141+
</LinearLayout>
142+
57143
</LinearLayout>
58144

59145
</ScrollView>

0 commit comments

Comments
 (0)