From f0917820cb9f261afbf512703fa0ef80a83d8da1 Mon Sep 17 00:00:00 2001 From: Nite Date: Sat, 6 Feb 2021 11:50:57 +0100 Subject: [PATCH] Migrated parts from SubsonicTabActivity, fixed theme changes --- .../activity/SubsonicTabActivity.java | 273 ------------------ .../ultrasonic/activity/NavigationActivity.kt | 77 +++++ .../util/SubsonicUncaughtExceptionHandler.kt | 40 +++ ultrasonic/src/main/res/values/themes.xml | 6 + 4 files changed, 123 insertions(+), 273 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java index 3809896a..d8ce6438 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/SubsonicTabActivity.java @@ -101,8 +101,6 @@ public class SubsonicTabActivity extends ResultActivity @Override protected void onCreate(Bundle bundle) { - setUncaughtExceptionHandler(); - Util.applyTheme(this); super.onCreate(bundle); setVolumeControlStream(AudioManager.STREAM_MUSIC); @@ -418,185 +416,6 @@ public class SubsonicTabActivity extends ResultActivity } } - @Override - protected Dialog onCreateDialog(final int id) - { - if (id == DIALOG_ASK_FOR_SHARE_DETAILS) - { - final LayoutInflater layoutInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); - final View layout = layoutInflater.inflate(R.layout.share_details, (ViewGroup) findViewById(R.id.share_details)); - - if (layout != null) - { - shareDescription = (EditText) layout.findViewById(R.id.share_description); - hideDialogCheckBox = (CheckBox) layout.findViewById(R.id.hide_dialog); - noExpirationCheckBox = (CheckBox) layout.findViewById(R.id.timeSpanDisableCheckBox); - saveAsDefaultsCheckBox = (CheckBox) layout.findViewById(R.id.save_as_defaults); - timeSpanPicker = (TimeSpanPicker) layout.findViewById(R.id.date_picker); - } - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.share_set_share_options); - - builder.setPositiveButton(R.string.common_save, new DialogInterface.OnClickListener() - { - @Override - public void onClick(final DialogInterface dialog, final int clickId) - { - if (!noExpirationCheckBox.isChecked()) - { - TimeSpan timeSpan = timeSpanPicker.getTimeSpan(); - TimeSpan now = TimeSpan.getCurrentTime(); - shareDetails.Expiration = now.add(timeSpan).getTotalMilliseconds(); - } - - shareDetails.Description = String.valueOf(shareDescription.getText()); - - if (hideDialogCheckBox.isChecked()) - { - Util.setShouldAskForShareDetails(SubsonicTabActivity.this, false); - } - - if (saveAsDefaultsCheckBox.isChecked()) - { - String timeSpanType = timeSpanPicker.getTimeSpanType(); - int timeSpanAmount = timeSpanPicker.getTimeSpanAmount(); - Util.setDefaultShareExpiration(SubsonicTabActivity.this, !noExpirationCheckBox.isChecked() && timeSpanAmount > 0 ? String.format("%d:%s", timeSpanAmount, timeSpanType) : ""); - Util.setDefaultShareDescription(SubsonicTabActivity.this, shareDetails.Description); - } - - share(); - } - }); - - builder.setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() - { - @Override - public void onClick(final DialogInterface dialog, final int clickId) - { - shareDetails = null; - dialog.cancel(); - } - }); - builder.setView(layout); - builder.setCancelable(true); - - timeSpanPicker.setTimeSpanDisableText(getResources().getString(R.string.no_expiration)); - - noExpirationCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() - { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) - { - timeSpanPicker.setEnabled(!b); - } - }); - - String defaultDescription = Util.getDefaultShareDescription(this); - String timeSpan = Util.getDefaultShareExpiration(this); - - String[] split = COMPILE.split(timeSpan); - - if (split.length == 2) - { - int timeSpanAmount = Integer.parseInt(split[0]); - String timeSpanType = split[1]; - - if (timeSpanAmount > 0) - { - noExpirationCheckBox.setChecked(false); - timeSpanPicker.setEnabled(true); - timeSpanPicker.setTimeSpanAmount(String.valueOf(timeSpanAmount)); - timeSpanPicker.setTimeSpanType(timeSpanType); - } - else - { - noExpirationCheckBox.setChecked(true); - timeSpanPicker.setEnabled(false); - } - } - else - { - noExpirationCheckBox.setChecked(true); - timeSpanPicker.setEnabled(false); - } - - shareDescription.setText(defaultDescription); - - return builder.create(); - } - else - { - return super.onCreateDialog(id); - } - } - - public void createShare(final List entries) - { - boolean askForDetails = Util.getShouldAskForShareDetails(this); - - shareDetails = new ShareDetails(); - shareDetails.Entries = entries; - - if (askForDetails) - { - showDialog(DIALOG_ASK_FOR_SHARE_DETAILS); - } - else - { - shareDetails.Description = Util.getDefaultShareDescription(this); - shareDetails.Expiration = TimeSpan.getCurrentTime().add(Util.getDefaultShareExpirationInMillis(this)).getTotalMilliseconds(); - share(); - } - } - - public void share() - { - BackgroundTask task = new TabActivityBackgroundTask(this, true) - { - @Override - protected Share doInBackground() throws Throwable - { - List ids = new ArrayList(); - - if (shareDetails.Entries.isEmpty()) - { - ids.add(getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ID)); - } - else - { - for (Entry entry : shareDetails.Entries) - { - ids.add(entry.getId()); - } - } - - MusicService musicService = MusicServiceFactory.getMusicService(SubsonicTabActivity.this); - - long timeInMillis = 0; - - if (shareDetails.Expiration != 0) - { - timeInMillis = shareDetails.Expiration; - } - - List shares = musicService.createShare(ids, shareDetails.Description, timeInMillis, SubsonicTabActivity.this, this); - return shares.get(0); - } - - @Override - protected void done(Share result) - { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, String.format("%s\n\n%s", Util.getShareGreeting(SubsonicTabActivity.this), result.getUrl())); - startActivity(Intent.createChooser(intent, getResources().getString(R.string.share_via))); - } - }; - - task.execute(); - } - public void setOnTouchListenerOnUiThread(final View view, final OnTouchListener listener) { this.runOnUiThread(new Runnable() @@ -682,18 +501,6 @@ public class SubsonicTabActivity extends ResultActivity return mediaPlayerControllerLazy.getValue(); } - protected void warnIfNetworkOrStorageUnavailable() - { - if (!Util.isExternalStoragePresent()) - { - Util.toast(this, R.string.select_album_no_sdcard); - } - else if (!ActiveServerProvider.Companion.isOffline(this) && !Util.isNetworkConnected(this)) - { - Util.toast(this, R.string.select_album_no_network); - } - } - protected void setActionBarDisplayHomeAsUp(boolean enabled) { ActionBar actionBar = getSupportActionBar(); @@ -704,86 +511,6 @@ public class SubsonicTabActivity extends ResultActivity } } - protected void setActionBarSubtitle(CharSequence title) - { - ActionBar actionBar = getSupportActionBar(); - - if (actionBar != null) - { - actionBar.setSubtitle(title); - } - } - - protected void setActionBarSubtitle(int id) - { - ActionBar actionBar = getSupportActionBar(); - - if (actionBar != null) - { - actionBar.setSubtitle(id); - } - } - - private void setUncaughtExceptionHandler() - { - Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler(); - if (!(handler instanceof SubsonicUncaughtExceptionHandler)) - { - Thread.setDefaultUncaughtExceptionHandler(new SubsonicUncaughtExceptionHandler(this)); - } - } - - /** - * Logs the stack trace of uncaught exceptions to a file on the SD card. - */ - private static class SubsonicUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler - { - - private final Thread.UncaughtExceptionHandler defaultHandler; - private final Context context; - private static final String filename = "ultrasonic-stacktrace.txt"; - - private SubsonicUncaughtExceptionHandler(Context context) - { - this.context = context; - defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); - } - - @Override - public void uncaughtException(Thread thread, Throwable throwable) - { - File file = null; - PrintWriter printWriter = null; - - try - { - file = new File(FileUtil.getUltrasonicDirectory(context), filename); - printWriter = new PrintWriter(file); - - String logMessage = String.format( - "Android API level: %s\nUltrasonic version name: %s\nUltrasonic version code: %s\n\n", - Build.VERSION.SDK_INT, Util.getVersionName(context), Util.getVersionCode(context)); - - printWriter.println(logMessage); - throwable.printStackTrace(printWriter); - Timber.e(throwable, "Uncaught Exception! %s", logMessage); - Timber.i("Stack trace written to %s", file); - } - catch (Throwable x) - { - Timber.e(x, "Failed to write stack trace to %s", file); - } - finally - { - Util.close(printWriter); - if (defaultHandler != null) - { - defaultHandler.uncaughtException(thread, throwable); - } - } - } - } - @Override protected void onRestoreInstanceState(Bundle inState) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index d91a782e..aff46138 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -4,10 +4,12 @@ import android.app.AlertDialog import android.app.SearchManager import android.content.Intent import android.content.res.Resources +import android.media.AudioManager import android.os.Bundle import android.preference.PreferenceManager import android.provider.MediaStore import android.provider.SearchRecentSuggestions +import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity @@ -25,12 +27,14 @@ import com.google.android.material.navigation.NavigationView import org.koin.android.ext.android.inject import org.koin.android.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.provider.SearchSuggestionProvider import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.SubsonicUncaughtExceptionHandler import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -39,6 +43,11 @@ import timber.log.Timber * A simple activity demonstrating use of a NavHostFragment with a navigation drawer. */ class NavigationActivity : AppCompatActivity() { + var chatMenuItem: MenuItem? = null + var bookmarksMenuItem: MenuItem? = null + var sharesMenuItem: MenuItem? = null + private var theme: String? = null + private lateinit var appBarConfiguration : AppBarConfiguration private val serverSettingsModel: ServerSettingsModel by viewModel() private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() @@ -48,7 +57,12 @@ class NavigationActivity : AppCompatActivity() { private var infoDialogDisplayed = false override fun onCreate(savedInstanceState: Bundle?) { + setUncaughtExceptionHandler() + Util.applyTheme(this) + super.onCreate(savedInstanceState) + + volumeControlStream = AudioManager.STREAM_MUSIC setContentView(R.layout.navigation_activity) val toolbar = findViewById(R.id.toolbar) @@ -78,6 +92,15 @@ class NavigationActivity : AppCompatActivity() { Integer.toString(destination.id) } Timber.d("Navigated to $dest") + + // TODO: Maybe we can find a better place for theme change. Currently the change occures when navigating between fragments + // but theoretically Settings could request a Navigation Activity recreate instantly when the theme setting changes + // Make sure to update theme if it has changed + if (theme == null) theme = Util.getTheme(this) + else if (theme != Util.getTheme(this)) { + theme = Util.getTheme(this) + recreate() + } } // Determine first run and migrate server settings to DB as early as possible @@ -91,6 +114,49 @@ class NavigationActivity : AppCompatActivity() { showInfoDialog(showWelcomeScreen) } + override fun onResume() { + super.onResume() + val visibility = !isOffline(this) + chatMenuItem?.isVisible = visibility + bookmarksMenuItem?.isVisible = visibility + sharesMenuItem?.isVisible = visibility + + Util.registerMediaButtonEventReceiver(this, false) + // Lifecycle support's constructor registers some event receivers so it should be created early + lifecycleSupport.onCreate() + + // TODO: Implement NowPlaying as a Fragment + // This must be filled here because onCreate is called before the derived objects would call setContentView + //getNowPlayingView() + + if (!SubsonicTabActivity.nowPlayingHidden) { + //showNowPlaying() + } else { + //hideNowPlaying() + } + } + + override fun onDestroy() { + Util.unregisterMediaButtonEventReceiver(this, false) + super.onDestroy() + + // TODO: Handle NowPlaying if necessary + //nowPlayingView = null + imageLoaderProvider.clearImageLoader() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + val isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + val isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP + val isVolumeAdjust = isVolumeDown || isVolumeUp + val isJukebox = mediaPlayerController.isJukeboxEnabled + if (isVolumeAdjust && isJukebox) { + mediaPlayerController.adjustJukeboxVolume(isVolumeUp) + return true + } + return super.onKeyDown(keyCode, event) + } + private fun setupNavigationMenu(navController: NavController) { val sideNavView = findViewById(R.id.nav_view) sideNavView?.setupWithNavController(navController) @@ -107,6 +173,10 @@ class NavigationActivity : AppCompatActivity() { } true } + + chatMenuItem = sideNavView.menu.findItem(R.id.menu_chat) + bookmarksMenuItem = sideNavView.menu.findItem(R.id.menu_bookmarks) + sharesMenuItem = sideNavView.menu.findItem(R.id.menu_shares) } private fun setupActionBar(navController: NavController, appBarConfig: AppBarConfiguration) { @@ -182,4 +252,11 @@ class NavigationActivity : AppCompatActivity() { } } } + + private fun setUncaughtExceptionHandler() { + val handler = Thread.getDefaultUncaughtExceptionHandler() + if (handler !is SubsonicUncaughtExceptionHandler) { + Thread.setDefaultUncaughtExceptionHandler(SubsonicUncaughtExceptionHandler(this)) + } + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt new file mode 100644 index 00000000..4e26bc54 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt @@ -0,0 +1,40 @@ +package org.moire.ultrasonic.util + +import android.content.Context +import android.os.Build +import timber.log.Timber +import java.io.File +import java.io.PrintWriter + +private const val filename = "ultrasonic-stacktrace.txt" + +/** + * Logs the stack trace of uncaught exceptions to a file on the SD card. + */ +class SubsonicUncaughtExceptionHandler ( + private val context: Context + ) : Thread.UncaughtExceptionHandler { + private val defaultHandler: Thread.UncaughtExceptionHandler? = Thread.getDefaultUncaughtExceptionHandler() + + override fun uncaughtException(thread: Thread, throwable: Throwable) { + var file: File? = null + var printWriter: PrintWriter? = null + + try { + file = File(FileUtil.getUltrasonicDirectory(context), filename) + printWriter = PrintWriter(file) + val logMessage = String.format( + "Android API level: %s\nUltrasonic version name: %s\nUltrasonic version code: %s\n\n", + Build.VERSION.SDK_INT, Util.getVersionName(context), Util.getVersionCode(context)) + printWriter.println(logMessage) + throwable.printStackTrace(printWriter) + Timber.e(throwable, "Uncaught Exception! %s", logMessage) + Timber.i("Stack trace written to %s", file) + } catch (x: Throwable) { + Timber.e(x, "Failed to write stack trace to %s", file) + } finally { + Util.close(printWriter) + defaultHandler?.uncaughtException(thread, throwable) + } + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/res/values/themes.xml b/ultrasonic/src/main/res/values/themes.xml index 3948a79e..dba21cd6 100644 --- a/ultrasonic/src/main/res/values/themes.xml +++ b/ultrasonic/src/main/res/values/themes.xml @@ -1,6 +1,8 @@