package com.atomjack.vcfp.activities; import android.Manifest; import android.animation.AnimatorInflater; import android.animation.AnimatorSet; import android.app.AlertDialog; import android.app.Dialog; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.AnimationDrawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.speech.tts.TextToSpeech; import android.support.design.widget.NavigationView; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v4.content.ContextCompat; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.media.MediaRouteSelector; import android.support.v7.media.MediaRouter; import android.support.v7.widget.SwitchCompat; import android.support.v7.widget.Toolbar; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.CheckedTextView; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.SeekBar; import android.widget.TextView; import com.android.vending.billing.IabHelper; import com.android.vending.billing.IabResult; import com.android.vending.billing.Purchase; import com.atomjack.shared.NewLogger; import com.atomjack.shared.PlayerState; import com.atomjack.shared.Preferences; import com.atomjack.shared.SendToDataLayerThread; import com.atomjack.shared.UriDeserializer; import com.atomjack.shared.UriSerializer; import com.atomjack.shared.WearConstants; import com.atomjack.vcfp.BuildConfig; import com.atomjack.vcfp.Feedback; import com.atomjack.vcfp.FutureRunnable; import com.atomjack.vcfp.MediaOptionsDialog; import com.atomjack.vcfp.NetworkMonitor; import com.atomjack.vcfp.R; import com.atomjack.vcfp.Utils; import com.atomjack.vcfp.VoiceControlForPlexApplication; import com.atomjack.vcfp.adapters.PlexListAdapter; import com.atomjack.vcfp.fragments.CastPlayerFragment; import com.atomjack.vcfp.fragments.MainFragment; import com.atomjack.vcfp.fragments.MusicPlayerFragment; import com.atomjack.vcfp.fragments.PlayerFragment; import com.atomjack.vcfp.fragments.PlexPlayerFragment; import com.atomjack.vcfp.fragments.SetupFragment; import com.atomjack.vcfp.interfaces.ActivityListener; import com.atomjack.vcfp.interfaces.MusicPlayerListener; import com.atomjack.vcfp.interfaces.MusicServiceListener; import com.atomjack.vcfp.interfaces.PlexSubscriptionListener; import com.atomjack.vcfp.interfaces.ScanHandler; import com.atomjack.vcfp.model.Pin; import com.atomjack.vcfp.model.PlexClient; import com.atomjack.vcfp.model.PlexDevice; import com.atomjack.vcfp.model.PlexMedia; import com.atomjack.vcfp.model.PlexServer; import com.atomjack.vcfp.model.PlexTrack; import com.atomjack.vcfp.model.PlexUser; import com.atomjack.vcfp.model.Stream; import com.atomjack.vcfp.net.PlexHttpClient; import com.atomjack.vcfp.net.PlexHttpUserHandler; import com.atomjack.vcfp.net.PlexPinResponseHandler; import com.atomjack.vcfp.services.LocalMusicService; import com.atomjack.vcfp.services.PlexScannerService; import com.atomjack.vcfp.services.SubscriptionService; import com.cubeactive.martin.inscription.WhatsNewDialog; import com.github.aakira.expandablelayout.ExpandableRelativeLayout; import com.google.android.gms.cast.CastDevice; import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.wearable.DataMap; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.splunk.mint.Mint; import org.honorato.multistatetogglebutton.MultiStateToggleButton; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import tourguide.tourguide.ChainTourGuide; import tourguide.tourguide.Overlay; import tourguide.tourguide.Sequence; import tourguide.tourguide.ToolTip; public class MainActivity extends AppCompatActivity implements VoiceControlForPlexApplication.NetworkChangeListener, ActivityListener, TextToSpeech.OnInitListener, MusicPlayerListener, CastPlayerFragment.CastPlayerFragmentListener, MediaOptionsDialog.StreamChangeListener { public final static int RESULT_VOICE_FEEDBACK_SELECTED = 0; public final static int RESULT_TASKER_PROJECT_IMPORTED = 1; public final static int RESULT_SHORTCUT_CREATED = 2; public final static String ACTION_SHOW_NOW_PLAYING = "com.atomjack.vcfp.action_show_now_playing"; public final static int SERVER_SCAN_INTERVAL = 1000*60*5; // scan for servers every 5 minutes private Handler handler; public final static String BUGSENSE_APIKEY = "879458d0"; private ArrayList<String> availableVoices; private boolean settingErrorFeedback = false; private TextToSpeech tts; private DrawerLayout mDrawer; private Toolbar toolbar; private NavigationView navigationViewMain; private ActionBarDrawerToggle drawerToggle; private LocalMusicService localMusicService; private boolean musicPlayerIsBound = false; private Intent musicServiceIntent; private SubscriptionService subscriptionService; private boolean subscriptionServiceIsBound = false; private Intent subscriptionServiceIntent; private String authToken; protected static final int REQUEST_WRITE_STORAGE = 112; // Whether or not we received device logs from a wear device. This will allow a timer to be run in case wear support has // been purchased, but no wear device is paired. When this happens, we'll go ahead and email just the mobile device's logs // private boolean receivedWearLogsResponse = false; // the currently selected server and client private PlexServer server; private PlexClient client; private TextView deviceSelectNoDevicesFound; private CheckBox deviceListResume; private FutureRunnable fetchPinTask; public Feedback feedback; MediaRouter mMediaRouter; MediaRouterCallback mMediaRouterCallback; MediaRouteSelector mMediaRouteSelector; protected PlexClient postChromecastPurchaseClient = null; protected Runnable postChromecastPurchaseAction = null; protected Gson gsonWrite = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriSerializer()) .create(); protected Gson gsonRead = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriDeserializer()) .create(); private boolean userIsInteracting; private ChainTourGuide tourGuideHandler; // This will be set to true if wear support is purchased immediately upon launch before // initial setup has been done, to delay the wear purchase popup until after that has finished private boolean showWearPurchase = false; // First time setup private boolean doingFirstTimeSetup = false; // The next two booleans will be set to true when their respective scans are finished. This will ensure // that when whichever of the two finishes last, the screen will refresh and first time setup will be done. private boolean firstTimeSetupServerScanFinished = false; private boolean firstTimeSetupClientScanFinished = false; Preferences prefs; private AlertDialog alertDialog; private MenuItem castIconMenuItem; protected Dialog deviceSelectDialog = null; private ProgressBar serverListRefreshSpinner; private ImageView serverListRefreshButton; private PlayerFragment playerFragment; private MainFragment mainFragment; private MusicPlayerFragment musicPlayerFragment; public enum NetworkState { DISCONNECTED, WIFI, MOBILE; public static NetworkState getCurrentNetworkState(Context context) { NetworkState currentNetworkState = NetworkState.DISCONNECTED; ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); if(activeNetwork != null) { if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) currentNetworkState = NetworkState.MOBILE; else if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) currentNetworkState = NetworkState.WIFI; } return currentNetworkState; } }; protected NetworkState currentNetworkState; protected NetworkMonitor networkMonitor; private NewLogger logger; LinearLayout navigationFooter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); logger = new NewLogger(this); logger.d("onCreate"); // This will enable the UI to be updated (Wear Support hidden/Wear Options shown) // once inventory is queried via Google, if wear support has been purchased VoiceControlForPlexApplication.getInstance().setOnHasWearActivity(this); bindSubscriptionService(); if(savedInstanceState != null) { client = savedInstanceState.getParcelable(com.atomjack.shared.Intent.EXTRA_CLIENT); playerFragment = (PlayerFragment)getSupportFragmentManager().getFragment(savedInstanceState, com.atomjack.shared.Intent.EXTRA_PLAYER_FRAGMENT); musicPlayerFragment = (MusicPlayerFragment) getSupportFragmentManager().getFragment(savedInstanceState, com.atomjack.shared.Intent.EXTRA_MUSIC_PLAYER_FRAGMENT); logger.d("musicPlayerFragment: %s", musicPlayerFragment); musicPlayerIsBound = savedInstanceState.getBoolean(com.atomjack.shared.Intent.EXTRA_MUSIC_PLAYER_IS_BOUND); } else { logger.d("savedInstanceState is null"); } prefs = VoiceControlForPlexApplication.getInstance().prefs; if(feedback == null) feedback = new Feedback(this); authToken = prefs.getString(Preferences.AUTHENTICATION_TOKEN); networkMonitor = new NetworkMonitor(this); VoiceControlForPlexApplication.getInstance().setNetworkChangeListener(this); currentNetworkState = NetworkState.getCurrentNetworkState(this); doingFirstTimeSetup = !prefs.get(Preferences.FIRST_TIME_SETUP_COMPLETED, false); // If auth token has already been set even though first time setup isn't complete, assume user // has upgraded if(authToken != null && doingFirstTimeSetup) { doingFirstTimeSetup = false; prefs.put(Preferences.FIRST_TIME_SETUP_COMPLETED, true); } // If plex email hasn't been saved, fetch it and refresh the navigation drawer when done. Previous versions // of the app were not saving the email, which is needed for the user icon. checkForMissingPlexEmail(); setContentView(R.layout.main); init(savedInstanceState != null); if(!doingFirstTimeSetup) doAutomaticDeviceScan(); } @Override public void onBackPressed() { Intent intent = new Intent(); intent.setAction(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); startActivity(intent); } private PlexSubscriptionListener plexSubscriptionListener = new PlexSubscriptionListener() { @Override public void onSubscribed(final PlexClient client, boolean showFeedback) { logger.d("onSubscribed"); prefs.put(Preferences.SUBSCRIBED_CLIENT, gsonWrite.toJson(client)); try { setCastIconActive(); } catch (Exception e) { e.printStackTrace(); } if(showFeedback) feedback.m(String.format(getString(R.string.connected_to2), client.name)); } @Override public void onTimeUpdate(PlayerState state, int seconds) { if(playerFragment != null) { playerFragment.setPosition(seconds); playerFragment.setState(state); handler.removeCallbacks(autoDisconnectPlayerTimer); } else logger.d("Got time update of %d seconds, but for some reason playerFragment is null", seconds); } @Override public void onMediaChanged(PlexMedia media, PlayerState state) { logger.d("onMediaChanged: %s %s", media.getTitle(), state); handler.removeCallbacks(autoDisconnectPlayerTimer); playerFragment.mediaChanged(media); VoiceControlForPlexApplication.getInstance().sendWearPlaybackChange(state, media); } @Override public void onPlayStarted(PlexMedia media, ArrayList<? extends PlexMedia> playlist, PlayerState state) { logger.d("onPlayStarted: %s", media.getTitle()); handler.removeCallbacks(autoDisconnectPlayerTimer); int layout = getLayoutForMedia(media, state); if(layout != -1) { playerFragment.init(layout, client, media, playlist, false); switchToPlayerFragment(); } VoiceControlForPlexApplication.getInstance().sendWearPlaybackChange(state, media); } @Override public void onStateChanged(PlexMedia media, PlayerState state) { logger.d("onStateChanged: %s", state); handler.removeCallbacks(autoDisconnectPlayerTimer); if(playerFragment != null && playerFragment.isVisible()) { if(state == PlayerState.STOPPED) { VoiceControlForPlexApplication.getInstance().cancelNotification(); switchToMainFragment(); // We've stopped, so if we're still subscribed and the client we stopped playing to is different from the default client, unsubscribe, since // the main screen UI says it is ready to cast to the default client, not the client we just got finished playing to. if(subscriptionService.isSubscribed() && !subscriptionService.getClient().equals(gsonRead.fromJson(prefs.get(Preferences.CLIENT, ""), PlexClient.class))) subscriptionService.unsubscribe(); } else { playerFragment.setState(state); } } else { logger.d("Got state change to %s, but for some reason playerFragment is null", state); } VoiceControlForPlexApplication.getInstance().sendWearPlaybackChange(state, media); } @Override public void onSubscribeError(String error) { logger.d("onSubscribeError: %s", error); setCastIconInactive(); feedback.e(error == null ? String.format(getString(R.string.cast_connect_error), client.name) : error); } @Override public void onUnsubscribed() { logger.d("unsubscribed"); setCastIconInactive(); VoiceControlForPlexApplication.getInstance().cancelNotification(); prefs.remove(Preferences.SUBSCRIBED_CLIENT); switchToMainFragment(); if(VoiceControlForPlexApplication.getInstance().hasWear()) { new SendToDataLayerThread(WearConstants.DISCONNECTED, MainActivity.this).start(); } feedback.m(R.string.disconnected); } }; private void switchToFragment(Fragment fragment) { getSupportFragmentManager().beginTransaction().replace(R.id.flContent, fragment).commitAllowingStateLoss(); } private void init() { init(false); } // There is an edge case that happens when voice control is triggered when the app is not currently running. The intent passed to // onCreate() directs the app to show the now playing media, however this intent will get sent again if an orientation change is // done after stopping playback, which will cause the now playing screen to show up again, when it shouldn't. If init() is passed // true for previouslyShutDown, we will skip showing the now playing screen. private void init(boolean previouslyShutDown) { handler = new Handler(); if(BuildConfig.USE_BUGSENSE) { Mint.disableNetworkMonitoring(); Mint.initAndStartSession(getApplicationContext(), BUGSENSE_APIKEY); } // Set a Toolbar to replace the ActionBar. toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout); Fragment fragment; if (!doingFirstTimeSetup) { setupMediaRouter(); server = gsonRead.fromJson(prefs.get(Preferences.SERVER, ""), PlexServer.class); if (server == null) server = PlexServer.getScanAllServer(); client = gsonRead.fromJson(prefs.get(Preferences.CLIENT, ""), PlexClient.class); setupNavigationDrawer(); // if(showWearPurchase) { showWearPurchase = false; showWearPurchase(); } logger.d("(init) Intent action: %s", getIntent().getAction()); Intent intent = getIntent(); if(intent.getAction() != null && getIntent().getAction().equals(ACTION_SHOW_NOW_PLAYING) && !previouslyShutDown) { handleShowNowPlayingIntent(intent); } else if(intent.getAction().equals(com.atomjack.shared.Intent.ACTION_PLAY_LOCAL) && !previouslyShutDown) { // this intent will only arrive when playing music. playing video will go to VideoPlayerActivity handlePlayLocalIntent(intent); } else { logger.d("Loading main fragment"); if(playerFragment != null) fragment = playerFragment; else if(musicPlayerFragment != null) fragment = musicPlayerFragment; else fragment = getMainFragment(); switchToFragment(fragment); if(prefs.get(Preferences.HAS_FINISHED_TUTORIAL1, false)) { // Only show the what's new dialog if this is not the first time the app is run showWhatsNewDialog(false); } else doTutorial(); } } else { WhatsNewDialog whatsNewDialog = new WhatsNewDialog(this); whatsNewDialog.updateLastShown(); fragment = new SetupFragment(); switchToFragment(fragment); } } private void setupMediaRouter() { if(mMediaRouter == null) { mMediaRouter = MediaRouter.getInstance(getApplicationContext()); mMediaRouteSelector = new MediaRouteSelector.Builder() .addControlCategory(CastMediaControlIntent.categoryForCast(BuildConfig.CHROMECAST_APP_ID)) .build(); mMediaRouterCallback = new MediaRouterCallback(); mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); } } private void doTutorial() { logger.d("cast button: %s", toolbar.findViewById(R.id.action_cast)); if(toolbar.findViewById(R.id.action_cast) == null) { handler.postDelayed(() -> { logger.d("doing tutorial again"); doTutorial(); }, 200); return; } prefs.put(Preferences.HAS_FINISHED_TUTORIAL1, true); ChainTourGuide castTour = ChainTourGuide.init(this) .setToolTip(new ToolTip().setDescription(getString(R.string.tutorial1_cast_button))) .playLater(toolbar.findViewById(R.id.action_cast)); ChainTourGuide navTour = ChainTourGuide.init(this) .setToolTip(new ToolTip().setDescription(getString(R.string.tutorial1_nav_button))) .playLater(getNavButtonView()); ChainTourGuide micTour = ChainTourGuide.init(this) .setToolTip(new ToolTip().setDescription(getString(R.string.tutorial1_mic_button))) .playLater(findViewById(R.id.mainMicButton)); Sequence sequence = new Sequence.SequenceBuilder() .add(castTour, navTour, micTour) .setDefaultOverlay(new Overlay() .setBackgroundColor(Color.parseColor("#aa000000")) .disableClick(true) .disableClickThroughHole(true) .setOnClickListener(v -> tourGuideHandler.next()) ).setDefaultPointer(null) .setContinueMethod(Sequence.ContinueMethod.OverlayListener) .build(); tourGuideHandler = ChainTourGuide.init(this).playInSequence(sequence); } private ImageButton getNavButtonView() { try { Class<?> toolbarClass = Toolbar.class; Field navButtonField = toolbarClass.getDeclaredField("mNavButtonView"); navButtonField.setAccessible(true); ImageButton navButtonView = (ImageButton) navButtonField.get(toolbar); return navButtonView; } catch (Exception e) { e.printStackTrace(); } return null; } private void showWhatsNewDialog(boolean force) { WhatsNewDialog whatsNewDialog = new WhatsNewDialog(this); whatsNewDialog.setStyle(String.format("body { background-color: %s; color: #ffffff; }" + "h1 { margin-left: 0px; font-size: 12pt; }" + "li { margin-left: 0px; font-size: 9pt; }" + "ul { padding-left: 30px; }" + ".summary { font-size: 9pt; color: #606060; display: block; clear: left; }" + ".date { font-size: 9pt; color: #606060; display: block; }", String.format("#%06X", (0xFFFFFF & ContextCompat.getColor(this, R.color.settings_popup_background))))); View customView = LayoutInflater.from(this).inflate(R.layout.popup_whatsnew, null, false); whatsNewDialog.setCustomView(customView); if(force) whatsNewDialog.forceShow(); else whatsNewDialog.show(); } @Override protected void onPause() { super.onPause(); logger.d("onPause"); handler.removeCallbacks(autoDisconnectPlayerTimer); VoiceControlForPlexApplication.applicationPaused(); if (isFinishing() && mMediaRouter != null) { mMediaRouter.removeCallback(mMediaRouterCallback); } } @Override protected void onStop() { super.onStop(); logger.d("onStop"); if(prefs.get(Preferences.SERVER_SCAN_FINISHED, true) == false) { Intent scannerIntent = new Intent(MainActivity.this, PlexScannerService.class); scannerIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); scannerIntent.setAction(PlexScannerService.CANCEL); startService(scannerIntent); } if(musicPlayerIsBound) getApplicationContext().unbindService(musicConnection); if(subscriptionServiceIsBound) { logger.d("unbinding subscriptions service"); subscriptionService.setListener(null); getApplicationContext().unbindService(subscriptionConnection); subscriptionServiceIsBound = false; } feedback.destroy(); handler.removeCallbacks(refreshServers); handler.removeCallbacks(refreshClients); if(mMediaRouter != null) mMediaRouter.removeCallback(mMediaRouterCallback); } @Override protected void onRestart() { super.onRestart(); logger.d("onRestart"); VoiceControlForPlexApplication.getInstance().refreshInAppInventory(); } private Runnable autoDisconnectPlayerTimer = new Runnable() { @Override public void run() { logger.d("Auto disconnecting player"); if((playerFragment != null && playerFragment.isVisible()) || (musicPlayerFragment != null && musicPlayerFragment.isVisible())) { VoiceControlForPlexApplication.getInstance().cancelNotification(); switchToMainFragment(); } } }; public void showPurchaseLocalMedia(MenuItem item) { showPurchaseLocalMedia(false); } public void showPurchaseLocalMedia(final boolean showPurchaseFromMenu) { AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = getLayoutInflater().inflate(R.layout.popup_localmedia_purchase_reminder, null); builder.setView(view); final AlertDialog dialog = builder.create(); TextView localMediaPurchaseReminderMessage = (TextView)view.findViewById(R.id.localMediaPurchaseReminderMessage); localMediaPurchaseReminderMessage.setText(String.format(getString(R.string.localmedia_purchase_reminder), VoiceControlForPlexApplication.getLocalmediaPrice())); Button localMediaPurchaseReminderOKButton = (Button)view.findViewById(R.id.localMediaPurchaseReminderOKButton); localMediaPurchaseReminderOKButton.setOnClickListener(v -> { dialog.dismiss(); dialog.cancel(); VoiceControlForPlexApplication.getInstance().getIabHelper().flagEndAsync(); VoiceControlForPlexApplication.getInstance().getIabHelper().launchPurchaseFlow(MainActivity.this, VoiceControlForPlexApplication.SKU_LOCALMEDIA, 10001, mPurchaseFinishedListener, VoiceControlForPlexApplication.SKU_TEST_PURCHASED == VoiceControlForPlexApplication.SKU_LOCALMEDIA ? VoiceControlForPlexApplication.getInstance().getEmailHash() : ""); }); Button localMediaPurchaseReminderNoButton = (Button)view.findViewById(R.id.localMediaPurchaseReminderNoButton); localMediaPurchaseReminderNoButton.setOnClickListener(v -> { dialog.dismiss(); if(showPurchaseFromMenu) { AlertDialog.Builder builder1 = new AlertDialog.Builder(MainActivity.this); View view1 = getLayoutInflater().inflate(R.layout.popup_localmedia_purchase_from_menu, null); builder1.setView(view1); TextView localMediaDisclaimer = (TextView) view1.findViewById(R.id.localMediaDisclaimer); localMediaDisclaimer.setText(R.string.localmedia_purchase_from_menu); final AlertDialog dialog1 = builder1.create(); Button localMediaDisclaimerButton = (Button) view1.findViewById(R.id.localMediaDisclaimerButton); localMediaDisclaimerButton.setOnClickListener(v1 -> { dialog1.dismiss(); prefs.put(Preferences.HAS_SHOWN_INITIAL_LOCALMEDIA_PURCHASE, true); localClientSelected(PlexClient.getLocalPlaybackClient()); }); dialog1.show(); } }); dialog.show(); } @Override protected void onResume() { super.onResume(); logger.d("onResume, interacting: %s", userIsInteracting); VoiceControlForPlexApplication.applicationResumed(); if(subscriptionServiceIsBound) subscriptionService.checkLastHeartbeat(); else if(!subscriptionConnection.binding) { bindSubscriptionService(); } if(musicPlayerIsBound) bindMusicPlayerService(); if(musicPlayerFragment != null && musicPlayerFragment.isVisible()) musicPlayerFragment.init(localMusicService.getTrack(), localMusicService.getPlaylist()); if(!doingFirstTimeSetup) { mDrawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); } else mDrawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); } private void doAutomaticDeviceScan() { if(BuildConfig.AUTO_REFRESH_DEVICES) { logger.d("Doing automatic device scan"); // Kick off a scan for servers, if it's been more than five minutes since the last one. // We'll do this every five, to keep the list up to date. Also, if the last server scan didn't // finish, kick off another one right now instead (another scan in 5 minutes will be queued up when that one finishes). int s = VoiceControlForPlexApplication.getInstance().getSecondsSinceLastServerScan(); logger.d("It's been %d seconds since last scan, last scan finished: %s", s, prefs.get(Preferences.SERVER_SCAN_FINISHED, true)); if (s >= (SERVER_SCAN_INTERVAL / 1000) || prefs.get(Preferences.SERVER_SCAN_FINISHED, true) == false) { logger.d("It's been more than 5 minutes since last scan, so scanning now."); refreshServers.run(); refreshClients.run(); } else { int d = (SERVER_SCAN_INTERVAL - (s * 1000)); logger.d("It's been less than 5 minutes since the last server scan, so doing another in %d ms", d); handler.removeCallbacks(refreshServers); handler.removeCallbacks(refreshClients); handler.postDelayed(refreshServers, d); handler.postDelayed(refreshClients, d); } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); logger.d("onActivityResult: %d, %d", requestCode, resultCode); // Pass on the activity result to the helper for handling if (VoiceControlForPlexApplication.getInstance().getIabHelper() == null || !VoiceControlForPlexApplication.getInstance().getIabHelper().handleActivityResult(requestCode, resultCode, data)) { if (requestCode == RESULT_VOICE_FEEDBACK_SELECTED) { if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) { // success, create the TTS instance availableVoices = data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES); Collections.sort(availableVoices); // Need this or else voice selection won't show up: tts = new TextToSpeech(this, this); } else { // missing data, install it Intent installIntent = new Intent(); installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA); startActivity(installIntent); } } else if (requestCode == RESULT_SHORTCUT_CREATED) { if (resultCode == RESULT_OK) { data.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); sendBroadcast(data); feedback.m(getString(R.string.shortcut_created)); } } } else { logger.d("onActivityResult handled by IABUtil."); } } public void mainLoadingBypassLogin(View v) { showFindingPlexClientsAndServers(); refreshServers.run(); refreshClients.run(); } public void showLogin(View v) { showLogin(); } public void showLogin(MenuItem item) { showLogin(); } public void showLogin() { PlexHttpClient.getPinCode(new PlexPinResponseHandler() { @Override public void onSuccess(Pin pin) { showPin(pin); } @Override public void onFailure(Throwable error) { error.printStackTrace(); showManualLogin(true); } }); } private void showPin(final Pin pin) { AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(MainActivity.this); View view = getLayoutInflater().inflate(R.layout.popup_plex_pin, null); alertDialogBuilder.setView(view); // create and show an alert dialog final AlertDialog pinAlert = alertDialogBuilder.create(); pinAlert.show(); TextView popupPlexPinMessage = (TextView)view.findViewById(R.id.popupPlexPinMessage); popupPlexPinMessage.setText(String.format(getString(R.string.pin_message), pin.code)); Button popupPlexPinManualButton = (Button)view.findViewById(R.id.popupPlexPinManualButton); popupPlexPinManualButton.setOnClickListener(v -> { pinAlert.dismiss(); fetchPinTask.getFuture().cancel(false); showManualLogin(); }); Button popupPlexPinCancelButton = (Button)view.findViewById(R.id.popupPlexPinCancelButton); popupPlexPinCancelButton.setOnClickListener(v -> { pinAlert.cancel(); fetchPinTask.getFuture().cancel(false); }); // Now set up a task to hit the below url (based on the "id" field returned in the above http POST) // every second. Once the user has entered the code on the plex website, the xml returned from the // below http GET will contain their authentication token. Once that is retrieved, save it, switch // the showLogin/logout buttons in the menu, and cancel the dialog. final Context context = MainActivity.this; fetchPinTask = new FutureRunnable() { @Override public void run() { PlexHttpClient.fetchPin(pin.id, new PlexPinResponseHandler() { @Override public void onSuccess(Pin pin) { if(pin.authToken != null) { authToken = pin.authToken; prefs.put(Preferences.AUTHENTICATION_TOKEN, authToken); PlexHttpClient.signin(authToken, new PlexHttpUserHandler() { @Override public void onSuccess(PlexUser user) { prefs.put(Preferences.PLEX_USERNAME, user.username); prefs.put(Preferences.PLEX_EMAIL, user.email); if(doingFirstTimeSetup) { showFindingPlexClientsAndServers(); refreshServers.run(); refreshClients.run(); } else { setupNavigationDrawer(); refreshServers(null); } setupMediaRouter(); } @Override public void onFailure(int statusCode) { // TODO: Handle failure } }); pinAlert.cancel(); Handler mainHandler = new Handler(context.getMainLooper()); mainHandler.post(() -> feedback.m(R.string.logged_in)); // We got the auth token, so cancel this task getFuture().cancel(false); } } @Override public void onFailure(Throwable error) { error.printStackTrace(); } }); } }; // Set up the schedule service and let fetchPinTask know of the Future object, so the task can cancel // itself once the authentication token is retrieved. ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); Future<?> future = executor.scheduleAtFixedRate(fetchPinTask, 0, 1000, TimeUnit.MILLISECONDS); fetchPinTask.setFuture(future); } // This gets called in first time setup, after the user has logged in private void showFindingPlexClientsAndServers() { LayoutInflater inflater = getLayoutInflater(); View layout = inflater.inflate(R.layout.search_popup, null); alertDialog = new AlertDialog.Builder(this) .setCancelable(false) .setView(layout) .create(); alertDialog.show(); } private void showManualLogin() { showManualLogin(false); } private void showManualLogin(boolean showPinError) { LayoutInflater layoutInflater = LayoutInflater.from(MainActivity.this); View view = layoutInflater.inflate(R.layout.login, null); AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setView(view); final EditText usernameInput = (EditText) view.findViewById(R.id.usernameInput); final EditText passwordInput = (EditText) view.findViewById(R.id.passwordInput); if(showPinError) { TextView loginPinError = (TextView) view.findViewById(R.id.loginPinError); loginPinError.setVisibility(View.VISIBLE); } final AlertDialog alertD = builder.create(); Button popupManualLoginPinButton = (Button)view.findViewById(R.id.popupManualLoginPinButton); popupManualLoginPinButton.setOnClickListener(v -> { alertD.dismiss(); showLogin(); }); Button popupManualLoginCancelButton = (Button)view.findViewById(R.id.popupManualLoginCancelButton); popupManualLoginCancelButton.setOnClickListener(v -> alertD.cancel()); Button popupManualLoginOKButton = (Button)view.findViewById(R.id.popupManualLoginOKButton); popupManualLoginOKButton.setOnClickListener(v -> PlexHttpClient.signin(usernameInput.getText().toString(), passwordInput.getText().toString(), new PlexHttpUserHandler() { @Override public void onSuccess(PlexUser user) { prefs.put(Preferences.AUTHENTICATION_TOKEN, user.authenticationToken); authToken = user.authenticationToken; prefs.put(Preferences.PLEX_USERNAME, user.username); prefs.put(Preferences.PLEX_EMAIL, user.email); feedback.m(R.string.logged_in); if(doingFirstTimeSetup) { showFindingPlexClientsAndServers(); refreshServers.run(); refreshClients.run(); } else { setupNavigationDrawer(); refreshServers(null); } setupMediaRouter(); alertD.cancel(); } @Override public void onFailure(int statusCode) { logger.d("Failure logging in"); String err = getString(R.string.login_error); if(statusCode == 401) { err = getString(R.string.login_incorrect); } feedback.e(err); alertD.cancel(); } })); builder .setCancelable(true); alertD.show(); } private void setServer(PlexServer s) { server = s; saveSettings(); if(getMainFragment().isVisible()) getMainFragment().setServer(s); if(!doingFirstTimeSetup) refreshNavServers(); } public void logout(View v) { logger.d("logging out"); prefs.remove(Preferences.AUTHENTICATION_TOKEN); prefs.remove(Preferences.PLEX_USERNAME); authToken = null; if(!server.local) { setServer(PlexServer.getScanAllServer()); } // Remove any non-local servers from our list for(PlexServer s : VoiceControlForPlexApplication.servers.values()) { if(!s.local) VoiceControlForPlexApplication.servers.remove(s.name); } refreshNavServers(); Type serverType = new TypeToken<ConcurrentHashMap<String, PlexServer>>(){}.getType(); prefs.put(Preferences.SAVED_SERVERS, gsonWrite.toJson(VoiceControlForPlexApplication.servers, serverType)); saveSettings(); // Refresh the navigation drawer setupNavigationDrawer(); feedback.m(R.string.logged_out); } private void setupNavigationDrawer() { mDrawer.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); drawerToggle = new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close) { @Override public void onDrawerSlide(View drawerView, float slideOffset) { // super.onDrawerSlide(drawerView, 0); // Do nothing. This will ensure the hamburger icon is restored } @Override public void onDrawerClosed(View drawerView) { super.onDrawerClosed(drawerView); setNavGroup(R.menu.nav_items_main); } }; mDrawer.addDrawerListener(drawerToggle); // Find our drawer view if(navigationViewMain == null) navigationViewMain = (NavigationView) findViewById(R.id.navigationViewMain); // Footer view navigationFooter = (LinearLayout) findViewById(R.id.navigationViewFooter); final LinearLayout navigationFooterHelpButton = (LinearLayout)navigationFooter.findViewById(R.id.navigationFooterHelpButton); navigationFooterHelpButton.setOnClickListener(v -> { navigationFooterHelpButton.setBackgroundColor(ContextCompat.getColor(MainActivity.this, R.color.primary_600)); handler.postDelayed(() -> navigationFooterHelpButton.setBackgroundColor(ContextCompat.getColor(MainActivity.this, R.color.navigation_drawer_background)), 200); AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); View view = getLayoutInflater().inflate(R.layout.help_dialog, null); builder.setView(view); final AlertDialog usageDialog = builder.create(); Button button = (Button)view.findViewById(R.id.helpCloseButton); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { usageDialog.dismiss(); } }); usageDialog.show(); }); final LinearLayout navigationFooterSettingsButton = (LinearLayout)navigationFooter.findViewById(R.id.navigationFooterSettingsButton); navigationFooterSettingsButton.setOnClickListener(v -> { navigationFooterSettingsButton.setBackgroundColor(ContextCompat.getColor(MainActivity.this, R.color.primary_600)); handler.postDelayed(() -> { navigationFooterSettingsButton.setBackgroundColor(ContextCompat.getColor(MainActivity.this, R.color.navigation_drawer_background)); setNavGroup(R.menu.nav_items_settings); }, 200); }); if(navigationViewMain.getHeaderView(0) != null) navigationViewMain.removeHeaderView(navigationViewMain.getHeaderView(0)); refreshNavServers(); if(authToken != null) { navigationViewMain.inflateHeaderView(R.layout.nav_header_logged_in); if(prefs.getString(Preferences.PLEX_EMAIL) != null) { setUserThumb(); logger.d("Username = %s", prefs.getString(Preferences.PLEX_USERNAME)); final View navHeader = navigationViewMain.getHeaderView(0); serverListRefreshSpinner = (ProgressBar)navHeader.findViewById(R.id.serverListRefreshSpinner); serverListRefreshButton = (ImageView)navHeader.findViewById(R.id.serverListRefreshButton); TextView navHeaderUsername = (TextView)navHeader.findViewById(R.id.navHeaderUsername); navHeaderUsername.setText(prefs.getString(Preferences.PLEX_USERNAME)); // When the user clicks on their username, show the logout button final LinearLayout navHeaderUserRow = (LinearLayout)navHeader.findViewById(R.id.navHeaderUserRow); final ExpandableRelativeLayout navHeaderLogoutFrame = (ExpandableRelativeLayout)navHeader.findViewById(R.id.navHeaderLogoutFrame); navHeaderUserRow.setOnClickListener(v -> { int flip = navHeaderLogoutFrame.isExpanded() ? R.animator.flip_down : R.animator.flip_up; navHeaderLogoutFrame.toggle(); // Flip the arrow that is to the right of the username ImageView image = (ImageView)navHeaderUserRow.findViewById(R.id.navHeaderUserArrow); AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(MainActivity.this, flip); set.setTarget(image); set.start(); }); } } else { navigationViewMain.inflateHeaderView(R.layout.nav_header_logged_out); final View navHeader = navigationViewMain.getHeaderView(0); serverListRefreshSpinner = (ProgressBar)navHeader.findViewById(R.id.serverListRefreshSpinner); serverListRefreshButton = (ImageView)navHeader.findViewById(R.id.serverListRefreshButton); final LinearLayout navHeaderUserRow = (LinearLayout)navHeader.findViewById(R.id.navHeaderUserRow); navHeaderUserRow.setOnClickListener(v -> showLogin()); } } @Override @SuppressWarnings("unchecked") protected void onNewIntent(final Intent intent) { super.onNewIntent(intent); logger.d("onNewIntent: %s", intent.getAction()); if(intent.getAction() != null) { if (intent.getAction().equals(PlexScannerService.ACTION_SERVER_SCAN_FINISHED)) { HashMap<String, PlexServer> s = (HashMap<String, PlexServer>) intent.getSerializableExtra(com.atomjack.shared.Intent.EXTRA_SERVERS); logger.d("finished scanning for servers, have %d servers", s.size()); // Save the fact that we've finished this server scan prefs.put(Preferences.SERVER_SCAN_FINISHED, true); VoiceControlForPlexApplication.servers = new ConcurrentHashMap<>(s); // If the currently selected server is not in the list of servers we now have, set the current server to scan all if(server == null || !VoiceControlForPlexApplication.servers.containsKey(server.name)) { setServer(PlexServer.getScanAllServer()); } Type serverType = new TypeToken<ConcurrentHashMap<String, PlexServer>>(){}.getType(); prefs.put(Preferences.SAVED_SERVERS, gsonWrite.toJson(VoiceControlForPlexApplication.servers, serverType)); logger.d("doing first time setup: %s, client scan finished: %s", doingFirstTimeSetup, firstTimeSetupClientScanFinished); if(doingFirstTimeSetup) { firstTimeSetupServerScanFinished = true; if(firstTimeSetupClientScanFinished) onFirstTimeScanFinished(); } else { // Refresh the list of servers in the navigation drawer refreshNavServers(); } if(onServerRefreshFinished != null) onServerRefreshFinished.run(); } else if(intent.getAction().equals(PlexScannerService.ACTION_CLIENT_SCAN_FINISHED)) { VoiceControlForPlexApplication.clients = new HashMap<>(); List<PlexClient> c = (ArrayList<PlexClient>)intent.getSerializableExtra(com.atomjack.shared.Intent.EXTRA_CLIENTS); if(c != null) { logger.d("Got %d clients", c.size()); for (PlexClient client : c) { if (!VoiceControlForPlexApplication.clients.containsKey(client.name)) { VoiceControlForPlexApplication.clients.put(client.name, client); logger.d("Saved %s", client.name); } } prefs.put(Preferences.SAVED_CLIENTS, gsonWrite.toJson(VoiceControlForPlexApplication.clients)); } if (doingFirstTimeSetup) { firstTimeSetupClientScanFinished = true; if(firstTimeSetupServerScanFinished) onFirstTimeScanFinished(); } if(deviceSelectDialog != null && deviceSelectDialog.isShowing()) { if (VoiceControlForPlexApplication.getAllClients().size() == 0) { deviceSelectNoDevicesFound.setVisibility(View.VISIBLE); deviceListResume.setVisibility(View.GONE); } else { deviceSelectNoDevicesFound.setVisibility(View.GONE); deviceListResume.setVisibility(View.VISIBLE); } } if(onClientRefreshFinished != null) { onClientRefreshFinished.run(); } } else if(intent.getAction().equals(ACTION_SHOW_NOW_PLAYING)) { handleShowNowPlayingIntent(intent); } else if(intent.getAction().equals(com.atomjack.shared.Intent.ACTION_PLAY_LOCAL)) { // this intent will only arrive when playing music. playing video will go to VideoPlayerActivity handlePlayLocalIntent(intent); } else if(intent.getAction() != null && intent.getAction().equals(com.atomjack.shared.Intent.SHOW_WEAR_PURCHASE)) { // An Android Wear device was successfully pinged, so show popup alerting the // user that they can purchase wear support, but only if we've never shown the popup before. if(VoiceControlForPlexApplication.getInstance().hasWear()) { hidePurchaseWearMenuItem(); } else { if (prefs.get(Preferences.HAS_SHOWN_WEAR_PURCHASE_POPUP, false) == false) { // If wear support has been purchased before initial setup has been done, the navigation drawer // won't have been setup yet, so let's delay showing the popup until after that is done // (since it's bad UI to show that popup so soon, and because upon successful purchase, the // wear options navigation item isn't even showing yet if(navigationViewMain == null) showWearPurchase = true; else showWearPurchase(); } } } else if(intent.getAction() != null && intent.getAction().equals(com.atomjack.shared.Intent.SHOW_WEAR_PURCHASE_REQUIRED)) { showWearPurchaseRequired(); } else if(intent.getAction() != null && intent.getAction().equals(WearConstants.GET_DEVICE_LOGS)) { String wearLog = intent.getStringExtra(WearConstants.LOG_CONTENTS); receivedWearLogsResponse = true; emailDeviceLogs(wearLog); } else if(intent.getAction().equals(com.atomjack.shared.Intent.GET_PLAYING_MEDIA)) { PlexMedia media = subscriptionService.getNowPlayingMedia(); if(media != null) { // Send information on the currently playing media to the wear device DataMap data = new DataMap(); data.putString(WearConstants.MEDIA_TITLE, media.title); data.putString(WearConstants.IMAGE, media.art); new SendToDataLayerThread(WearConstants.GET_PLAYING_MEDIA, data, this).start(); } } } } // This is called after first time setup client & server scan is done. private void onFirstTimeScanFinished() { logger.d("first time scan finished"); doingFirstTimeSetup = false; prefs.put(Preferences.FIRST_TIME_SETUP_COMPLETED, true); if(alertDialog != null) alertDialog.dismiss(); init(); drawerToggle.syncState(); doAutomaticDeviceScan(); } private void handlePlayLocalIntent(Intent intent) { logger.d("Binding to LocalMusicService"); bindMusicPlayerService(); final PlexTrack track = intent.getParcelableExtra(com.atomjack.shared.Intent.EXTRA_MEDIA); final ArrayList<? extends PlexMedia> playlist = intent.getParcelableArrayListExtra(com.atomjack.shared.Intent.EXTRA_PLAYLIST); logger.d("Got track %s and media container with %d tracks", (track != null ? track.title : null), playlist.size()); if(musicPlayerFragment != null && musicPlayerFragment.isVisible()) { localMusicService.setTrack(track); localMusicService.setPlaylist(playlist); localMusicService.reset(); localMusicService.playSong(); } else { musicConnection.setOnConnected(() -> { localMusicService.setTrack(track); localMusicService.setPlaylist(playlist); localMusicService.reset(); localMusicService.playSong(); setCastIconActive(); if (musicPlayerFragment == null) musicPlayerFragment = new MusicPlayerFragment(); musicPlayerFragment.init(localMusicService.getTrack(), localMusicService.getPlaylist()); logger.d("Switching to music"); switchToFragment(musicPlayerFragment); }); } } private void handleShowNowPlayingIntent(Intent intent) { client = intent.getParcelableExtra(com.atomjack.shared.Intent.EXTRA_CLIENT); PlexMedia media = intent.getParcelableExtra(com.atomjack.shared.Intent.EXTRA_MEDIA); ArrayList<? extends PlexMedia> playlist = intent.getParcelableArrayListExtra(com.atomjack.shared.Intent.EXTRA_PLAYLIST); boolean fromWear = intent.getBooleanExtra(WearConstants.FROM_WEAR, false); // Need to overwrite what media is playing from the subscription manager, if it exists. if(client.isLocalClient && media instanceof PlexTrack) { logger.d("Binding to LocalMusicService"); bindMusicPlayerService(); musicConnection.setOnConnected(() -> { musicPlayerFragment = new MusicPlayerFragment(); try { musicPlayerFragment.init(localMusicService.getTrack(), localMusicService.getPlaylist()); switchToFragment(musicPlayerFragment); } catch (Exception e) { e.printStackTrace(); } }); } else { Runnable r = () -> { PlayerState state = subscriptionService.getCurrentState(); PlexMedia media2 = subscriptionService.getNowPlayingMedia(); if(!subscriptionService.isSubscribed() && !subscriptionService.isSubscribing()) subscriptionService.subscribe(client, !subscriptionService.isSubscribed()); if(media2 != null) logger.d("show now playing: %s", media2.getTitle()); int layout = getLayoutForMedia(media2, state); if(layout != -1) { playerFragment.init(layout, client, media2, playlist, fromWear); if(playerFragment.isVisible()) playerFragment.mediaChanged(media2); else switchToPlayerFragment(); int seconds = intent.getBooleanExtra(com.atomjack.shared.Intent.EXTRA_STARTING_PLAYBACK, false) ? 10 : 3; logger.d("Setting auto disconnect for %d seconds", seconds); handler.postDelayed(autoDisconnectPlayerTimer, seconds*1000); } }; if(subscriptionServiceIsBound) r.run(); else bindSubscriptionService(r); } } private void switchToPlayerFragment() { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); if(getMainFragment().isVisible()) { transaction.setCustomAnimations(R.anim.slide_in_up, R.anim.slide_out_up); } transaction.replace(R.id.flContent, playerFragment); transaction.commit(); } private void switchToMainFragment() { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.setCustomAnimations(R.anim.slide_in_down, R.anim.slide_out_down); transaction.replace(R.id.flContent, getMainFragment()); transaction.commit(); } private Runnable refreshServers = new Runnable() { @Override public void run() { // First, save the fact that we have started but not yet finished this server scan. On startup, we'll check for this and if it hasn't finished, kick off // a new scan right away. logger.d("Refreshing servers"); prefs.put(Preferences.SERVER_SCAN_FINISHED, false); Intent scannerIntent = new Intent(MainActivity.this, PlexScannerService.class); scannerIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); scannerIntent.putExtra(PlexScannerService.CLASS, MainActivity.class); scannerIntent.setAction(PlexScannerService.ACTION_SCAN_SERVERS); startService(scannerIntent); prefs.put(Preferences.LAST_SERVER_SCAN, new Date().getTime()); handler.postDelayed(refreshServers, SERVER_SCAN_INTERVAL); } }; private Runnable refreshClients = new Runnable() { @Override public void run() { logger.d("Refreshing clients"); Intent scannerIntent = new Intent(MainActivity.this, PlexScannerService.class); scannerIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); scannerIntent.putExtra(PlexScannerService.CLASS, MainActivity.class); scannerIntent.putExtra(com.atomjack.shared.Intent.EXTRA_CONNECT_TO_CLIENT, false); scannerIntent.setAction(PlexScannerService.ACTION_SCAN_CLIENTS); startService(scannerIntent); handler.postDelayed(refreshClients, SERVER_SCAN_INTERVAL); } }; // This needs to be called every time we update the servers list, so the spinner updates and sets the selection to the currently selected server private void refreshNavServers() { final HashMap<Integer, PlexServer> menuItemServerMap = new HashMap<>(); MenuItem.OnMenuItemClickListener serverItemClickListener = item -> { PlexServer s = menuItemServerMap.get(item.getItemId()); setServer(s); return true; }; Menu menu = navigationViewMain.getMenu(); menu.clear(); PlexServer scanAllServer = PlexServer.getScanAllServer(); MenuItem scanAllItem = menu.add(Menu.NONE, 0, 0, scanAllServer.name); scanAllItem.setCheckable(true); scanAllItem.setChecked(server.isScanAllServer); scanAllItem.setOnMenuItemClickListener(serverItemClickListener); scanAllItem.setIcon(R.drawable.menu_server); menuItemServerMap.put(0, scanAllServer); int id = 1; for(PlexServer thisServer : VoiceControlForPlexApplication.servers.values()) { MenuItem item = menu.add(Menu.NONE, id, id, thisServer.owned ? thisServer.name : thisServer.sourceTitle); item.setIcon(R.drawable.menu_server); item.setCheckable(true); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); LinearLayout layout = (LinearLayout)getLayoutInflater().inflate(thisServer.owned ? R.layout.nav_server_list_sections : R.layout.nav_server_list_sections_unowned, null); int numSections = thisServer.movieSections.size() + thisServer.tvSections.size() + thisServer.musicSections.size(); TextView serverExtra = (TextView)layout.findViewById(R.id.serverListSections); serverExtra.setText(String.format("(%d %s)", numSections, getString(R.string.sections))); if (!thisServer.owned) { TextView serverExtraName = (TextView)layout.findViewById(R.id.serverListName); serverExtraName.setText(thisServer.name); } item.setActionView(layout); menuItemServerMap.put(id, thisServer); item.setChecked(server != null && server.machineIdentifier != null && server.machineIdentifier.equals(thisServer.machineIdentifier)); item.setOnMenuItemClickListener(serverItemClickListener); id++; } } public void navMenuSettingsBack(MenuItem item) { setNavGroup(R.menu.nav_items_main); } private void setNavGroup(int group) { navigationViewMain.getMenu().clear(); navigationViewMain.inflateMenu(group); Menu menu = navigationViewMain.getMenu(); LinearLayout navHeaderPlexServersTitle = (LinearLayout)navigationViewMain.findViewById(R.id.navHeaderPlexServersTitle); if(group == R.menu.nav_items_main) { if(navHeaderPlexServersTitle != null) navHeaderPlexServersTitle.setVisibility(View.VISIBLE); navigationViewMain.setItemBackground(ContextCompat.getDrawable(this, R.drawable.nav_drawer_server_item)); navigationFooter.setVisibility(View.VISIBLE); handler.postDelayed(() -> refreshNavServers(), 1); } else { navHeaderPlexServersTitle.setVisibility(View.GONE); if(group == R.menu.nav_items_settings) { navigationViewMain.setItemBackground(ContextCompat.getDrawable(this, R.drawable.nav_drawer_item)); navigationFooter.setVisibility(View.GONE); if(VoiceControlForPlexApplication.getInstance().hasWear()) hidePurchaseWearMenuItem(); if(VoiceControlForPlexApplication.getInstance().hasLocalmedia()) hidePurchaseLocalmediaMenuItem(); MenuItem chromecastOptionsItem = menu.findItem(R.id.menu_chromecast_video); MenuItem chromecastPurchaseItem = menu.findItem(R.id.menu_purchase_chromecast); chromecastPurchaseItem.setVisible(!VoiceControlForPlexApplication.getInstance().hasChromecast()); chromecastOptionsItem.setVisible(VoiceControlForPlexApplication.getInstance().hasChromecast()); SwitchCompat usageExamplesSwitch = (SwitchCompat)menu.findItem(R.id.menu_usage_hints_switch).getActionView(); usageExamplesSwitch.setChecked(prefs.get(Preferences.SHOW_USAGE_HINTS, true)); usageExamplesSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { prefs.put(Preferences.SHOW_USAGE_HINTS, isChecked); if(getMainFragment().isVisible()) { getMainFragment().setUsageHintsActive(isChecked); } }); if (!hasValidAutoVoice() && !hasValidUtter()) { menu.findItem(R.id.menu_tasker_import).setVisible(false); if (!hasValidTasker()) { menu.findItem(R.id.menu_install_tasker).setVisible(true); } if (!hasValidUtter()) { menu.findItem(R.id.menu_install_utter).setVisible(true); } if (!hasValidAutoVoice()) { menu.findItem(R.id.menu_install_autovoice).setVisible(true); } } } } } public void refreshServers(View v) { serverListRefreshButton.setVisibility(View.GONE); serverListRefreshSpinner.setVisibility(View.VISIBLE); onServerRefreshFinished = () -> { serverListRefreshButton.setVisibility(View.VISIBLE); serverListRefreshSpinner.setVisibility(View.GONE); refreshNavServers(); onServerRefreshFinished = null; }; refreshServers.run(); } private void setUserThumb() { setUserThumb(false); } private void setUserThumb(boolean skipThumb) { final Bitmap bitmap = VoiceControlForPlexApplication.getInstance().getCachedBitmap(VoiceControlForPlexApplication.getInstance().getUserThumbKey()); if(bitmap == null && !skipThumb) { fetchUserThumb(); } else { runOnUiThread(() -> { View navHeader = navigationViewMain.getHeaderView(0); ImageView imageView = (ImageView) navHeader.findViewById(R.id.navHeaderUserIcon); if(bitmap == null) imageView.setImageResource(R.drawable.nav_default_user); else imageView.setImageBitmap(bitmap); }); } } private void fetchUserThumb() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { try { String url = String.format("http://www.gravatar.com/avatar/%s?s=60&d=404", Utils.md5(prefs.getString(Preferences.PLEX_EMAIL))); logger.d("url: %s", url); byte[] imageData = PlexHttpClient.getSyncBytes(url); if(imageData != null) { logger.d("got %d bytes", imageData.length); InputStream is = new ByteArrayInputStream(imageData); is.reset(); VoiceControlForPlexApplication.getInstance().mSimpleDiskCache.put(VoiceControlForPlexApplication.getInstance().getUserThumbKey(), is); } setUserThumb(true); } catch(SocketTimeoutException e) { logger.d("Couldn't get user thumb."); } catch(Exception ex) { ex.printStackTrace(); } return null; } }.execute(); } @Override public boolean onCreateOptionsMenu(Menu menu) { if(!doingFirstTimeSetup) { getMenuInflater().inflate(R.menu.toolbar_cast, menu); castIconMenuItem = menu.findItem(R.id.action_cast); if (isSubscribed()) { setCastIconActive(); } } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // The action bar home/up action should open or close the drawer. switch (item.getItemId()) { case android.R.id.home: mDrawer.openDrawer(GravityCompat.START); return true; case R.id.action_cast: castIconClick(onClientChosen); return true; } return super.onOptionsItemSelected(item); } private void animateCastIcon() { castIconMenuItem.setIcon(R.drawable.mr_ic_media_route_connecting_holo_dark); AnimationDrawable ad = (AnimationDrawable) castIconMenuItem.getIcon(); ad.start(); } // This is the default action that will be taken when a user subscribes to a client via the UI. private ScanHandler onClientChosen = new ScanHandler() { @Override public void onDeviceSelected(PlexDevice device, boolean resume) { if(device != null) { final PlexClient clientSelected = (PlexClient)device; if(clientSelected.isLocalClient) { if(!prefs.get(Preferences.HAS_SHOWN_INITIAL_LOCALMEDIA_PURCHASE, false) && !VoiceControlForPlexApplication.getInstance().hasLocalmedia()) { showPurchaseLocalMedia(true); } else { localClientSelected(clientSelected); } return; } setClient(clientSelected); // Start animating the action bar icon animateCastIcon(); Runnable r = () -> { if(clientSelected.isCastClient) { if(VoiceControlForPlexApplication.getInstance().hasChromecast() || prefs.get(Preferences.HAS_SHOWN_INITIAL_CHROMECAST_PURCHASE, false)) { client = clientSelected; logger.d("subscribing to %s", client.name); subscriptionService.subscribe(client, !subscriptionService.isSubscribed()); } else { setCastIconInactive(); showChromecastPurchase(clientSelected, new Runnable() { @Override public void run() { animateCastIcon(); subscriptionService.subscribe(postChromecastPurchaseClient, true); } }); } } else subscriptionService.subscribe(clientSelected, true); }; if(subscriptionServiceIsBound) { r.run(); } else { subscriptionConnection.setOnConnected(r); bindSubscriptionService(); } /* if (clientSelected.isCastClient) { if(VoiceControlForPlexApplication.getInstance().hasChromecast() || prefs.get(Preferences.HAS_SHOWN_INITIAL_CHROMECAST_PURCHASE, false)) { client = clientSelected; logger.d("subscribing to %s", client.name); castPlayerManager.subscribe(client, !castPlayerManager.isSubscribed()); } else { setCastIconInactive(); showChromecastPurchase(clientSelected, new Runnable() { @Override public void run() { animateCastIcon(); castPlayerManager.subscribe(postChromecastPurchaseClient, true); } }); } } else { Runnable onSubscriptionServiceConnected = () -> { subscriptionService.startSubscription(clientSelected, true); }; if(subscriptionServiceIsBound) { onSubscriptionServiceConnected.run(); } else { subscriptionConnection.setOnConnected(onSubscriptionServiceConnected); bindSubscriptionService(); } } */ } } }; private void localClientSelected(PlexClient clientSelected) { setClient(clientSelected); setCastIconActive(); subscriptionService.subscribe(clientSelected, !subscriptionService.isSubscribed()); prefs.put(Preferences.SUBSCRIBED_CLIENT, gsonWrite.toJson(clientSelected)); client = clientSelected; } protected void setClient(PlexClient _client) { logger.d("setClient"); client = _client; prefs.put(Preferences.CLIENT, gsonWrite.toJson(_client)); if(getMainFragment().isVisible()) getMainFragment().setClient(_client); } private boolean isSubscribed() { if(!subscriptionServiceIsBound) return false; else { return subscriptionService.isSubscribed(); } } private boolean isSubscribing() { return subscriptionServiceIsBound && subscriptionService.isSubscribing(); } private Runnable onClientRefreshFinished = null; private Runnable onServerRefreshFinished = null; private void castIconClick(final ScanHandler onFinish) { if(!isSubscribed() && !isSubscribing()) { final PlexListAdapter adapter = new PlexListAdapter(this, PlexListAdapter.TYPE_CLIENT); AlertDialog.Builder builder = new AlertDialog.Builder(this); LayoutInflater inflater = getLayoutInflater(); final View layout = inflater.inflate(R.layout.device_select, null); final TextView headerView = (TextView)layout.findViewById(R.id.deviceListHeader); headerView.setText(R.string.select_plex_client); final ImageButton button = (ImageButton)layout.findViewById(R.id.deviceListRefreshButton); final ProgressBar spinnerImage = (ProgressBar) layout.findViewById(R.id.deviceListRefreshSpinner); button.setOnClickListener(v -> { onClientRefreshFinished = () -> { logger.d("Changing buttons"); button.setVisibility(View.VISIBLE); spinnerImage.setVisibility(View.GONE); logger.d("Setting %d clients", VoiceControlForPlexApplication.getAllClients().size()); adapter.setClients(VoiceControlForPlexApplication.getAllClients()); adapter.notifyDataSetChanged(); onClientRefreshFinished = null; }; logger.d("Refreshing"); button.setVisibility(View.GONE); spinnerImage.setVisibility(View.VISIBLE); handler.removeCallbacks(refreshClients); refreshClients.run(); }); deviceListResume = (CheckBox) layout.findViewById(R.id.deviceListResume); deviceListResume.setVisibility(View.VISIBLE); deviceListResume.setChecked(prefs.get(Preferences.RESUME, false)); deviceListResume.setOnClickListener(v -> prefs.put(Preferences.RESUME, ((CheckBox) v).isChecked())); deviceSelectNoDevicesFound = (TextView)layout.findViewById(R.id.deviceSelectNoDevicesFound); if(VoiceControlForPlexApplication.getAllClients().size() == 0) { deviceSelectNoDevicesFound.setVisibility(View.VISIBLE); deviceListResume.setVisibility(View.GONE); } else { deviceSelectNoDevicesFound.setVisibility(View.GONE); deviceListResume.setVisibility(View.VISIBLE); } builder.setView(layout); deviceSelectDialog = builder.create(); deviceSelectDialog.show(); final ListView clientListView = (ListView) deviceSelectDialog.findViewById(R.id.serverListView); adapter.setClients(VoiceControlForPlexApplication.getAllClients()); clientListView.setAdapter(adapter); clientListView.setOnItemClickListener((parentAdapter, view, position, id) -> { PlexClient s = (PlexClient) parentAdapter.getItemAtPosition(position); logger.d("client clicked: %s", s.name); deviceSelectDialog.dismiss(); if (onFinish != null) onFinish.onDeviceSelected(s, deviceListResume.isChecked()); }); } else if(!isSubscribing()) { client = subscriptionService.getClient(); if(client == null) { logger.d("Lost subscribed client."); setCastIconInactive(); } else { View view = getLayoutInflater().inflate(R.layout.popup_connected_to_client, null); AlertDialog.Builder builder = new AlertDialog.Builder(this) .setView(view); final AlertDialog subscribeDialog = builder.create(); CheckBox resumeCheckbox = (CheckBox)view.findViewById(R.id.resumeCheckbox); resumeCheckbox.setChecked(prefs.get(Preferences.RESUME, false)); resumeCheckbox.setOnClickListener(v -> prefs.put(Preferences.RESUME, ((CheckBox) v).isChecked())); TextView clientName = (TextView)view.findViewById(R.id.popupConnectedToClientName); clientName.setText(client.name); Button disconnectButton = (Button)view.findViewById(R.id.popupConnectedToClientCancelButton); disconnectButton.setOnClickListener(v -> { if(subscriptionServiceIsBound) subscriptionService.unsubscribe(); subscribeDialog.dismiss(); }); if(client.isCastClient) { final SeekBar volumeSeekBar = (SeekBar)view.findViewById(R.id.volumeSeekBar); volumeSeekBar.setVisibility(View.VISIBLE); volumeSeekBar.setMax(100); volumeSeekBar.setProgress((int)(subscriptionService.getVolume()*100)); volumeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { double v = ((double)progress) / 100; subscriptionService.setVolume(v); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); // React to volume button controls while this dialog is open subscribeDialog.setOnKeyListener((dialog, keyCode, event) -> { boolean ret = MainActivity.this.dispatchKeyEvent(event); volumeSeekBar.setProgress((int)(subscriptionService.getVolume()*100)); return ret; }); } subscribeDialog.show(); } } } // Make sure this is the method with just `Bundle` as the signature @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); if(drawerToggle != null) drawerToggle.syncState(); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggles drawerToggle.onConfigurationChanged(newConfig); } private void saveSettings() { prefs.put(Preferences.SERVER, gsonWrite.toJson(server)); prefs.put(Preferences.CLIENT, gsonWrite.toJson(client)); prefs.put(Preferences.RESUME, prefs.get(Preferences.RESUME, false)); } public void deviceSelectDialogRefresh() { ListView serverListView = (ListView) deviceSelectDialog.findViewById(R.id.serverListView); PlexListAdapter adapter = (PlexListAdapter)serverListView.getAdapter(); adapter.setClients(VoiceControlForPlexApplication.getAllClients()); adapter.notifyDataSetChanged(); } private class MediaRouterCallback extends MediaRouter.Callback { @Override public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo route) { super.onRouteRemoved(router, route); logger.d("Cast Client %s has gone missing. Removing.", route.getName()); if(VoiceControlForPlexApplication.castClients.containsKey(route.getName())) { VoiceControlForPlexApplication.castClients.remove(route.getName()); if(deviceSelectDialog != null && deviceSelectDialog.isShowing()) { deviceSelectDialogRefresh(); } } } @Override public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) { logger.d("onRouteAdded: %s", route); if(!VoiceControlForPlexApplication.castClients.containsKey(route.getName())) { VoiceControlForPlexApplication.castClients.remove(route.getName()); } PlexClient client = new PlexClient(); client.isCastClient = true; client.name = route.getName(); client.product = route.getDescription(); client.castDevice = CastDevice.getFromBundle(route.getExtras()); client.machineIdentifier = client.castDevice.getDeviceId(); VoiceControlForPlexApplication.castClients.put(client.name, client); logger.d("Added cast client %s (%s)", client.name, client.machineIdentifier); if(deviceSelectDialog != null && deviceSelectDialog.isShowing()) { deviceSelectDialogRefresh(); } } @Override public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) { logger.d("onRouteSelected: %s", route); } @Override public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) { logger.d("onRouteUnselected: %s", route); } @Override public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { logger.d("onRouteChanged: %s", route); onRouteAdded(router, route); } } public void showAbout(final MenuItem item) { navigationViewMain.setSelected(false); AlertDialog.Builder alertDialog = new AlertDialog.Builder(MainActivity.this); LayoutInflater inflater = getLayoutInflater(); View layout = inflater.inflate(R.layout.popup_about, null); alertDialog.setView(layout); alertDialog.show(); handler.postDelayed(() -> item.setChecked(false), 500); } public void installShortcut(MenuItem item) { Intent intent = new Intent(this, ShortcutProviderActivity.class); startActivityForResult(intent, RESULT_SHORTCUT_CREATED); } public void donate(MenuItem item) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UJF9QY9QELERG")); startActivity(intent); } public void setFeedback(MenuItem item) { AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = getLayoutInflater().inflate(R.layout.popup_feedback, null); builder.setView(view); final AlertDialog dialog = builder.create(); MultiStateToggleButton feedbackToggleButton = (MultiStateToggleButton)view.findViewById(R.id.feedbackToggleButton); boolean[] v = new boolean[2]; v[prefs.get(Preferences.FEEDBACK, 1) == 0 ? 0 : 1] = true; feedbackToggleButton.setStates(v); feedbackToggleButton.setOnValueChangedListener(value -> { prefs.put(Preferences.FEEDBACK, value); if(value == 0) { onVoiceFeedbackSelected(false); } }); MultiStateToggleButton errorsToggleButton = (MultiStateToggleButton)view.findViewById(R.id.errorsToggleButton); v = new boolean[2]; v[prefs.get(Preferences.ERRORS, 1) == 0 ? 0 : 1] = true; errorsToggleButton.setStates(v); errorsToggleButton.setOnValueChangedListener(value -> { prefs.put(Preferences.ERRORS, value); if(value == 0) { onVoiceFeedbackSelected(true); } }); Button popupFeedbackOKButton = (Button)view.findViewById(R.id.popupFeedbackOKButton); popupFeedbackOKButton.setOnClickListener(v1 -> dialog.dismiss()); dialog.show(); } private void onVoiceFeedbackSelected(boolean errors) { Intent checkIntent = new Intent(); checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA); tts = new TextToSpeech(this, i -> {}); String engine = tts.getDefaultEngine(); if (engine != null) checkIntent.setPackage(engine); settingErrorFeedback = errors; startActivityForResult(checkIntent, RESULT_VOICE_FEEDBACK_SELECTED); } public void installTasker(MenuItem item) { openAppInPlayStore("net.dinglisch.android.taskerm"); } public void showChangelog(MenuItem item) { showWhatsNewDialog(true); } public void installUtter(MenuItem item) { openAppInPlayStore("com.brandall.nutter"); } public void installAutoVoice(MenuItem item) { openAppInPlayStore("com.joaomgcd.autovoice"); } public void purchaseWear(MenuItem item) { showWearPurchaseRequired(); } public void emailDeviceLogs(MenuItem item) { if(VoiceControlForPlexApplication.getInstance().hasWear()) { receivedWearLogsResponse = false; new SendToDataLayerThread(WearConstants.GET_DEVICE_LOGS, this).start(); logger.d("requesting device logs from wear device"); // Now start a 5 second timer. If receivedWearLogsResponse is not true, go ahead and email just the mobile device's log final Handler handler = new Handler(); handler.postDelayed(() -> { if(receivedWearLogsResponse == false) emailDeviceLogs(""); }, 2000); } else { emailDeviceLogs(""); } } // The passed 'wearLog' string is the contents of the wear device's log. If there is no wear device paired with the mobile device, // the passed string will be empty ("") // public void emailDeviceLogs(final String wearLog) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { try { boolean hasPermission = (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED); if(!hasPermission) { ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_WRITE_STORAGE); } else { logger.d("Emailing device logs"); Intent emailIntent = new Intent(Intent.ACTION_SEND_MULTIPLE); emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Voice Control for Plex Android Logs"); emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // Build the body of the email StringBuilder body = new StringBuilder(); body.append(String.format("Manufacturer: %s\n", Build.MANUFACTURER)); body.append(String.format("Device: %s\n", Build.DEVICE)); body.append(String.format("Model: %s\n", Build.MODEL)); body.append(String.format("Product: %s\n", Build.PRODUCT)); body.append(String.format("Version: %s\n", Build.VERSION.RELEASE)); body.append(String.format("App Version: %s\n\n", getPackageManager().getPackageInfo(getPackageName(), 0).versionName)); body.append(String.format("Logged in: %s\n\n", prefs.getString(Preferences.PLEX_USERNAME) != null ? "yes" : "no")); body.append("Description of the issue:\n\n"); emailIntent.setType("application/octet-stream"); emailIntent.putExtra(Intent.EXTRA_TEXT, body.toString()); File tempDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmp"); if (!tempDirectory.exists()) tempDirectory.mkdirs(); File tempFile = new File(tempDirectory, "/vcfp-log.txt"); FileOutputStream fos = new FileOutputStream(tempFile); Writer out = new OutputStreamWriter(fos, "UTF-8"); Process process = Runtime.getRuntime().exec("logcat -d *:V"); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream())); StringBuilder log = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { log.append(line); log.append(System.getProperty("line.separator")); } bufferedReader.close(); out.write(log.toString()); out.flush(); out.close(); ArrayList<Uri> uris = new ArrayList<Uri>(); uris.add(Uri.parse("file://" + tempFile.getAbsolutePath())); // uris.add(FileProvider.getUriForFile(MainActivity.this, "com.atomjack.vcfp.fileprovider", tempFile)); if (!wearLog.equals("")) { logger.d("attaching wear log"); tempFile = new File(tempDirectory, "/vcfp-wear-log.txt"); fos = new FileOutputStream(tempFile); out = new OutputStreamWriter(fos, "UTF-8"); out.write(wearLog); out.flush(); out.close(); uris.add(Uri.parse("file://" + tempFile.getAbsolutePath())); // uris.add(FileProvider.getUriForFile(MainActivity.this, "com.atomjack.vcfp.fileprovider", tempFile)); } emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); startActivity(emailIntent); } } catch (final Exception ex) { logger.d("Exception emailing device logs: %s", ex); runOnUiThread(() -> feedback.e("Error emailing device logs: %s", ex.getMessage())); } return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private void openAppInPlayStore(String packageName) { try { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName))); } catch (android.content.ActivityNotFoundException anfe) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + packageName))); } } public void googleNowOptions(MenuItem item) { AlertDialog.Builder builder = new AlertDialog.Builder(this); LayoutInflater inflater = getLayoutInflater(); View layout = inflater.inflate(R.layout.popup_google_now_options, null); builder.setView(layout); final AlertDialog dialog = builder.create(); MultiStateToggleButton feedbackToggleButton = (MultiStateToggleButton)layout.findViewById(R.id.googleNowOptionsToggleButton); boolean[] v = new boolean[2]; // yes = v[0], no = v[1], since yes is listed before no in the UI try { v[prefs.get(Preferences.GOOGLE_NOW_LAUNCH_NOW_PLAYING, true) ? 0 : 1] = true; } catch (Exception e) { e.printStackTrace(); v[0] = true; prefs.put(Preferences.GOOGLE_NOW_LAUNCH_NOW_PLAYING, true); } feedbackToggleButton.setStates(v); feedbackToggleButton.setOnValueChangedListener(value -> prefs.put(Preferences.GOOGLE_NOW_LAUNCH_NOW_PLAYING, value == 0)); Button okButton = (Button)layout.findViewById(R.id.googleNowOptionsOKButton); okButton.setOnClickListener(v1 -> dialog.dismiss()); dialog.show(); } public void cinemaTrailers(MenuItem item) { AlertDialog.Builder builder = new AlertDialog.Builder(this); LayoutInflater inflater = getLayoutInflater(); View layout = inflater.inflate(R.layout.popup_cinema_trailers, null); final ListView listView = (ListView)layout.findViewById(R.id.cinemaTrailersList); List<String> list = new ArrayList<>(); list.add(getString(R.string.none)); list.add("1"); list.add("2"); list.add("3"); list.add("4"); list.add("5"); ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(getApplicationContext(), android.R.layout.simple_list_item_single_choice, list); listView.setAdapter(arrayAdapter); int numTrailers = prefs.get(Preferences.NUM_CINEMA_TRAILERS, 0); listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); listView.setItemChecked(numTrailers, true); builder.setView(layout); final AlertDialog dialog = builder.create(); listView.setOnItemClickListener((parent, view, position, id) -> { CheckedTextView item1 = (CheckedTextView)view; item1.setChecked(true); prefs.put(Preferences.NUM_CINEMA_TRAILERS, position); handler.postDelayed(new Runnable() { @Override public void run() { dialog.dismiss(); } }, 500); }); dialog.show(); } public void purchaseChromecast(MenuItem item) { VoiceControlForPlexApplication.getInstance().getIabHelper().launchPurchaseFlow(MainActivity.this, VoiceControlForPlexApplication.SKU_CHROMECAST, 10001, mPurchaseFinishedListener, VoiceControlForPlexApplication.SKU_TEST_PURCHASED == VoiceControlForPlexApplication.SKU_CHROMECAST ? VoiceControlForPlexApplication.getInstance().getEmailHash() : ""); } protected void showChromecastPurchase(PlexClient client, final Runnable onSuccess) { postChromecastPurchaseClient = client; postChromecastPurchaseAction = onSuccess; prefs.put(Preferences.HAS_SHOWN_INITIAL_CHROMECAST_PURCHASE, true); AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = getLayoutInflater().inflate(R.layout.popup_chromecast_purchase, null); builder.setView(view); final AlertDialog dialog = builder.setCancelable(false).create(); TextView popupChromecastPurchaseMessage = (TextView)view.findViewById(R.id.popupChromecastPurchaseMessage); popupChromecastPurchaseMessage.setText(String.format(getString(R.string.must_purchase_chromecast2), VoiceControlForPlexApplication.getChromecastPrice())); Button popupChromecastPurchaseOKButton = (Button)view.findViewById(R.id.popupChromecastPurchaseOKButton); popupChromecastPurchaseOKButton.setOnClickListener(v -> { dialog.cancel(); VoiceControlForPlexApplication.getInstance().getIabHelper().launchPurchaseFlow(MainActivity.this, VoiceControlForPlexApplication.SKU_CHROMECAST, 10001, mPurchaseFinishedListener, VoiceControlForPlexApplication.SKU_TEST_PURCHASED == VoiceControlForPlexApplication.SKU_CHROMECAST ? VoiceControlForPlexApplication.getInstance().getEmailHash() : ""); }); Button popupChromecastPurchaseNoButton = (Button)view.findViewById(R.id.popupChromecastPurchaseNoButton); popupChromecastPurchaseNoButton.setOnClickListener(v -> { dialog.cancel(); onSuccess.run(); }); dialog.show(); } protected void showWearPurchaseRequired() { showWearPurchase(R.string.wear_purchase_required, false); } protected void showWearPurchase() { showWearPurchase(R.string.wear_detected_can_purchase, true); } protected void showWearPurchase(int stringResource, final boolean showPurchaseFromMenu) { AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = getLayoutInflater().inflate(R.layout.popup_wear_purchase_required, null); builder.setView(view); final AlertDialog dialog = builder.create(); TextView wearPurchaseRequiredTitle = (TextView)view.findViewById(R.id.wearPurchaseRequiredTitle); wearPurchaseRequiredTitle.setText(String.format(getString(stringResource), VoiceControlForPlexApplication.getWearPrice())); Button wearPurchaseRequiredNoThanksButton = (Button)view.findViewById(R.id.wearPurchaseRequiredNoThanksButton); wearPurchaseRequiredNoThanksButton.setOnClickListener(v -> { dialog.cancel(); prefs.put(Preferences.HAS_SHOWN_WEAR_PURCHASE_POPUP, true); if(showPurchaseFromMenu) { AlertDialog.Builder builder2 = new AlertDialog.Builder(MainActivity.this); View view1 = getLayoutInflater().inflate(R.layout.popup_wear_purchase_menu, null); builder2.setView(view1).setCancelable(false); final AlertDialog dialog1 = builder2.create(); Button popupWearPurchaseMenuOKButton = (Button) view1.findViewById(R.id.popupWearPurchaseMenuOKButton); popupWearPurchaseMenuOKButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dialog1.cancel(); } }); dialog1.show(); } }); Button wearPurchaseRequiredOKButton = (Button)view.findViewById(R.id.wearPurchaseRequiredOKButton); wearPurchaseRequiredOKButton.setOnClickListener(v -> { prefs.put(Preferences.HAS_SHOWN_WEAR_PURCHASE_POPUP, true); dialog.cancel(); VoiceControlForPlexApplication.getInstance().getIabHelper().flagEndAsync(); VoiceControlForPlexApplication.getInstance().getIabHelper().launchPurchaseFlow(MainActivity.this, VoiceControlForPlexApplication.SKU_WEAR, 10001, mPurchaseFinishedListener, VoiceControlForPlexApplication.SKU_TEST_PURCHASED == VoiceControlForPlexApplication.SKU_WEAR ? VoiceControlForPlexApplication.getInstance().getEmailHash() : ""); }); dialog.show(); } IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() { public void onIabPurchaseFinished(IabResult result, Purchase purchase) { if (result.isFailure()) { logger.d("Error purchasing: " + result); if(result.getResponse() != -1005) { feedback.e(result.getMessage()); } // TODO: this // Only reset the cast icon if we aren't subscribed (if we are, the only way to get here is through main client selection) // if(!isSubscribed()) // setCastIconInactive(); return; } else if (purchase.getSku().equals(VoiceControlForPlexApplication.SKU_CHROMECAST)) { logger.d("Purchased chromecast!"); VoiceControlForPlexApplication.getInstance().setHasChromecast(true); if(postChromecastPurchaseAction != null) { postChromecastPurchaseAction.run(); } } else if(purchase.getSku().equals(VoiceControlForPlexApplication.SKU_WEAR)) { logger.d("Purchased Wear Support!"); VoiceControlForPlexApplication.getInstance().setHasWear(true); hidePurchaseWearMenuItem(); // Send a message to the wear device that wear support has been purchased new SendToDataLayerThread(WearConstants.WEAR_PURCHASED, MainActivity.this).start(); } else if(purchase.getSku().equals(VoiceControlForPlexApplication.SKU_LOCALMEDIA)) { logger.d("Purchased Local Media Support!"); VoiceControlForPlexApplication.getInstance().setHasLocalmedia(true); hidePurchaseLocalmediaMenuItem(); } } }; public void showWearOptions(MenuItem item) { AlertDialog.Builder builder = new AlertDialog.Builder(this); LayoutInflater inflater = getLayoutInflater(); View layout = inflater.inflate(R.layout.popup_wear_options, null); builder.setView(layout); final AlertDialog chooserDialog = builder.create(); Button playPauseButton = (Button)layout.findViewById(R.id.wearOptionsPlayPauseButton); playPauseButton.setOnClickListener(v -> { DataMap dataMap = new DataMap(); dataMap.putBoolean(WearConstants.PRIMARY_FUNCTION_VOICE_INPUT, true); new SendToDataLayerThread(WearConstants.SET_WEAR_OPTIONS, dataMap, MainActivity.this).start(); chooserDialog.dismiss(); }); Button voiceInputButton = (Button)layout.findViewById(R.id.wearOptionsVoiceInputButton); voiceInputButton.setOnClickListener(v -> { DataMap dataMap = new DataMap(); dataMap.putBoolean(WearConstants.PRIMARY_FUNCTION_VOICE_INPUT, false); new SendToDataLayerThread(WearConstants.SET_WEAR_OPTIONS, dataMap, MainActivity.this).start(); chooserDialog.dismiss(); }); chooserDialog.show(); } public void showVideoOptions(MenuItem item) { AlertDialog.Builder builder = new AlertDialog.Builder(this); View layout = getLayoutInflater().inflate(R.layout.popup_video_options, null); builder.setView(layout); final AlertDialog chooserDialog = builder.create(); Button popupChromecastOptionsRemoteButton = (Button)layout.findViewById(R.id.popupVideoOptionsRemoteButton); popupChromecastOptionsRemoteButton.setOnClickListener(v -> { chooserDialog.dismiss(); showVideoOptions(true, false); }); Button popupChromecastOptionsLocalButton = (Button)layout.findViewById(R.id.popupVideoOptionsLocalButton); popupChromecastOptionsLocalButton.setOnClickListener(v -> { chooserDialog.dismiss(); showVideoOptions(true, true); }); chooserDialog.show(); } public void showLocalVideoOptions(MenuItem item) { AlertDialog.Builder builder = new AlertDialog.Builder(this); View layout = getLayoutInflater().inflate(R.layout.popup_video_options, null); TextView videoOptionsTitle = (TextView)layout.findViewById(R.id.videoOptionsTitle); videoOptionsTitle.setText(R.string.local_video_options_header); TextView videoOptionsDescription = (TextView)layout.findViewById(R.id.videoOptionsDescription); videoOptionsDescription.setText(R.string.local_video_description); builder.setView(layout); final AlertDialog chooserDialog = builder.create(); Button popupVideoOptionsRemoteButton = (Button)layout.findViewById(R.id.popupVideoOptionsRemoteButton); popupVideoOptionsRemoteButton.setOnClickListener(v -> { chooserDialog.dismiss(); showVideoOptions(false, false); }); Button popupVideoOptionsLocalButton = (Button)layout.findViewById(R.id.popupVideoOptionsLocalButton); popupVideoOptionsLocalButton.setOnClickListener(v -> { chooserDialog.dismiss(); showVideoOptions(false, true); }); chooserDialog.show(); } private void showVideoOptions(final boolean chromecast, final boolean localNetwork) { HashMap<String, String[]> videoQualityOptions; if(chromecast) videoQualityOptions = VoiceControlForPlexApplication.chromecastVideoQualityOptions; else videoQualityOptions = VoiceControlForPlexApplication.localVideoQualityOptions; AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = getLayoutInflater().inflate(R.layout.popup_video_options_detail, null); builder.setView(view); final AlertDialog dialog = builder.create(); TextView videoOptionsTitle = (TextView)view.findViewById(R.id.videoOptionsTitle); int title; if(chromecast) { title = localNetwork ? R.string.chromecast_video_local_full : R.string.chromecast_video_remote_full; } else { title = localNetwork ? R.string.local_video_local_full : R.string.local_video_remote_full; } videoOptionsTitle.setText(title); final String prefKey; if(chromecast) { prefKey = localNetwork ? Preferences.CHROMECAST_VIDEO_QUALITY_LOCAL : Preferences.CHROMECAST_VIDEO_QUALITY_REMOTE; } else { prefKey = localNetwork ? Preferences.LOCAL_VIDEO_QUALITY_LOCAL : Preferences.LOCAL_VIDEO_QUALITY_REMOTE; } RadioGroup videoOptionsRadioGroup = (RadioGroup)view.findViewById(R.id.videoOptionsRadioGroup); final CharSequence[] items = videoQualityOptions.keySet().toArray(new CharSequence[videoQualityOptions.size()]); int videoQuality = new ArrayList<>(videoQualityOptions.keySet()).indexOf(prefs.getString(prefKey)); logger.d("videoQuality: %d", videoQuality); logger.d("options: %s", videoQualityOptions); if(videoQuality == -1 || !videoQualityOptions.containsKey(prefs.getString(prefKey))) videoQuality = new ArrayList<>(videoQualityOptions.keySet()).indexOf(chromecast ? VoiceControlForPlexApplication.chromecastVideoQualityDefault : VoiceControlForPlexApplication.localVideoQualityDefault); LinearLayout.LayoutParams layoutParams = new RadioGroup.LayoutParams( RadioGroup.LayoutParams.WRAP_CONTENT, RadioGroup.LayoutParams.WRAP_CONTENT); for(int i=0;i<items.length;i++) { RadioButton button = (RadioButton)getLayoutInflater().inflate(R.layout.popup_video_options_button, null); button.setText(items[i]); button.setId(i); videoOptionsRadioGroup.addView(button, layoutParams); } videoOptionsRadioGroup.check(videoQuality); videoOptionsRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { logger.d("Checked %s", items[checkedId]); prefs.put(prefKey, (String)items[checkedId]); handler.postDelayed(new Runnable() { @Override public void run() { dialog.dismiss(); } }, 500); }); dialog.show(); } public void importTaskerProject(MenuItem item) { String xmlfile = "VoiceControlForPlex.prj.xml"; try { AssetManager am = getAssets(); InputStream is = am.open(xmlfile); int size = is.available(); byte[] buffer = new byte[size]; is.read(buffer); is.close(); String xmlContents = new String(buffer); xmlContents = xmlContents.replace("%RECOGNITION_REGEX%", getString(R.string.pattern_recognition)); buffer = xmlContents.getBytes(); logger.d("directory: %s", Environment.getExternalStorageDirectory()); File f = new File(Environment.getExternalStorageDirectory() + "/" + xmlfile); FileOutputStream fos = new FileOutputStream(f); fos.write(buffer); fos.close(); logger.d("Wrote xml file"); Intent i = new Intent(); i.setAction(Intent.ACTION_VIEW); i.setDataAndType(Uri.fromFile(f), "text/xml"); startActivityForResult(i, RESULT_TASKER_PROJECT_IMPORTED); } catch (Exception e) { logger.d("Exception opening tasker profile xml: "); e.printStackTrace(); return; } } @Override public void onUserInteraction() { super.onUserInteraction(); userIsInteracting = true; } public void hidePurchaseWearMenuItem() { if(navigationViewMain != null) { MenuItem wearItem = navigationViewMain.getMenu().findItem(R.id.menu_purchase_wear); if (wearItem != null) wearItem.setVisible(false); MenuItem wearOptionsItem = navigationViewMain.getMenu().findItem(R.id.menu_wear_options); if (wearOptionsItem != null) wearOptionsItem.setVisible(true); } } public void hidePurchaseLocalmediaMenuItem() { if(navigationViewMain != null) { MenuItem localmediaItem = navigationViewMain.getMenu().findItem(R.id.menu_purchase_localmedia); if (localmediaItem != null) localmediaItem.setVisible(false); MenuItem localVideoOptions = navigationViewMain.getMenu().findItem(R.id.menu_local_video); if (localVideoOptions != null) localVideoOptions.setVisible(true); } } private boolean hasValidAutoVoice() { try { if(hasValidTasker()) { PackageInfo pinfo = getPackageManager().getPackageInfo("com.joaomgcd.autovoice", 0); return true; } } catch(Exception e) { logger.d("Exception getting autovoice version: " + e.getStackTrace()); } return false; } private boolean hasValidUtter() { try { if(hasValidTasker()) { PackageInfo pinfo = getPackageManager().getPackageInfo("com.brandall.nutter", 0); return true; } } catch(Exception e) { logger.d("Exception getting utter version: " + e.getStackTrace()); } return false; } private boolean hasValidTasker() { PackageInfo pinfo; try { pinfo = getPackageManager().getPackageInfo("net.dinglisch.android.tasker", 0); return true; } catch(Exception e) {} try { pinfo = getPackageManager().getPackageInfo("net.dinglisch.android.taskerm", 0); return true; } catch(Exception e) { logger.d("Exception getting tasker version: " + e.getStackTrace()); } return false; } protected void setCastIconInactive() { logger.d("setCastIconInactive"); try { castIconMenuItem.setIcon(R.drawable.mr_ic_media_route_holo_dark); } catch (Exception e) {} } protected void setCastIconActive() { logger.d("setCastIconActive"); if(castIconMenuItem != null) { try { castIconMenuItem.setIcon(R.drawable.mr_ic_media_route_on_holo_dark); } catch (Exception e) { e.printStackTrace(); } } } @Override protected void onDestroy() { super.onDestroy(); logger.d("onDestroy"); networkMonitor.unregister(); } @Override public void setStream(Stream stream) { subscriptionService.setStream(stream); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); logger.d("Saving instance state"); outState.putParcelable(com.atomjack.shared.Intent.EXTRA_CLIENT, client); if(playerFragment != null && playerFragment.isVisible()) { getSupportFragmentManager().putFragment(outState, com.atomjack.shared.Intent.EXTRA_PLAYER_FRAGMENT, playerFragment); } if(musicPlayerFragment != null && musicPlayerFragment.isVisible()) { getSupportFragmentManager().putFragment(outState, com.atomjack.shared.Intent.EXTRA_MUSIC_PLAYER_FRAGMENT, musicPlayerFragment); } outState.putBoolean(com.atomjack.shared.Intent.EXTRA_MUSIC_PLAYER_IS_BOUND, musicPlayerIsBound); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { try { super.onRestoreInstanceState(savedInstanceState); } catch (Exception e) {} client = savedInstanceState.getParcelable(com.atomjack.shared.Intent.EXTRA_CLIENT); } @Override public void onDisconnected() { logger.d("Disconnected"); currentNetworkState = NetworkState.DISCONNECTED; // We have no network connection, so hide the cast button if(castIconMenuItem != null) castIconMenuItem.setVisible(false); } @Override public void onConnected(int connectionType) { logger.d("Connected with type %d", connectionType); // Only show the cast button if the previous state was disconnected. if(currentNetworkState == NetworkState.DISCONNECTED && castIconMenuItem != null) { castIconMenuItem.setVisible(true); } if(connectionType == ConnectivityManager.TYPE_MOBILE) currentNetworkState = NetworkState.MOBILE; else if(connectionType == ConnectivityManager.TYPE_WIFI) currentNetworkState = NetworkState.WIFI; if(isSubscribed()) { // If it's been more than 30 seconds since we last heard from the subscribed client, force a (non-heartbeat) // subscription request right now to refresh. It shouldn't be a heartbeat request in case the client // booted us off for being unreachable for 90 seconds. if(subscriptionService.timeLastHeardFromClient != null) { if((new Date().getTime() - subscriptionService.timeLastHeardFromClient.getTime()) / 1000 >= 30) { subscriptionService.subscribe(subscriptionService.getClient(), true); } } } } @Override public void onLayoutNotFound() { // This is passed by PlayerFragment in the case where it is not able to tell which layout (tv/movie/music) to use. We should switch back to the main fragment switchToMainFragment(); } private MainFragment getMainFragment() { if(mainFragment == null) mainFragment = new MainFragment(); return mainFragment; } private int getLayoutForMedia(PlexMedia media, PlayerState state) { if(media == null) return -1; if(playerFragment == null) { playerFragment = client.isCastClient ? new CastPlayerFragment() : new PlexPlayerFragment(); } else if(client.isCastClient && playerFragment instanceof PlexPlayerFragment) playerFragment = new CastPlayerFragment(); else if(!client.isCastClient && playerFragment instanceof CastPlayerFragment) playerFragment = new PlexPlayerFragment(); playerFragment.setRetainInstance(false); playerFragment.setState(state); playerFragment.setPosition(Integer.parseInt(media.viewOffset)/1000); // View offset from PMS is in ms int layout = -1; if(media.isMovie() || media.isClip()) layout = R.layout.now_playing_movie; else if(media.isShow()) layout = R.layout.now_playing_show; else if(media.isMusic()) layout = R.layout.now_playing_music; return layout; } private void checkForMissingPlexEmail() { if(prefs.get(Preferences.PLEX_EMAIL, null) == null && authToken != null) { PlexHttpClient.getPlexAccount(authToken, new PlexHttpUserHandler() { @Override public void onSuccess(PlexUser user) { prefs.put(Preferences.PLEX_EMAIL, user.email); init(); } @Override public void onFailure(int statusCode) { logger.d("Failure: %d",statusCode); } }); } } @Override public boolean dispatchKeyEvent(KeyEvent event) { double VOLUME_INCREMENT = 0.05; if(subscriptionService.isSubscribed()) { int action = event.getAction(); int keyCode = event.getKeyCode(); if(keyCode == KeyEvent.KEYCODE_VOLUME_UP) { if(action == KeyEvent.ACTION_DOWN) { double currentVolume = subscriptionService.getVolume(); if(currentVolume < 1.0) { subscriptionService.setVolume(Math.min(currentVolume + VOLUME_INCREMENT, 1.0)); } } return true; } else if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { if(action == KeyEvent.ACTION_DOWN) { double currentVolume = subscriptionService.getVolume(); if(currentVolume > 0.0) { subscriptionService.setVolume(Math.max(currentVolume - VOLUME_INCREMENT, 0.0)); } } return true; } } return super.dispatchKeyEvent(event); } @Override public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { final String pref = settingErrorFeedback ? Preferences.ERRORS_VOICE : Preferences.FEEDBACK_VOICE; if (availableVoices != null) { AlertDialog.Builder adb = new AlertDialog.Builder(this); View view = getLayoutInflater().inflate(R.layout.popup_language_selector, null); adb.setView(view); final CharSequence items[] = availableVoices.toArray(new CharSequence[availableVoices.size()]); int selectedVoice = -1; String v = VoiceControlForPlexApplication.getInstance().prefs.get(pref, "Locale.US"); if (availableVoices.indexOf(v) > -1) selectedVoice = availableVoices.indexOf(v); final AlertDialog dialog = adb.create(); Button languageSelectorCancelButton = (Button)view.findViewById(R.id.languageSelectorCancelButton); languageSelectorCancelButton.setOnClickListener(v1 -> dialog.cancel()); RadioGroup languageSelectorRadioGroup = (RadioGroup)view.findViewById(R.id.languageSelectorRadioGroup); LinearLayout.LayoutParams layoutParams = new RadioGroup.LayoutParams( RadioGroup.LayoutParams.WRAP_CONTENT, RadioGroup.LayoutParams.WRAP_CONTENT); for(int i=0;i<items.length;i++) { RadioButton button = (RadioButton)getLayoutInflater().inflate(R.layout.popup_video_options_button, null); button.setText(items[i]); button.setId(i); languageSelectorRadioGroup.addView(button, layoutParams); } languageSelectorRadioGroup.check(selectedVoice); languageSelectorRadioGroup.setOnCheckedChangeListener((group, checkedId) -> { VoiceControlForPlexApplication.getInstance().prefs.put(pref, items[checkedId].toString()); dialog.dismiss(); }); dialog.show(); } else { VoiceControlForPlexApplication.getInstance().prefs.put(pref, "Locale.US"); } } } private MusicConnection musicConnection = new MusicConnection(); class MusicConnection implements ServiceConnection { private Runnable runnable; @Override public void onServiceConnected(ComponentName name, IBinder service) { LocalMusicService.MusicBinder binder = (LocalMusicService.MusicBinder)service; localMusicService = binder.getService(); musicPlayerIsBound = true; logger.d("Got local music service"); binder.setListener(new MusicServiceListener() { @Override public void onTimeUpdate(PlayerState state, int time) { musicPlayerFragment.onTimeUpdate(state, time); } @Override public void onTrackChange(PlexTrack track) { if(musicPlayerFragment != null) musicPlayerFragment.onTrackChange(track); } @Override public void onFinished() { logger.d("MusicConnection onFinished"); handler.post(() -> { switchToMainFragment(); musicPlayerFragment = null; getApplicationContext().stopService(musicServiceIntent); if(musicPlayerIsBound) getApplicationContext().unbindService(musicConnection); musicPlayerIsBound = false; }); } }); if(runnable != null) runnable.run(); } @Override public void onServiceDisconnected(ComponentName name) { logger.d("onServiceDisconnected"); musicPlayerIsBound = false; } public void setOnConnected(Runnable runnable) { this.runnable = runnable; } } private void bindMusicPlayerService() { musicServiceIntent = new Intent(getApplicationContext(), LocalMusicService.class); getApplicationContext().bindService(musicServiceIntent, musicConnection, Context.BIND_AUTO_CREATE); getApplicationContext().startService(musicServiceIntent); } // Implement MusicPlayerListener. Pass actions from music player fragment into the music player service @Override public void doNext() { localMusicService.doNext(); } @Override public void doPlay() { localMusicService.doPlay(); } @Override public void doPause() { localMusicService.doPause(); } @Override public void doPrevious() { localMusicService.doPrevious(); } @Override public void doStop() { localMusicService.doStop(); } @Override public PlexTrack getTrack() { return localMusicService.getTrack(); } @Override public void seek(int time) { localMusicService.seek(time); } @Override public boolean isPlaying() { return localMusicService.isPlaying(); } // End implement MusicPlayerListener private SubscriptionConnection subscriptionConnection = new SubscriptionConnection(); class SubscriptionConnection implements ServiceConnection { private Runnable runnable; private boolean binding = false; @Override public void onServiceConnected(ComponentName name, IBinder service) { binding = false; SubscriptionService.SubscriptionBinder binder = (SubscriptionService.SubscriptionBinder)service; subscriptionService = binder.getService(); subscriptionServiceIsBound = true; logger.d("Got subscription service, subscribed: %s", subscriptionService.isSubscribed()); subscriptionService.setListener(plexSubscriptionListener); if(!isSubscribed() && gsonRead.fromJson(prefs.get(Preferences.SUBSCRIBED_CLIENT, ""), PlexClient.class) != null) { logger.d("found subbed client"); if(prefs.get(Preferences.CRASHED, false)) { prefs.remove(Preferences.SUBSCRIBED_CLIENT); } else { client = gsonRead.fromJson(prefs.get(Preferences.SUBSCRIBED_CLIENT, ""), PlexClient.class); subscriptionService.subscribe(client, !subscriptionService.isSubscribed()); } prefs.put(Preferences.CRASHED, false); } if(!isSubscribed()) { // In case the notification is still up due to a crash VoiceControlForPlexApplication.getInstance().cancelNotification(); // If we get unsubscribed from the notification, and the app isn't visible, the next time we show up the app will think // it's still subscribed, so we have to set the UI to be unsubbed. Also, need to make sure we're not in the middle of subscribing, // as that will happen when a voice search is done to play something - this activity will be launched before the subscribe // process is done. If this isn't checked, we end up switching to the main fragment when we should stay with the player fragment, and crash. if (!isSubscribing() && !doingFirstTimeSetup && mainFragment != null && (mainFragment == null || !mainFragment.isVisible())) { switchToMainFragment(); logger.d("issubbed: %s, bound: %s", isSubscribed(), subscriptionServiceIsBound); setCastIconInactive(); prefs.remove(Preferences.SUBSCRIBED_CLIENT); } } else { // If the screen is turned off, and playback on the subscribed client stops, when the app launches the next time, switch to the main screen if(subscriptionService.getCurrentState().equals(PlayerState.STOPPED) && (mainFragment == null || !mainFragment.isVisible())) { switchToMainFragment(); } } if(runnable != null) runnable.run(); runnable = null; } @Override public void onServiceDisconnected(ComponentName name) { logger.d("onServiceDisconnected"); subscriptionServiceIsBound = false; } public void setOnConnected(Runnable runnable) { this.runnable = runnable; } } private void bindSubscriptionService() { bindSubscriptionService(null); } private void bindSubscriptionService(Runnable onConnected) { subscriptionConnection.binding = true; subscriptionConnection.setOnConnected(onConnected); subscriptionServiceIntent = new Intent(getApplicationContext(), SubscriptionService.class); getApplicationContext().bindService(subscriptionServiceIntent, subscriptionConnection, Context.BIND_AUTO_CREATE); getApplicationContext().startService(subscriptionServiceIntent); } @Override public SubscriptionService getSubscriptionService() { return subscriptionService; } }