Skip to content

Commit 5965b80

Browse files
author
MoreOrLessSoftware
committed
Adds the ability to create Quick Launch shortcuts in Android
1 parent bb66708 commit 5965b80

File tree

9 files changed

+274
-13
lines changed

9 files changed

+274
-13
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,19 @@
115115
<meta-data android:name="WindowManagerPreference:FreeformWindowSize" android:value="system-default" />
116116
<meta-data android:name="WindowManagerPreference:FreeformWindowOrientation" android:value="landscape" />
117117
</activity>
118+
119+
<!-- Activity for creating Quick Launch shortcuts -->
120+
<activity
121+
android:name=".QuickLaunchShortcutPicker"
122+
android:label="Quick Launch App"
123+
android:icon="@mipmap/ic_shortcut_quick_launch"
124+
android:exported="true"
125+
android:theme="@android:style/Theme.Material.Light.Dialog">
126+
<intent-filter>
127+
<action android:name="android.intent.action.CREATE_SHORTCUT" />
128+
<category android:name="android.intent.category.DEFAULT" />
129+
</intent-filter>
130+
</activity>
118131
<activity
119132
android:name=".AppView"
120133
android:resizeableActivity="true"
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.limelight;
2+
3+
import android.app.Activity;
4+
import android.content.Intent;
5+
import android.os.Build;
6+
import android.os.Bundle;
7+
import android.view.LayoutInflater;
8+
import android.view.View;
9+
import android.view.ViewGroup;
10+
import android.widget.AdapterView;
11+
import android.widget.ArrayAdapter;
12+
import android.widget.ListView;
13+
import android.widget.TextView;
14+
import android.widget.Toast;
15+
16+
17+
import com.limelight.utils.QuickLaunchManager;
18+
import com.limelight.utils.UiHelper;
19+
20+
import java.util.List;
21+
22+
/**
23+
* Activity that allows users to pick a Quick Launch item to create a shortcut for.
24+
* This is called when the user tries to add a Moonlight shortcut from their launcher or other apps.
25+
*/
26+
public class QuickLaunchShortcutPicker extends Activity {
27+
28+
@Override
29+
protected void onCreate(Bundle savedInstanceState) {
30+
super.onCreate(savedInstanceState);
31+
32+
UiHelper.setLocale(this);
33+
setContentView(R.layout.activity_shortcut_picker);
34+
35+
// Get all Quick Launch items
36+
QuickLaunchManager quickLaunchManager = QuickLaunchManager.getInstance(this);
37+
final List<QuickLaunchManager.QuickLaunchItem> items = quickLaunchManager.getAllQuickLaunchItems();
38+
39+
if (items.isEmpty()) {
40+
Toast.makeText(this, "No Quick Launch items found. Please create some first.", Toast.LENGTH_LONG).show();
41+
setResult(RESULT_CANCELED);
42+
finish();
43+
return;
44+
}
45+
46+
// Set up the list view with custom adapter
47+
ListView listView = findViewById(R.id.shortcut_list);
48+
49+
ArrayAdapter<QuickLaunchManager.QuickLaunchItem> adapter = new ArrayAdapter<QuickLaunchManager.QuickLaunchItem>(
50+
this, R.layout.list_item_quick_launch, items) {
51+
@Override
52+
public View getView(int position, View convertView, ViewGroup parent) {
53+
View view = convertView;
54+
if (view == null) {
55+
view = LayoutInflater.from(getContext()).inflate(R.layout.list_item_quick_launch, parent, false);
56+
}
57+
58+
QuickLaunchManager.QuickLaunchItem item = getItem(position);
59+
if (item != null) {
60+
TextView customNameView = view.findViewById(R.id.custom_name);
61+
TextView originalAppNameView = view.findViewById(R.id.original_app_name);
62+
63+
customNameView.setText(item.customName);
64+
originalAppNameView.setText(item.originalAppName);
65+
}
66+
67+
return view;
68+
}
69+
};
70+
71+
listView.setAdapter(adapter);
72+
73+
// Handle item selection
74+
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
75+
@Override
76+
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
77+
QuickLaunchManager.QuickLaunchItem selectedItem = items.get(position);
78+
createShortcut(selectedItem);
79+
}
80+
});
81+
82+
UiHelper.applyStatusBarPadding(listView);
83+
}
84+
85+
private void createShortcut(QuickLaunchManager.QuickLaunchItem item) {
86+
// Create the intent that will be launched when the shortcut is tapped
87+
Intent launchIntent = new Intent(this, ShortcutTrampoline.class);
88+
launchIntent.setAction(Intent.ACTION_VIEW);
89+
launchIntent.putExtra(ShortcutTrampoline.EXTRA_QUICK_LAUNCH_NAME, item.customName);
90+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
91+
92+
// Create the shortcut result that will be returned to the caller
93+
Intent shortcutIntent = new Intent();
94+
shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent);
95+
shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, item.customName);
96+
97+
// Use adaptive icon on Android 8.0+, simple icon on older versions
98+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
99+
// Use adaptive icon resource with gradient background
100+
shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
101+
Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_shortcut_quick_launch));
102+
} else {
103+
// Use simple play icon for pre-8.0 devices
104+
shortcutIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
105+
Intent.ShortcutIconResource.fromContext(this, R.drawable.ic_play));
106+
}
107+
108+
// Return the shortcut to the caller (launcher, automation app, etc.)
109+
setResult(RESULT_OK, shortcutIntent);
110+
finish();
111+
}
112+
}

