merge sources of Termux:Widget

This commit is contained in:
Leonid Plyushch 2019-02-25 14:06:42 +02:00
parent 865d38db11
commit ef34333d4f
12 changed files with 643 additions and 43 deletions

View file

@ -128,6 +128,35 @@
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
</application>
<receiver
android:name="com.termux.widget.TermuxWidgetProvider"
android:label="@string/shortcut_widget_name" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/termux_appwidget_info" />
</receiver>
<activity
android:name="com.termux.widget.TermuxCreateShortcutActivity"
android:label="@string/single_shortcut_name"
android:theme="@android:style/Theme.Material.Light.DarkActionBar">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter>
</activity>
<activity
android:name="com.termux.widget.TermuxLaunchShortcutActivity"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay"
android:exported="true"/>
<service
android:name="com.termux.widget.TermuxWidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
</application>
</manifest>

View file

@ -0,0 +1,98 @@
package com.termux.widget;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.termux.R;
import java.io.File;
import java.util.Arrays;
public class TermuxCreateShortcutActivity extends Activity {
private ListView mListView;
private File mCurrentDirectory;
private File[] mCurrentFiles;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.shortcuts_listview);
mListView = findViewById(R.id.list);
}
@Override
protected void onResume() {
super.onResume();
updateListview(TermuxWidgetService.SHORTCUTS_DIR);
mListView.setOnItemClickListener((parent, view, position, id) -> {
final Context context = TermuxCreateShortcutActivity.this;
File clickedFile = mCurrentFiles[position];
if (clickedFile.isDirectory()) {
updateListview(clickedFile);
return;
}
Intent.ShortcutIconResource icon = Intent.ShortcutIconResource.fromContext(context, R.drawable.ic_launcher);
Uri scriptUri = new Uri.Builder().scheme("com.termux.file").path(clickedFile.getAbsolutePath()).build();
Intent executeIntent = new Intent(context, TermuxLaunchShortcutActivity.class);
executeIntent.setData(scriptUri);
executeIntent.putExtra(TermuxLaunchShortcutActivity.TOKEN_NAME, TermuxLaunchShortcutActivity.getGeneratedToken(context));
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, executeIntent);
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, clickedFile.getName());
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon);
setResult(RESULT_OK, intent);
finish();
});
}
private void updateListview(File directory) {
mCurrentDirectory = directory;
mCurrentFiles = directory.listFiles(pathname -> !pathname.getName().startsWith("."));
if (mCurrentFiles == null) mCurrentFiles = new File[0];
Arrays.sort(mCurrentFiles, (f1, f2) -> f1.getName().compareTo(f2.getName()));
final boolean isTopDir = directory.equals(TermuxWidgetService.SHORTCUTS_DIR);
getActionBar().setDisplayHomeAsUpEnabled(!isTopDir);
if (isTopDir && mCurrentFiles.length == 0) {
// Create if necessary so user can more easily add.
TermuxWidgetService.SHORTCUTS_DIR.mkdirs();
new AlertDialog.Builder(this)
.setMessage(R.string.no_shortcut_scripts)
.setOnDismissListener(dialog -> finish()).show();
return;
}
final String[] values = new String[mCurrentFiles.length];
for (int i = 0; i < values.length; i++)
values[i] = mCurrentFiles[i].getName() +
(mCurrentFiles[i].isDirectory() ? "/" : "");
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, android.R.id.text1, values);
mListView.setAdapter(adapter);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
updateListview(mCurrentDirectory.getParentFile());
return true;
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,69 @@
package com.termux.widget;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Log;
import android.view.Gravity;
import android.widget.Toast;
import com.termux.R;
import java.io.File;
import java.util.UUID;
/**
* An activity to launch a shortcut. We want to a launch a service directly, but a shortcut
* cannot be used to launch a service, only activities, so have to go through this activity.
*/
public class TermuxLaunchShortcutActivity extends Activity {
static final String TOKEN_NAME = "com.termux.shortcut.token";
public static String getGeneratedToken(Context context) {
SharedPreferences prefs = context.getSharedPreferences("token", Context.MODE_PRIVATE);
String token = prefs.getString("token", null);
if (token == null) {
token = UUID.randomUUID().toString();
prefs.edit().putString("token", token).apply();
}
return token;
}
@Override
protected void onResume() {
super.onResume();
Intent intent = getIntent();
String token = intent.getStringExtra(TOKEN_NAME);
if (token == null || !token.equals(getGeneratedToken(this))) {
Log.w("termux", "Strange token: " + token);
Toast.makeText(this, R.string.bad_token_message, Toast.LENGTH_LONG).show();
finish();
return;
}
File clickedFile = new File(intent.getData().getPath());
TermuxWidgetProvider.ensureFileReadableAndExecutable(clickedFile);
// Do not use the intent data passed in, since that may be an old one with a file:// uri
// which is not allowed starting with Android 7.
Uri scriptUri = new Uri.Builder().scheme("com.termux.file").path(clickedFile.getAbsolutePath()).build();
Intent executeIntent = new Intent(TermuxWidgetProvider.ACTION_EXECUTE, scriptUri);
executeIntent.setClassName("com.termux", TermuxWidgetProvider.TERMUX_SERVICE);
if (clickedFile.getParentFile().getName().equals("tasks")) {
executeIntent.putExtra("com.termux.execute.background", true);
// Show feedback for executed background task.
String message = "Task executed: " + clickedFile.getName();
Toast toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
}
TermuxWidgetProvider.startTermuxService(this, executeIntent);
finish();
}
}

