Migrated parts from SubsonicTabActivity, fixed theme changes

This commit is contained in:
Nite 2021-02-06 11:50:57 +01:00
parent a395bd6feb
commit f0917820cb
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
4 changed files with 123 additions and 273 deletions

View File

@ -101,8 +101,6 @@ public class SubsonicTabActivity extends ResultActivity
@Override @Override
protected void onCreate(Bundle bundle) protected void onCreate(Bundle bundle)
{ {
setUncaughtExceptionHandler();
Util.applyTheme(this);
super.onCreate(bundle); super.onCreate(bundle);
setVolumeControlStream(AudioManager.STREAM_MUSIC); 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<Entry> 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<Share> task = new TabActivityBackgroundTask<Share>(this, true)
{
@Override
protected Share doInBackground() throws Throwable
{
List<String> ids = new ArrayList<String>();
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<Share> 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) public void setOnTouchListenerOnUiThread(final View view, final OnTouchListener listener)
{ {
this.runOnUiThread(new Runnable() this.runOnUiThread(new Runnable()
@ -682,18 +501,6 @@ public class SubsonicTabActivity extends ResultActivity
return mediaPlayerControllerLazy.getValue(); 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) protected void setActionBarDisplayHomeAsUp(boolean enabled)
{ {
ActionBar actionBar = getSupportActionBar(); 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 @Override
protected void onRestoreInstanceState(Bundle inState) protected void onRestoreInstanceState(Bundle inState)
{ {

View File

@ -4,10 +4,12 @@ import android.app.AlertDialog
import android.app.SearchManager import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.media.AudioManager
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.SearchRecentSuggestions import android.provider.SearchRecentSuggestions
import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity 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.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel import org.koin.android.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.provider.SearchSuggestionProvider import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.SubsonicUncaughtExceptionHandler
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
@ -39,6 +43,11 @@ import timber.log.Timber
* A simple activity demonstrating use of a NavHostFragment with a navigation drawer. * A simple activity demonstrating use of a NavHostFragment with a navigation drawer.
*/ */
class NavigationActivity : AppCompatActivity() { 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 lateinit var appBarConfiguration : AppBarConfiguration
private val serverSettingsModel: ServerSettingsModel by viewModel() private val serverSettingsModel: ServerSettingsModel by viewModel()
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject() private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
@ -48,7 +57,12 @@ class NavigationActivity : AppCompatActivity() {
private var infoDialogDisplayed = false private var infoDialogDisplayed = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setUncaughtExceptionHandler()
Util.applyTheme(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
volumeControlStream = AudioManager.STREAM_MUSIC
setContentView(R.layout.navigation_activity) setContentView(R.layout.navigation_activity)
val toolbar = findViewById<Toolbar>(R.id.toolbar) val toolbar = findViewById<Toolbar>(R.id.toolbar)
@ -78,6 +92,15 @@ class NavigationActivity : AppCompatActivity() {
Integer.toString(destination.id) Integer.toString(destination.id)
} }
Timber.d("Navigated to $dest") 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 // Determine first run and migrate server settings to DB as early as possible
@ -91,6 +114,49 @@ class NavigationActivity : AppCompatActivity() {
showInfoDialog(showWelcomeScreen) 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) { private fun setupNavigationMenu(navController: NavController) {
val sideNavView = findViewById<NavigationView>(R.id.nav_view) val sideNavView = findViewById<NavigationView>(R.id.nav_view)
sideNavView?.setupWithNavController(navController) sideNavView?.setupWithNavController(navController)
@ -107,6 +173,10 @@ class NavigationActivity : AppCompatActivity() {
} }
true 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) { 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))
}
}
} }

View File

@ -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)
}
}
}

View File

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="UltrasonicTheme.Black" parent="Theme.MaterialComponents"> <style name="UltrasonicTheme.Black" parent="Theme.MaterialComponents">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="color_background">@color/background_color_dark</item> <item name="color_background">@color/background_color_dark</item>
<item name="color_selected">@color/selected_color_dark</item> <item name="color_selected">@color/selected_color_dark</item>
<item name="star_hollow">@drawable/ic_star_hollow_dark</item> <item name="star_hollow">@drawable/ic_star_hollow_dark</item>
@ -59,6 +61,8 @@
</style> </style>
<style name="UltrasonicTheme" parent="Theme.AppCompat"> <style name="UltrasonicTheme" parent="Theme.AppCompat">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="color_background">@color/background_color_dark</item> <item name="color_background">@color/background_color_dark</item>
<item name="color_selected">@color/selected_color_dark</item> <item name="color_selected">@color/selected_color_dark</item>
<item name="star_hollow">@drawable/ic_star_hollow_dark</item> <item name="star_hollow">@drawable/ic_star_hollow_dark</item>
@ -117,6 +121,8 @@
</style> </style>
<style name="UltrasonicTheme.Light" parent="Theme.AppCompat.Light"> <style name="UltrasonicTheme.Light" parent="Theme.AppCompat.Light">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="color_background">@color/background_color_light</item> <item name="color_background">@color/background_color_light</item>
<item name="color_selected">@color/selected_color_light</item> <item name="color_selected">@color/selected_color_light</item>
<item name="star_hollow">@drawable/ic_star_hollow_light</item> <item name="star_hollow">@drawable/ic_star_hollow_light</item>