app/src/main/java/com/limelight/ShortcutTrampoline.java

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.limelight.nvstream.wol.WakeOnLanSender;
1919
import com.limelight.utils.CacheHelper;
2020
import com.limelight.utils.Dialog;
21+
import com.limelight.utils.QuickLaunchManager;
2122
import com.limelight.utils.ServerHelper;
2223
import com.limelight.utils.SpinnerDialog;
2324
import com.limelight.utils.UiHelper;
@@ -31,9 +32,12 @@
3132
import java.util.UUID;
3233

3334
public class ShortcutTrampoline extends Activity {
35+
public static final String EXTRA_QUICK_LAUNCH_NAME = "QuickLaunchName";
36+
3437
private String uuidString;
3538
private NvApp app;
36-
private ArrayList<Intent> intentStack = new ArrayList<>();
39+
private String quickLaunchKey;
40+
private final ArrayList<Intent> intentStack = new ArrayList<>();
3741

3842
private int wakeHostTries = 10;
3943
private ComputerDetails computer;
@@ -111,46 +115,68 @@ public void notifyComputerUpdated(final ComputerDetails details) {
111115
runOnUiThread(new Runnable() {
112116
@Override
113117
public void run() {
118+
// If the managerBinder was already cleaned up by a previous callback,
119+
// just return early to avoid processing the same state multiple times
120+
if (managerBinder == null) {
121+
return;
122+
}
123+
114124
// Stop showing the spinner
115125
if (blockingLoadSpinner != null) {
116126
blockingLoadSpinner.dismiss();
117127
blockingLoadSpinner = null;
118128
}
119129

120-
// If the managerBinder was destroyed before this callback,
121-
// just finish the activity.
122-
if (managerBinder == null) {
123-
finish();
124-
return;
125-
}
126-
127130
if (details.state == ComputerDetails.State.ONLINE && details.pairState == PairingManager.PairState.PAIRED) {
128-
131+
129132
// Launch game if provided app ID, otherwise launch app view
130133
if (app != null) {
131134
if (details.runningGameId == 0 || details.runningGameId == app.getAppId()) {
132-
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this, app, details, managerBinder));
135+
// Add the PC view at the back (and clear the task)
136+
Intent i = new Intent(ShortcutTrampoline.this, PcView.class);
137+
i.setAction(Intent.ACTION_MAIN);
138+
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
139+
intentStack.add(i);
140+
141+
// Add the game intent
142+
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this, app, details, managerBinder, quickLaunchKey));
133143

134144
// Close this activity
135145
finish();
136146

137147
// Now start the activities
138148
startActivities(intentStack.toArray(new Intent[]{}));
149+
// Disable transition animation for seamless launch
150+
overridePendingTransition(0, 0);
139151
} else {
140152
// Create the start intent immediately, so we can safely unbind the managerBinder
141153
// below before we return.
142-
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this, app, details, managerBinder);
154+
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this, app, details, managerBinder, quickLaunchKey);
155+
156+
// Stop polling and unbind BEFORE showing the dialog to prevent it from flashing
157+
managerBinder.stopPolling();
158+
unbindService(serviceConnection);
159+
managerBinder = null;
143160

144161
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
145162
@Override
146163
public void run() {
164+
// Add the PC view at the back (and clear the task)
165+
Intent i = new Intent(ShortcutTrampoline.this, PcView.class);
166+
i.setAction(Intent.ACTION_MAIN);
167+
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
168+
intentStack.add(i);
169+
170+
// Add the game intent
147171
intentStack.add(startIntent);
148172

149173
// Close this activity
150174
finish();
151175

152176
// Now start the activities
153177
startActivities(intentStack.toArray(new Intent[]{}));
178+
// Disable transition animation for seamless launch
179+
overridePendingTransition(0, 0);
154180
}
155181
}, new Runnable() {
156182
@Override
@@ -159,6 +185,9 @@ public void run() {
159185
finish();
160186
}
161187
});
188+
189+
// Don't continue to the cleanup code below since we already cleaned up
190+
return;
162191
}
163192
} else {
164193
// Close this activity
@@ -184,6 +213,8 @@ public void run() {
184213

185214
// Now start the activities
186215
startActivities(intentStack.toArray(new Intent[]{}));
216+
// Disable transition animation for seamless launch
217+
overridePendingTransition(0, 0);
187218
}
188219

189220
}
@@ -284,6 +315,33 @@ protected void onCreate(Bundle savedInstanceState) {
284315
// App arguments, both are optional, but one must be provided in order to start an app
285316
String appIdString = getIntent().getStringExtra(Game.EXTRA_APP_ID);
286317
String appNameString = getIntent().getStringExtra(Game.EXTRA_APP_NAME);
318+
String quickLaunchName = getIntent().getStringExtra(EXTRA_QUICK_LAUNCH_NAME);
319+
320+
// Handle Quick Launch name lookup first
321+
if (quickLaunchName != null && !quickLaunchName.isEmpty()) {
322+
QuickLaunchManager quickLaunchManager = QuickLaunchManager.getInstance(this);
323+
QuickLaunchManager.QuickLaunchItem item = quickLaunchManager.getQuickLaunchItemByName(quickLaunchName);
324+
325+
if (item == null) {
326+
Dialog.displayDialog(ShortcutTrampoline.this,
327+
getResources().getString(R.string.conn_error_title),
328+
"Quick Launch item not found: " + quickLaunchName,
329+
true);
330+
return;
331+
}
332+
333+
// Set up the intent with the Quick Launch item details
334+
uuidString = item.computerUuid;
335+
appIdString = String.valueOf(item.appId);
336+
appNameString = item.originalAppName;
337+
quickLaunchKey = item.key;
338+
339+
// Update the intent extras
340+
setIntent(new Intent(getIntent())
341+
.putExtra(AppView.UUID_EXTRA, uuidString)
342+
.putExtra(Game.EXTRA_APP_ID, appIdString)
343+
.putExtra(Game.EXTRA_APP_NAME, appNameString));
344+
}
287345

288346
if (!validateInput(uuidString, appIdString, nameString)) {
289347
// Invalid input, so just return

app/src/main/java/com/limelight/utils/QuickLaunchManager.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ public boolean isQuickLaunchItemRunning(String key) {
238238
return key.equals(state.lastStartedQuickLaunchKey) && state.runningAppId == item.appId;
239239
}
240240

241-
private QuickLaunchItem getQuickLaunchItemByKey(String key) {
241+
public QuickLaunchItem getQuickLaunchItemByKey(String key) {
242242
if (key == null) return null;
243243

244244
List<QuickLaunchItem> items = getAllQuickLaunchItems();
@@ -249,7 +249,22 @@ private QuickLaunchItem getQuickLaunchItemByKey(String key) {
249249
}
250250
return null;
251251
}
252-
252+
253+
/**
254+
* Get a Quick Launch item by its custom name (case-insensitive)
255+
*/
256+
public QuickLaunchItem getQuickLaunchItemByName(String name) {
257+
if (name == null || name.isEmpty()) return null;
258+
259+
List<QuickLaunchItem> items = getAllQuickLaunchItems();
260+
for (QuickLaunchItem item : items) {
261+
if (item.customName.equalsIgnoreCase(name)) {
262+
return item;
263+
}
264+
}
265+
return null;
266+
}
267+
253268
/**
254269
* Add an app to Quick Launch
255270
*/
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<shape xmlns:android="http://schemas.android.com/apk/res/android">
3+
<gradient
4+
android:angle="135"
5+
android:startColor="#0D47A1"
6+
android:endColor="#1E88E5"
7+
android:type="linear" />
8+
</shape>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
3+
<!-- Add padding to keep the icon in the safe zone (middle 66%) of adaptive icon -->
4+
<item
5+
android:drawable="@drawable/ic_play"
6+
android:gravity="center"
7+
android:width="48dp"
8+
android:height="48dp">
9+
</item>
10+
</layer-list>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:layout_width="match_parent"
4+
android:layout_height="match_parent"
5+
android:orientation="vertical"
6+
android:background="?android:attr/colorBackground">
7+
8+
<ListView
9+
android:id="@+id/shortcut_list"
10+
android:layout_width="match_parent"
11+
android:layout_height="match_parent"
12+
android:divider="?android:attr/listDivider"
13+
android:dividerHeight="1dp" />
14+
15+
</LinearLayout>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:layout_width="match_parent"
4+
android:layout_height="wrap_content"
5+
android:orientation="vertical"
6+
android:padding="16dp"
7+
android:background="?android:attr/selectableItemBackground">
8+
9+
<TextView
10+
android:id="@+id/custom_name"
11+
android:layout_width="match_parent"
12+
android:layout_height="wrap_content"
13+
android:textSize="16sp"
14+
android:textStyle="bold"
15+
android:textColor="?android:attr/textColorPrimary" />
16+
17+
<TextView
18+
android:id="@+id/original_app_name"
19+
android:layout_width="match_parent"
20+
android:layout_height="wrap_content"
21+
android:layout_marginTop="4dp"
22+
android:textSize="14sp"
23+
android:textColor="?android:attr/textColorSecondary" />
24+
25+
</LinearLayout>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3+
<background android:drawable="@drawable/ic_shortcut_background"/>
4+
<foreground android:drawable="@drawable/ic_shortcut_foreground"/>
5+
</adaptive-icon>

0 commit comments

Comments
 (0)