diff --git a/app/build.gradle b/app/build.gradle index d077a313..bc2d83b4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,12 +4,12 @@ import org.apache.tools.ant.taskdefs.condition.Os apply plugin: 'com.android.application' android { - compileSdkVersion 24 - buildToolsVersion "24.0.3" + compileSdkVersion 25 + buildToolsVersion "25.0.0" defaultConfig { minSdkVersion 16 - targetSdkVersion 24 + targetSdkVersion 25 versionName "4.7.2" versionCode = 106 diff --git a/app/libs/limelight-common.jar b/app/libs/limelight-common.jar index a7a5eb05..002770b3 100644 Binary files a/app/libs/limelight-common.jar and b/app/libs/limelight-common.jar differ diff --git a/app/src/main/java/com/limelight/AppView.java b/app/src/main/java/com/limelight/AppView.java index f567cfe5..a4e75a67 100644 --- a/app/src/main/java/com/limelight/AppView.java +++ b/app/src/main/java/com/limelight/AppView.java @@ -11,12 +11,14 @@ import com.limelight.grid.AppGridAdapter; import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvHTTP; +import com.limelight.nvstream.http.PairingManager; import com.limelight.preferences.PreferenceConfiguration; import com.limelight.ui.AdapterFragment; import com.limelight.ui.AdapterFragmentCallbacks; import com.limelight.utils.CacheHelper; import com.limelight.utils.Dialog; import com.limelight.utils.ServerHelper; +import com.limelight.utils.ShortcutHelper; import com.limelight.utils.SpinnerDialog; import com.limelight.utils.UiHelper; @@ -43,14 +45,16 @@ import android.widget.AdapterView.AdapterContextMenuInfo; public class AppView extends Activity implements AdapterFragmentCallbacks { private AppGridAdapter appGridAdapter; private String uuidString; + private ShortcutHelper shortcutHelper; private ComputerDetails computer; private ComputerManagerService.ApplistPoller poller; - private SpinnerDialog blockingLoadSpinner; + private SpinnerDialog blockingLoadSpinner, blockingServerinfoSpinner; private String lastRawApplist; private int lastRunningAppId; private boolean suspendGridUpdates; private boolean inForeground; + private boolean launchedFromShortcut; private final static int START_OR_RESUME_ID = 1; private final static int QUIT_ID = 2; @@ -59,6 +63,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { public final static String NAME_EXTRA = "Name"; public final static String UUID_EXTRA = "UUID"; + public final static String SHORTCUT_EXTRA = "Shortcut"; private ComputerManagerService.ComputerManagerBinder managerBinder; private final ServiceConnection serviceConnection = new ServiceConnection() { @@ -132,7 +137,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { managerBinder.startPolling(new ComputerManagerListener() { @Override - public void notifyComputerUpdated(ComputerDetails details) { + public void notifyComputerUpdated(final ComputerDetails details) { // Do nothing if updates are suspended if (suspendGridUpdates) { return; @@ -157,6 +162,48 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { return; } + // Close immediately if the PC is no longer paired + if (details.state == ComputerDetails.State.ONLINE && details.pairState != PairingManager.PairState.PAIRED) { + AppView.this.runOnUiThread(new Runnable() { + @Override + public void run() { + // Disable shortcuts referencing this PC for now + shortcutHelper.disableShortcut(details.uuid.toString(), + getResources().getString(R.string.scut_not_paired)); + + // Display a toast to the user and quit the activity + Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show(); + finish(); + } + }); + + return; + } + + if (launchedFromShortcut) { + if (details.state == ComputerDetails.State.ONLINE) { + if (blockingServerinfoSpinner != null) { + blockingServerinfoSpinner.dismiss(); + blockingServerinfoSpinner = null; + } + + if (details.runningGameId != 0) { + AppView.this.runOnUiThread(new Runnable() { + @Override + public void run() { + // We have to finish this activity here otherwise we'll get into a loop + // when the user hits back + finish(); + + // When launched from shortcut, resume the running game + ServerHelper.doStart(AppView.this, new NvApp("app", details.runningGameId), computer, managerBinder); + } + }); + return; + } + } + } + // App list is the same or empty if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) { @@ -208,6 +255,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + shortcutHelper = new ShortcutHelper(this); + String locale = PreferenceConfiguration.readPreferences(this).language; if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { Configuration config = new Configuration(getResources().getConfiguration()); @@ -221,6 +270,15 @@ public class AppView extends Activity implements AdapterFragmentCallbacks { uuidString = getIntent().getStringExtra(UUID_EXTRA); + launchedFromShortcut = getIntent().getBooleanExtra(SHORTCUT_EXTRA, false); + if (launchedFromShortcut) { + // Display blocking loading spinner + blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title), + getResources().getString(R.string.applist_connect_msg), true); + } + + shortcutHelper.reportShortcutUsed(uuidString); + String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA); TextView label = (TextView) findViewById(R.id.appListText); setTitle(labelText); diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index f8c2f858..870fc124 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -24,6 +24,7 @@ import com.limelight.ui.AdapterFragment; import com.limelight.ui.AdapterFragmentCallbacks; import com.limelight.utils.Dialog; import com.limelight.utils.ServerHelper; +import com.limelight.utils.ShortcutHelper; import com.limelight.utils.UiHelper; import android.app.Activity; @@ -52,6 +53,7 @@ import android.widget.AdapterView.AdapterContextMenuInfo; public class PcView extends Activity implements AdapterFragmentCallbacks { private RelativeLayout noPcFoundLayout; private PcGridAdapter pcGridAdapter; + private ShortcutHelper shortcutHelper; private ComputerManagerService.ComputerManagerBinder managerBinder; private boolean freezeUpdates, runningPolling, inForeground; private final ServiceConnection serviceConnection = new ServiceConnection() { @@ -142,6 +144,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + shortcutHelper = new ShortcutHelper(this); + String locale = PreferenceConfiguration.readPreferences(this).language; if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) { Configuration config = new Configuration(getResources().getConfiguration()); @@ -334,6 +338,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { else if (pairState == PairingManager.PairState.FAILED) { message = getResources().getString(R.string.pair_fail); } + else if (pairState == PairingManager.PairState.ALREADY_IN_PROGRESS) { + message = getResources().getString(R.string.pair_already_in_progress); + } else if (pairState == PairingManager.PairState.PAIRED) { // Just navigate to the app view without displaying a toast message = null; @@ -486,6 +493,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { return; } + shortcutHelper.createAppViewShortcut(computer.uuid.toString(), computer); + Intent i = new Intent(this, AppView.class); i.putExtra(AppView.NAME_EXTRA, computer.name); i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString()); @@ -558,6 +567,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); if (details.equals(computer.details)) { + // Disable or delete shortcuts referencing this PC + shortcutHelper.disableShortcut(details.uuid.toString(), + getResources().getString(R.string.scut_deleted_pc)); + pcGridAdapter.removeComputer(computer); pcGridAdapter.notifyDataSetChanged(); diff --git a/app/src/main/java/com/limelight/utils/ShortcutHelper.java b/app/src/main/java/com/limelight/utils/ShortcutHelper.java new file mode 100644 index 00000000..16814796 --- /dev/null +++ b/app/src/main/java/com/limelight/utils/ShortcutHelper.java @@ -0,0 +1,115 @@ +package com.limelight.utils; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.os.Build; + +import com.limelight.AppView; +import com.limelight.R; +import com.limelight.nvstream.http.ComputerDetails; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +public class ShortcutHelper { + + private final ShortcutManager sm; + private final Context context; + + public ShortcutHelper(Context context) { + this.context = context; + if (Build.VERSION.SDK_INT >= 25) { + sm = context.getSystemService(ShortcutManager.class); + } + else { + sm = null; + } + } + + @TargetApi(25) + private void reapShortcutsForDynamicAdd() { + List dynamicShortcuts = sm.getDynamicShortcuts(); + while (dynamicShortcuts.size() >= sm.getMaxShortcutCountPerActivity()) { + ShortcutInfo maxRankShortcut = dynamicShortcuts.get(0); + for (ShortcutInfo scut : dynamicShortcuts) { + if (maxRankShortcut.getRank() < scut.getRank()) { + maxRankShortcut = scut; + } + } + sm.removeDynamicShortcuts(Arrays.asList(maxRankShortcut.getId())); + } + } + + @TargetApi(25) + private List getAllShortcuts() { + LinkedList list = new LinkedList<>(); + list.addAll(sm.getDynamicShortcuts()); + list.addAll(sm.getPinnedShortcuts()); + return list; + } + + @TargetApi(25) + private ShortcutInfo getInfoForId(String id) { + List shortcuts = getAllShortcuts(); + + for (ShortcutInfo info : shortcuts) { + if (info.getId().equals(id)) { + return info; + } + } + + return null; + } + + public void reportShortcutUsed(String id) { + if (Build.VERSION.SDK_INT >= 25) { + ShortcutInfo sinfo = getInfoForId(id); + if (sinfo != null) { + sm.reportShortcutUsed(id); + } + } + } + + public void createAppViewShortcut(String id, ComputerDetails details) { + if (Build.VERSION.SDK_INT >= 25) { + Intent i = new Intent(context, AppView.class); + i.putExtra(AppView.NAME_EXTRA, details.name); + i.putExtra(AppView.UUID_EXTRA, details.uuid.toString()); + i.putExtra(AppView.SHORTCUT_EXTRA, true); + i.setAction(Intent.ACTION_DEFAULT); + + ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id) + .setIntent(i) + .setShortLabel(details.name) + .setLongLabel(details.name) + .build(); + + ShortcutInfo existingSinfo = getInfoForId(id); + if (existingSinfo != null) { + // Update in place + sm.updateShortcuts(Arrays.asList(sinfo)); + sm.enableShortcuts(Arrays.asList(id)); + } + else { + // Reap shortcuts to make space for this new one + reapShortcutsForDynamicAdd(); + + // Add the new shortcut + sm.addDynamicShortcuts(Arrays.asList(sinfo)); + } + } + } + + public void disableShortcut(String id, CharSequence reason) { + if (Build.VERSION.SDK_INT >= 25) { + ShortcutInfo sinfo = getInfoForId(id); + if (sinfo != null) { + sm.disableShortcuts(Arrays.asList(id), reason); + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d7d549aa..ade1f97c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,8 @@ + + PC deleted + PC not paired View Game List @@ -16,6 +19,7 @@ Please enter the following PIN on the target PC: Incorrect PIN Pairing failed + Pairing already in progress Computer is online @@ -63,6 +67,7 @@ Apps on + Connecting to PC… Resume Session Quit Session Quit Current Game and Start diff --git a/build.gradle b/build.gradle index 305dc055..e7e3c60b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.0' + classpath 'com.android.tools.build:gradle:2.2.2' } }