View file

@ -0,0 +1,132 @@
package com.termux.widget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.view.Gravity;
import android.widget.RemoteViews;
import android.widget.Toast;
import com.termux.R;
import java.io.File;
/**
* Widget providing a list to launch scripts in $HOME/.termux/shortcuts/.
* <p>
* See https://developer.android.com/guide/topics/appwidgets/index.html
*/
public final class TermuxWidgetProvider extends AppWidgetProvider {
private static final String LIST_ITEM_CLICKED_ACTION = "com.termux.widgets.LIST_ITEM_CLICKED_ACTION";
private static final String REFRESH_WIDGET_ACTION = "com.termux.widgets.REFRESH_WIDGET_ACTION";
public static final String EXTRA_CLICKED_FILE = "com.termux.widgets.EXTRA_CLICKED_FILE";
public static final String TERMUX_SERVICE = "com.termux.app.TermuxService";
public static final String ACTION_EXECUTE = "com.termux.service_execute";
/**
* "This is called to update the App Widget at intervals defined by the updatePeriodMillis attribute in the
* AppWidgetProviderInfo (see Adding the AppWidgetProviderInfo Metadata above). This method is also called when the
* user adds the App Widget, so it should perform the essential setup, such as define event handlers for Views and
* start a temporary Service, if necessary. However, if you have declared a configuration Activity, this method is
* not called when the user adds the App Widget, but is called for the subsequent updates. It is the responsibility
* of the configuration Activity to perform the first update when configuration is done. (See Creating an App Widget
* Configuration Activity below.)"
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
for (int appWidgetId : appWidgetIds) {
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
// The empty view is displayed when the collection has no items. It should be a sibling
// of the collection view:
rv.setEmptyView(R.id.widget_list, R.id.empty_view);
// Setup intent which points to the TermuxWidgetService which will provide the views for this collection.
Intent intent = new Intent(context, TermuxWidgetService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
// When intents are compared, the extras are ignored, so we need to embed the extras
// into the data so that the extras will not be ignored.
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
rv.setRemoteAdapter(R.id.widget_list, intent);
// Setup refresh button:
Intent refreshIntent = new Intent(context, TermuxWidgetProvider.class);
refreshIntent.setAction(TermuxWidgetProvider.REFRESH_WIDGET_ACTION);
refreshIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
refreshIntent.setData(Uri.parse(refreshIntent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent refreshPendingIntent = PendingIntent.getBroadcast(context, 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.refresh_button, refreshPendingIntent);
// Here we setup the a pending intent template. Individuals items of a collection
// cannot setup their own pending intents, instead, the collection as a whole can
// setup a pending intent template, and the individual items can set a fillInIntent
// to create unique before on an item to item basis.
Intent toastIntent = new Intent(context, TermuxWidgetProvider.class);
toastIntent.setAction(TermuxWidgetProvider.LIST_ITEM_CLICKED_ACTION);
toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setPendingIntentTemplate(R.id.widget_list, toastPendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, rv);
}
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
switch (intent.getAction()) {
case LIST_ITEM_CLICKED_ACTION:
String clickedFilePath = intent.getStringExtra(EXTRA_CLICKED_FILE);
File clickedFile = new File(clickedFilePath);
if (clickedFile.isDirectory()) return;
ensureFileReadableAndExecutable(clickedFile);
Uri scriptUri = new Uri.Builder().scheme("com.termux.file").path(clickedFilePath).build();
// Note: Must match TermuxService#ACTION_EXECUTE constant:
Intent executeIntent = new Intent(ACTION_EXECUTE, scriptUri);
executeIntent.setClassName("com.termux", TERMUX_SERVICE);
if (clickedFile.getParentFile().getName().equals("tasks")) {
executeIntent.putExtra("com.termux.execute.background", true);
// Show feedback for executed background task.
String message = "Task executed: " + clickedFile.getName();
Toast toast = Toast.makeText(context, message, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
}
startTermuxService(context, executeIntent);
break;
case REFRESH_WIDGET_ACTION:
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list);
Toast toast = Toast.makeText(context, R.string.scripts_reloaded, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
break;
}
}
static void startTermuxService(Context context, Intent executeIntent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// https://developer.android.com/about/versions/oreo/background.html
context.startForegroundService(executeIntent);
} else {
context.startService(executeIntent);
}
}
/** Ensure readable and executable file if user forgot to do so. */
static void ensureFileReadableAndExecutable(File file) {
if (!file.canRead()) file.setReadable(true);
if (!file.canExecute()) file.setExecutable(true);
}
}

