diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d920d69e..3773df19 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -67,8 +67,9 @@ = Build.VERSION_CODES.O) { + menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut)); + } } @Override @@ -346,7 +357,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); final AppObject app = (AppObject) appGridAdapter.getItem(info.position); switch (item.getItemId()) { - case START_WTIH_QUIT: + case START_WITH_QUIT: // Display a confirmation dialog first UiHelper.displayQuitConfirmationDialog(this, new Runnable() { @Override @@ -386,6 +397,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { case CANCEL_ID: return true; + case VIEW_DETAILS_ID: + Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details), + getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false); + return true; + + case CREATE_SHORTCUT_ID: + ImageView appImageView = info.targetView.findViewById(R.id.grid_image); + Bitmap appBits = ((BitmapDrawable)appImageView.getDrawable()).getBitmap(); + if (!shortcutHelper.createPinnedGameShortcut(uuidString + Integer.valueOf(app.app.getAppId()).toString(), appBits, computer, app.app)) { + Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show(); + } + return true; + default: return super.onContextItemSelected(item); } diff --git a/app/src/main/java/com/limelight/AppViewShortcutTrampoline.java b/app/src/main/java/com/limelight/AppViewShortcutTrampoline.java deleted file mode 100644 index 40ac9d12..00000000 --- a/app/src/main/java/com/limelight/AppViewShortcutTrampoline.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.limelight; - -import android.app.Activity; -import android.app.Service; -import android.content.ComponentName; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.Bundle; -import android.os.IBinder; - -import com.limelight.computers.ComputerManagerListener; -import com.limelight.computers.ComputerManagerService; -import com.limelight.nvstream.http.ComputerDetails; -import com.limelight.nvstream.http.NvApp; -import com.limelight.utils.Dialog; -import com.limelight.utils.ServerHelper; -import com.limelight.utils.SpinnerDialog; -import com.limelight.utils.UiHelper; - -import java.util.ArrayList; -import java.util.UUID; - -public class AppViewShortcutTrampoline extends Activity { - private String uuidString; - - private ComputerDetails computer; - private SpinnerDialog blockingLoadSpinner; - - public final static String UUID_EXTRA = "UUID"; - - private ComputerManagerService.ComputerManagerBinder managerBinder; - private final ServiceConnection serviceConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder binder) { - final ComputerManagerService.ComputerManagerBinder localBinder = - ((ComputerManagerService.ComputerManagerBinder)binder); - - // Wait in a separate thread to avoid stalling the UI - new Thread() { - @Override - public void run() { - // Wait for the binder to be ready - localBinder.waitForReady(); - - // Now make the binder visible - managerBinder = localBinder; - - // Get the computer object - computer = managerBinder.getComputer(UUID.fromString(uuidString)); - - // Force CMS to repoll this machine - managerBinder.invalidateStateForComputer(computer.uuid); - - // Start polling - managerBinder.startPolling(new ComputerManagerListener() { - @Override - public void notifyComputerUpdated(final ComputerDetails details) { - // Don't care about other computers - if (!details.uuid.toString().equalsIgnoreCase(uuidString)) { - return; - } - - if (details.state != ComputerDetails.State.UNKNOWN) { - runOnUiThread(new Runnable() { - @Override - public void run() { - // Stop showing the spinner - if (blockingLoadSpinner != null) { - blockingLoadSpinner.dismiss(); - blockingLoadSpinner = null; - } - - // If the managerBinder was destroyed before this callback, - // just finish the activity. - if (managerBinder == null) { - finish(); - return; - } - - if (details.state == ComputerDetails.State.ONLINE) { - // Close this activity - finish(); - - // Create a new activity stack for this launch - ArrayList intentStack = new ArrayList<>(); - Intent i; - - // Add the PC view at the back (and clear the task) - i = new Intent(AppViewShortcutTrampoline.this, PcView.class); - i.setAction(Intent.ACTION_MAIN); - i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); - intentStack.add(i); - - // Take this intent's data and create an intent to start the app view - i = new Intent(getIntent()); - i.setClass(AppViewShortcutTrampoline.this, AppView.class); - intentStack.add(i); - - // If a game is running, we'll make the stream the top level activity - if (details.runningGameId != 0) { - intentStack.add(ServerHelper.createStartIntent(AppViewShortcutTrampoline.this, - new NvApp("app", details.runningGameId, false), details, managerBinder)); - } - - // Now start the activities - startActivities(intentStack.toArray(new Intent[]{})); - } - else if (details.state == ComputerDetails.State.OFFLINE) { - // Computer offline - display an error dialog - Dialog.displayDialog(AppViewShortcutTrampoline.this, - getResources().getString(R.string.conn_error_title), - getResources().getString(R.string.error_pc_offline), - true); - } - - // We don't want any more callbacks from now on, so go ahead - // and unbind from the service - if (managerBinder != null) { - managerBinder.stopPolling(); - unbindService(serviceConnection); - managerBinder = null; - } - } - }); - } - } - }); - } - }.start(); - } - - public void onServiceDisconnected(ComponentName className) { - managerBinder = null; - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - UiHelper.notifyNewRootView(this); - - uuidString = getIntent().getStringExtra(UUID_EXTRA); - - // Bind to the computer manager service - bindService(new Intent(this, ComputerManagerService.class), serviceConnection, - Service.BIND_AUTO_CREATE); - - blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title), - getResources().getString(R.string.applist_connect_msg), true); - } - - @Override - protected void onPause() { - super.onPause(); - - if (blockingLoadSpinner != null) { - blockingLoadSpinner.dismiss(); - blockingLoadSpinner = null; - } - - Dialog.closeDialogs(); - - if (managerBinder != null) { - managerBinder.stopPolling(); - unbindService(serviceConnection); - managerBinder = null; - } - - finish(); - } -} diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index 5c7c4458..d04a9e61 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -113,6 +113,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { private final static int DELETE_ID = 5; private final static int RESUME_ID = 6; private final static int QUIT_ID = 7; + private final static int VIEW_DETAILS_ID = 8; private void initializeViews() { setContentView(R.layout.activity_pc_view); @@ -333,6 +334,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { // it with delete which actually work menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc)); } + menu.add(Menu.NONE, VIEW_DETAILS_ID, 5, getResources().getString(R.string.pcview_menu_details)); } @Override @@ -603,6 +605,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { }, null); return true; + case VIEW_DETAILS_ID: + Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false); + return true; + default: return super.onContextItemSelected(item); } diff --git a/app/src/main/java/com/limelight/ShortcutTrampoline.java b/app/src/main/java/com/limelight/ShortcutTrampoline.java new file mode 100644 index 00000000..a80e0213 --- /dev/null +++ b/app/src/main/java/com/limelight/ShortcutTrampoline.java @@ -0,0 +1,263 @@ +package com.limelight; + +import android.app.Activity; +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; + +import com.limelight.computers.ComputerManagerListener; +import com.limelight.computers.ComputerManagerService; +import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.NvApp; +import com.limelight.nvstream.http.PairingManager; +import com.limelight.utils.Dialog; +import com.limelight.utils.ServerHelper; +import com.limelight.utils.SpinnerDialog; +import com.limelight.utils.UiHelper; + +import java.util.ArrayList; +import java.util.UUID; + +public class ShortcutTrampoline extends Activity { + private String uuidString; + private String appIdString; + private ArrayList intentStack = new ArrayList<>(); + + private ComputerDetails computer; + private SpinnerDialog blockingLoadSpinner; + + public final static String APP_ID_EXTRA = "AppId"; + + private ComputerManagerService.ComputerManagerBinder managerBinder; + private final ServiceConnection serviceConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder binder) { + final ComputerManagerService.ComputerManagerBinder localBinder = + ((ComputerManagerService.ComputerManagerBinder)binder); + + // Wait in a separate thread to avoid stalling the UI + new Thread() { + @Override + public void run() { + // Wait for the binder to be ready + localBinder.waitForReady(); + + // Now make the binder visible + managerBinder = localBinder; + + // Get the computer object + computer = managerBinder.getComputer(UUID.fromString(uuidString)); + + if (computer == null) { + Dialog.displayDialog(ShortcutTrampoline.this, + getResources().getString(R.string.conn_error_title), + getResources().getString(R.string.scut_pc_not_found), + true); + + if (blockingLoadSpinner != null) { + blockingLoadSpinner.dismiss(); + blockingLoadSpinner = null; + } + + if (managerBinder != null) { + unbindService(serviceConnection); + managerBinder = null; + } + + return; + } + + // Force CMS to repoll this machine + managerBinder.invalidateStateForComputer(computer.uuid); + + // Start polling + managerBinder.startPolling(new ComputerManagerListener() { + @Override + public void notifyComputerUpdated(final ComputerDetails details) { + // Don't care about other computers + if (!details.uuid.toString().equalsIgnoreCase(uuidString)) { + return; + } + + if (details.state != ComputerDetails.State.UNKNOWN) { + runOnUiThread(new Runnable() { + @Override + public void run() { + // Stop showing the spinner + if (blockingLoadSpinner != null) { + blockingLoadSpinner.dismiss(); + blockingLoadSpinner = null; + } + + // If the managerBinder was destroyed before this callback, + // just finish the activity. + if (managerBinder == null) { + finish(); + return; + } + + if (details.state == ComputerDetails.State.ONLINE && details.pairState == PairingManager.PairState.PAIRED) { + + // Launch game if provided app ID, otherwise launch app view + if (appIdString != null && appIdString.length() > 0) { + if (details.runningGameId == 0 || details.runningGameId == Integer.parseInt(appIdString)) { + intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this, + new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder)); + + // Close this activity + finish(); + + // Now start the activities + startActivities(intentStack.toArray(new Intent[]{})); + } else { + UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() { + @Override + public void run() { + intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this, + new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder)); + + // Close this activity + finish(); + + // Now start the activities + startActivities(intentStack.toArray(new Intent[]{})); + } + }, new Runnable() { + @Override + public void run() { + // Close this activity + finish(); + } + }); + } + } else { + // Close this activity + finish(); + + // Add the PC view at the back (and clear the task) + Intent i; + i = new Intent(ShortcutTrampoline.this, PcView.class); + i.setAction(Intent.ACTION_MAIN); + i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + intentStack.add(i); + + // Take this intent's data and create an intent to start the app view + i = new Intent(getIntent()); + i.setClass(ShortcutTrampoline.this, AppView.class); + intentStack.add(i); + + // If a game is running, we'll make the stream the top level activity + if (details.runningGameId != 0) { + intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this, + new NvApp("app", details.runningGameId, false), details, managerBinder)); + } + + // Now start the activities + startActivities(intentStack.toArray(new Intent[]{})); + } + + } + else if (details.state == ComputerDetails.State.OFFLINE) { + // Computer offline - display an error dialog + Dialog.displayDialog(ShortcutTrampoline.this, + getResources().getString(R.string.conn_error_title), + getResources().getString(R.string.error_pc_offline), + true); + } else if (details.pairState != PairingManager.PairState.PAIRED) { + // Computer not paired - display an error dialog + Dialog.displayDialog(ShortcutTrampoline.this, + getResources().getString(R.string.conn_error_title), + getResources().getString(R.string.scut_not_paired), + true); + } + + // We don't want any more callbacks from now on, so go ahead + // and unbind from the service + if (managerBinder != null) { + managerBinder.stopPolling(); + unbindService(serviceConnection); + managerBinder = null; + } + } + }); + } + } + }); + } + }.start(); + } + + public void onServiceDisconnected(ComponentName className) { + managerBinder = null; + } + }; + + protected boolean validateInput() { + // Validate UUID + try { + UUID.fromString(uuidString); + } catch (IllegalArgumentException ex) { + Dialog.displayDialog(ShortcutTrampoline.this, + getResources().getString(R.string.conn_error_title), + getResources().getString(R.string.scut_invalid_uuid), + true); + return false; + } + + // Validate App ID (if provided) + if (appIdString != null && !appIdString.isEmpty()) { + try { + Integer.parseInt(appIdString); + } catch (NumberFormatException ex) { + Dialog.displayDialog(ShortcutTrampoline.this, + getResources().getString(R.string.conn_error_title), + getResources().getString(R.string.scut_invalid_app_id), + true); + return false; + } + } + + return true; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + UiHelper.notifyNewRootView(this); + + uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA); + appIdString = getIntent().getStringExtra(APP_ID_EXTRA); + + if (validateInput()) { + // Bind to the computer manager service + bindService(new Intent(this, ComputerManagerService.class), serviceConnection, + Service.BIND_AUTO_CREATE); + + blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title), + getResources().getString(R.string.applist_connect_msg), true); + } + } + + @Override + protected void onStop() { + super.onStop(); + + if (blockingLoadSpinner != null) { + blockingLoadSpinner.dismiss(); + blockingLoadSpinner = null; + } + + Dialog.closeDialogs(); + + if (managerBinder != null) { + managerBinder.stopPolling(); + unbindService(serviceConnection); + managerBinder = null; + } + + finish(); + } +} diff --git a/app/src/main/java/com/limelight/utils/ShortcutHelper.java b/app/src/main/java/com/limelight/utils/ShortcutHelper.java index c02f0c1c..e47509a5 100644 --- a/app/src/main/java/com/limelight/utils/ShortcutHelper.java +++ b/app/src/main/java/com/limelight/utils/ShortcutHelper.java @@ -5,13 +5,15 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; +import android.graphics.Bitmap; import android.graphics.drawable.Icon; import android.os.Build; import com.limelight.AppView; -import com.limelight.AppViewShortcutTrampoline; +import com.limelight.ShortcutTrampoline; import com.limelight.R; import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.NvApp; import java.util.Collections; import java.util.LinkedList; @@ -80,8 +82,7 @@ public class ShortcutHelper { public void reportShortcutUsed(String id) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - ShortcutInfo sinfo = getInfoForId(id); - if (sinfo != null) { + if (getInfoForId(id) != null) { sm.reportShortcutUsed(id); } } @@ -89,7 +90,7 @@ public class ShortcutHelper { public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - Intent i = new Intent(context, AppViewShortcutTrampoline.class); + Intent i = new Intent(context, ShortcutTrampoline.class); i.putExtra(AppView.NAME_EXTRA, computerName); i.putExtra(AppView.UUID_EXTRA, computerUuid); i.setAction(Intent.ACTION_DEFAULT); @@ -127,10 +128,42 @@ public class ShortcutHelper { createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd); } + @TargetApi(Build.VERSION_CODES.O) + public boolean createPinnedGameShortcut(String id, Bitmap iconBits, String computerName, String computerUuid, String appName, String appId) { + if (sm.isRequestPinShortcutSupported()) { + Icon appIcon; + Intent i = new Intent(context, ShortcutTrampoline.class); + + i.putExtra(AppView.NAME_EXTRA, computerName); + i.putExtra(AppView.UUID_EXTRA, computerUuid); + i.putExtra(ShortcutTrampoline.APP_ID_EXTRA, appId); + i.setAction(Intent.ACTION_DEFAULT); + + if (iconBits != null) { + appIcon = Icon.createWithAdaptiveBitmap(iconBits); + } else { + appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut); + } + + ShortcutInfo sInfo = new ShortcutInfo.Builder(context, id) + .setIntent(i) + .setShortLabel(appName + " (" + computerName + ")") + .setIcon(appIcon) + .build(); + + return sm.requestPinShortcut(sInfo, null); + } else { + return false; + } + } + + public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) { + return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid.toString(), app.getAppName(), Integer.valueOf(app.getAppId()).toString()); + } + public void disableShortcut(String id, CharSequence reason) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - ShortcutInfo sinfo = getInfoForId(id); - if (sinfo != null) { + if (getInfoForId(id) != null) { sm.disableShortcuts(Collections.singletonList(id), reason); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 62c8ac90..6d0ddc62 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,6 +6,9 @@ PC deleted PC not paired + PC not found + Provided PC is not valid + Provided App is not valid Help Viewer @@ -17,6 +20,7 @@ Unpair Send Wake-On-LAN request Delete PC + View Details Pairing… @@ -55,6 +59,7 @@ Video Settings Reset Your device\'s video decoder continues to crash at your selected streaming settings. Your streaming settings have been reset to default. USB access is prohibited by your device administrator. Check your Knox or MDM settings. + Your current launcher does not allow for creating pinned shortcuts. Establishing Connection @@ -76,6 +81,7 @@ Yes No Lost connection to PC + Details Help @@ -85,6 +91,8 @@ Quit Session Quit Current Game and Start Cancel + View Details + Create Shortcut App List Refreshing apps… Error @@ -93,6 +101,7 @@ Successfully quit Failed to quit Are you sure you want to quit the running app? All unsaved data will be lost. + App ID: Add PC Manually