View file

@ -0,0 +1,149 @@
package com.termux.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
import com.termux.R;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class TermuxWidgetService extends RemoteViewsService {
@SuppressLint("SdCardPath")
public static final File SHORTCUTS_DIR = new File("/data/data/com.termux/files/home/.shortcuts");
public static final class TermuxWidgetItem {
/** Label to display in the list. */
public final String mLabel;
/** The file which this item represents, sent with the {@link TermuxWidgetProvider#EXTRA_CLICKED_FILE} extra. */
public final String mFile;
public TermuxWidgetItem(File file, int depth) {
this.mLabel = (depth > 0 ? (file.getParentFile().getName() + "/") : "")
+ file.getName().replace('-', ' ');
this.mFile = file.getAbsolutePath();
}
}
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new ListRemoteViewsFactory(getApplicationContext());
}
public static class ListRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private final List<TermuxWidgetItem> mWidgetItems = new ArrayList<>();
private final Context mContext;
public ListRemoteViewsFactory(Context context) {
mContext = context;
}
@Override
public void onCreate() {
// In onCreate() you setup any connections / cursors to your data source. Heavy lifting,
// for example downloading or creating content etc, should be deferred to onDataSetChanged()
// or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
}
@Override
public void onDestroy() {
mWidgetItems.clear();
}
@Override
public int getCount() {
return mWidgetItems.size();
}
@Override
public RemoteViews getViewAt(int position) {
// Position will always range from 0 to getCount() - 1.
TermuxWidgetItem widgetItem = mWidgetItems.get(position);
// Construct remote views item based on the item xml file and set text based on position.
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
rv.setTextViewText(R.id.widget_item, widgetItem.mLabel);
// 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(TermuxWidgetProvider.EXTRA_CLICKED_FILE, widgetItem.mFile);
rv.setOnClickFillInIntent(R.id.widget_item_layout, fillInIntent);
// You can do heaving lifting in here, synchronously. For example, if you need to
// process an image, fetch something from the network, etc., it is ok to do it here,
// synchronously. A loading view will show up in lieu of the actual contents in the
// interim.
return rv;
}
@Override
public RemoteViews getLoadingView() {
// You can create a custom loading view (for instance when getViewAt() is slow.) If you
// return null here, you will get the default loading view.
return null;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return true;
}
@SuppressLint("SdCardPath")
@Override
public void onDataSetChanged() {
// This is triggered when you call AppWidgetManager notifyAppWidgetViewDataChanged
// on the collection view corresponding to this factory. You can do heaving lifting in
// here, synchronously. For example, if you need to process an image, fetch something
// from the network, etc., it is ok to do it here, synchronously. The widget will remain
// in its current state while work is being done here, so you don't need to worry about
// locking up the widget.
mWidgetItems.clear();
// Create directory if necessary so user more easily finds where to put shortcuts:
SHORTCUTS_DIR.mkdirs();
addFile(SHORTCUTS_DIR, mWidgetItems, 0);
}
}
private static void addFile(File dir, List<TermuxWidgetItem> widgetItems, int depth) {
if (depth > 5) return;
File[] files = dir.listFiles(pathname -> !pathname.getName().startsWith("."));
if (files == null) return;
Arrays.sort(files, (lhs, rhs) -> {
if (lhs.isDirectory() != rhs.isDirectory()) {
return lhs.isDirectory() ? 1 : -1;
}
return lhs.getName().compareToIgnoreCase(rhs.getName());
});
for (File file : files) {
if (file.isDirectory()) {
addFile(file, widgetItems, depth + 1);
} else {
widgetItems.add(new TermuxWidgetItem(file, depth));
}
}
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/darker_gray" >
<item android:id="@android:id/mask">
<shape android:shape="rectangle" >
<solid android:color="#FF000000" />
</shape>
</item>
</ripple>

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list"
android:layout_height="wrap_content"
android:layout_width="match_parent">
</ListView>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ripple_mask"
android:padding="10dp" >
<!--
See http://stackoverflow.com/questions/16278159/why-linearlayouts-margin-is-being-ignored-if-used-as-listview-row-view
for the extra layout wrapper.
-->
<TextView
android:id="@+id/widget_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/primary_text_light" />
</FrameLayout>

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="0dp" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="0dp"
android:paddingTop="6dp" >
<LinearLayout
android:id="@+id/top_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FF000000"
android:orientation="horizontal"
android:padding="6dp" >
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="2.0"
android:text="@string/termux"
android:textColor="@android:color/primary_text_dark"
android:textSize="18sp" />
</LinearLayout>
<ListView
android:id="@+id/widget_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/top_row"
android:background="@android:color/background_light"
android:divider="@android:color/darker_gray"
android:dividerHeight="1px" >
</ListView>
<!-- Shown for empty collection due to rv.setEmptyView(R.id.widget_list, R.id.empty_view) being called: -->
<TextView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/top_row"
android:background="@android:color/white"
android:padding="6dp"
android:text="@string/no_shortcut_scripts"
android:textColor="@android:color/black" />
</RelativeLayout>
<ImageButton
android:id="@+id/refresh_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:contentDescription="@string/refresh"
android:tint="#99000000"
android:src="@android:drawable/stat_notify_sync" />
</RelativeLayout>

View file

@ -49,8 +49,15 @@
<string name="file_received_edit_button">Edit</string>
<string name="file_received_open_folder_button">Open folder</string>
<string name="color_prompt">Choose color</string>
<string name="font_prompt">Choose font</string>
<string name="writing_failed">Failed to install file:\n\n</string>
<string name="shortcut_widget_name">All shortcuts</string>
<string name="single_shortcut_name">Single shortcut</string>
<string name="no_shortcut_scripts">No files in\n$HOME/.shortcuts/</string>
<string name="refresh">Refresh</string>
<string name="termux">Termux</string>
<string name="scripts_reloaded">Termux shortcuts reloaded</string>
<string name="bad_token_message">This shortcut has become invalid - remove and add again.</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
See
http://developer.android.com/guide/practices/ui_guidelines/widget_design.html#anatomy_determining_size
for how to determine size:
-->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_layout"
android:minHeight="110dp"
android:minWidth="110dp"
android:previewImage="@drawable/widgetpreview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen|keyguard" />