1
0
mirror of https://github.com/ultrasonic/ultrasonic synced 2025-03-05 20:07:50 +01:00

Merge pull request #382 from nitehu/refactor/menudrawer_to_navigationui

Refactor menudrawer to navigationui
This commit is contained in:
Nite 2021-03-07 15:21:03 +01:00 committed by GitHub
commit b235acf521
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
154 changed files with 9129 additions and 15908 deletions

View File

@ -1,13 +0,0 @@
apply from: bootstrap.androidModule
android {
lintOptions {
baselineFile file("lint-baseline.xml")
abortOnError true
}
}
dependencies {
implementation "androidx.appcompat:appcompat-resources:1.2.0"
implementation other.timber
}

View File

@ -1,246 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="4" by="lint 2.3.3">
<issue
id="InlinedApi"
message="Field requires API level 17 (current min is 14): `android.view.View#LAYOUT_DIRECTION_RTL`"
errorLine1=" if (mSlideDrawable != null) mSlideDrawable.setIsRtl(layoutDirection == LAYOUT_DIRECTION_RTL);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/MenuDrawer.java"
line="882"
column="80"/>
</issue>
<issue
id="InlinedApi"
message="Field requires API level 17 (current min is 14): `android.view.View#LAYOUT_DIRECTION_RTL`"
errorLine1=" mSlideDrawable.setIsRtl(ViewHelper.getLayoutDirection(this) == LAYOUT_DIRECTION_RTL);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/MenuDrawer.java"
line="1325"
column="72"/>
</issue>
<issue
id="OldTargetApi"
message="Not targeting the latest versions of Android; compatibility modes apply. Consider testing and updating this version. Consult the `android.os.Build.VERSION_CODES` javadoc for details."
errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;7&quot; android:targetSdkVersion=&quot;16&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="7"
column="41"/>
</issue>
<issue
id="GradleOverrides"
message="This `minSdkVersion` value (`7`) is not used; it is always overridden by the value specified in the Gradle build script (`14`)"
errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;7&quot; android:targetSdkVersion=&quot;16&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="7"
column="15"/>
</issue>
<issue
id="GradleOverrides"
message="This `targetSdkVersion` value (`16`) is not used; it is always overridden by the value specified in the Gradle build script (`22`)"
errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;7&quot; android:targetSdkVersion=&quot;16&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="7"
column="41"/>
</issue>
<issue
id="ParcelClassLoader"
message="Using the default class loader will not work if you are restoring your own classes. Consider using for example `readBundle(getClass().getClassLoader())` instead."
errorLine1=" mState = in.readBundle();"
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/MenuDrawer.java"
line="1630"
column="25"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is never &lt; 14"
errorLine1=" if (mUsesCompat &amp;&amp; Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.ICE_CREAM_SANDWICH) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="41"
column="28"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="43"
column="20"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is never &lt; 14"
errorLine1=" if (mUsesCompat &amp;&amp; Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.ICE_CREAM_SANDWICH) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="51"
column="28"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="53"
column="20"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is never &lt; 14"
errorLine1=" if (mUsesCompat &amp;&amp; Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.ICE_CREAM_SANDWICH) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="59"
column="28"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="61"
column="20"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is never &lt; 14"
errorLine1=" if (mUsesCompat &amp;&amp; Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.ICE_CREAM_SANDWICH) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="67"
column="28"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="69"
column="20"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is never &lt; 14"
errorLine1=" if (mUsesCompat &amp;&amp; Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.ICE_CREAM_SANDWICH) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="77"
column="28"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="79"
column="20"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/DraggableDrawer.java"
line="572"
column="13"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/DraggableDrawer.java"
line="580"
column="13"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/DraggableDrawer.java"
line="588"
column="13"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/DraggableDrawer.java"
line="596"
column="13"/>
</issue>
<issue
id="FloatMath"
message="Use `java.lang.Math#sqrt` instead of `android.util.FloatMath#sqrt()` since it is faster as of API 8"
errorLine1=" float hyp = FloatMath.sqrt(dx * dx + dy * dy);"
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/Scroller.java"
line="374"
column="25"/>
</issue>
<issue
id="FloatMath"
message="Use `java.lang.Math#sqrt` instead of `android.util.FloatMath#sqrt()` since it is faster as of API 8"
errorLine1=" float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);"
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/Scroller.java"
line="391"
column="26"/>
</issue>
</issues>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.simonvt.menudrawer"
android:versionCode="6"
android:versionName="3.0.2">
</manifest>

View File

@ -1,99 +0,0 @@
package net.simonvt.menudrawer;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* FrameLayout which caches the hardware layer if available.
* <p/>
* If it's not posted twice the layer either wont be built on start, or it'll be built twice.
*/
class BuildLayerFrameLayout extends FrameLayout {
private boolean mChanged;
private boolean mHardwareLayersEnabled = true;
private boolean mAttached;
private boolean mFirst = true;
public BuildLayerFrameLayout(Context context) {
super(context);
if (MenuDrawer.USE_TRANSLATIONS) {
setLayerType(LAYER_TYPE_HARDWARE, null);
}
}
public BuildLayerFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
if (MenuDrawer.USE_TRANSLATIONS) {
setLayerType(LAYER_TYPE_HARDWARE, null);
}
}
public BuildLayerFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if (MenuDrawer.USE_TRANSLATIONS) {
setLayerType(LAYER_TYPE_HARDWARE, null);
}
}
void setHardwareLayersEnabled(boolean enabled) {
mHardwareLayersEnabled = enabled;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mAttached = true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mAttached = false;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (MenuDrawer.USE_TRANSLATIONS && mHardwareLayersEnabled) {
post(new Runnable() {
@Override
public void run() {
mChanged = true;
invalidate();
}
});
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mChanged && MenuDrawer.USE_TRANSLATIONS) {
post(new Runnable() {
@Override
public void run() {
if (mAttached) {
final int layerType = getLayerType();
// If it's already a hardware layer, it'll be built anyway.
if (layerType != LAYER_TYPE_HARDWARE || mFirst) {
mFirst = false;
setLayerType(LAYER_TYPE_HARDWARE, null);
buildLayer();
setLayerType(LAYER_TYPE_NONE, null);
}
}
}
});
mChanged = false;
}
}
}

View File

@ -1,170 +0,0 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.simonvt.menudrawer;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
/**
* A specialized Drawable that fills the Canvas with a specified color.
* Note that a ColorDrawable ignores the ColorFilter.
* <p/>
* <p>It can be defined in an XML file with the <code>&lt;color></code> element.</p>
*
* @attr ref android.R.styleable#ColorDrawable_color
*/
class ColorDrawable extends Drawable {
private ColorState mState;
private final Paint mPaint = new Paint();
/** Creates a new black ColorDrawable. */
public ColorDrawable() {
this(null);
}
/**
* Creates a new ColorDrawable with the specified color.
*
* @param color The color to draw.
*/
public ColorDrawable(int color) {
this(null);
setColor(color);
}
private ColorDrawable(ColorState state) {
mState = new ColorState(state);
}
@Override
public int getChangingConfigurations() {
return super.getChangingConfigurations() | mState.mChangingConfigurations;
}
@Override
public void draw(Canvas canvas) {
if ((mState.mUseColor >>> 24) != 0) {
mPaint.setColor(mState.mUseColor);
canvas.drawRect(getBounds(), mPaint);
}
}
/**
* Gets the drawable's color value.
*
* @return int The color to draw.
*/
public int getColor() {
return mState.mUseColor;
}
/**
* Sets the drawable's color value. This action will clobber the results of prior calls to
* {@link #setAlpha(int)} on this object, which side-affected the underlying color.
*
* @param color The color to draw.
*/
public void setColor(int color) {
if (mState.mBaseColor != color || mState.mUseColor != color) {
invalidateSelf();
mState.mBaseColor = mState.mUseColor = color;
}
}
/**
* Returns the alpha value of this drawable's color.
*
* @return A value between 0 and 255.
*/
public int getAlpha() {
return mState.mUseColor >>> 24;
}
/**
* Sets the color's alpha value.
*
* @param alpha The alpha value to set, between 0 and 255.
*/
public void setAlpha(int alpha) {
alpha += alpha >> 7; // make it 0..256
int baseAlpha = mState.mBaseColor >>> 24;
int useAlpha = baseAlpha * alpha >> 8;
int oldUseColor = mState.mUseColor;
mState.mUseColor = (mState.mBaseColor << 8 >>> 8) | (useAlpha << 24);
if (oldUseColor != mState.mUseColor) {
invalidateSelf();
}
}
/**
* Setting a color filter on a ColorDrawable has no effect.
*
* @param colorFilter Ignore.
*/
public void setColorFilter(ColorFilter colorFilter) {
}
public int getOpacity() {
switch (mState.mUseColor >>> 24) {
case 255:
return PixelFormat.OPAQUE;
case 0:
return PixelFormat.TRANSPARENT;
}
return PixelFormat.TRANSLUCENT;
}
@Override
public ConstantState getConstantState() {
mState.mChangingConfigurations = getChangingConfigurations();
return mState;
}
static final class ColorState extends ConstantState {
int mBaseColor; // base color, independent of setAlpha()
int mUseColor; // basecolor modulated by setAlpha()
int mChangingConfigurations;
ColorState(ColorState state) {
if (state != null) {
mBaseColor = state.mBaseColor;
mUseColor = state.mUseColor;
}
}
@Override
public Drawable newDrawable() {
return new ColorDrawable(this);
}
@Override
public Drawable newDrawable(Resources res) {
return new ColorDrawable(this);
}
@Override
public int getChangingConfigurations() {
return mChangingConfigurations;
}
}
}

View File

@ -1,619 +0,0 @@
package net.simonvt.menudrawer;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
public abstract class DraggableDrawer extends MenuDrawer {
/**
* Key used when saving menu visibility state.
*/
private static final String STATE_MENU_VISIBLE = "net.simonvt.menudrawer.MenuDrawer.menuVisible";
/**
* Interpolator used for peeking at the drawer.
*/
private static final Interpolator PEEK_INTERPOLATOR = new PeekInterpolator();
/**
* The maximum alpha of the dark menu overlay used for dimming the menu.
*/
protected static final int MAX_MENU_OVERLAY_ALPHA = 185;
/**
* Default delay from {@link #peekDrawer()} is called until first animation is run.
*/
private static final long DEFAULT_PEEK_START_DELAY = 5000;
/**
* Default delay between each subsequent animation, after {@link #peekDrawer()} has been called.
*/
private static final long DEFAULT_PEEK_DELAY = 10000;
/**
* The duration of the peek animation.
*/
protected static final int PEEK_DURATION = 5000;
/**
* Distance in dp from closed position from where the drawer is considered closed with regards to touch events.
*/
private static final int CLOSE_ENOUGH = 3;
protected static final int INVALID_POINTER = -1;
/**
* Slop before starting a drag.
*/
protected int mTouchSlop;
/**
* Runnable used when the peek animation is running.
*/
protected final Runnable mPeekRunnable = new Runnable() {
@Override
public void run() {
peekDrawerInvalidate();
}
};
/**
* Runnable used when animating the drawer open/closed.
*/
private final Runnable mDragRunnable = new Runnable() {
@Override
public void run() {
postAnimationInvalidate();
}
};
/**
* Indicates whether the drawer is currently being dragged.
*/
protected boolean mIsDragging;
/**
* The current pointer id.
*/
protected int mActivePointerId = INVALID_POINTER;
/**
* The initial X position of a drag.
*/
protected float mInitialMotionX;
/**
* The initial Y position of a drag.
*/
protected float mInitialMotionY;
/**
* The last X position of a drag.
*/
protected float mLastMotionX = -1;
/**
* The last Y position of a drag.
*/
protected float mLastMotionY = -1;
/**
* Default delay between each subsequent animation, after {@link #peekDrawer()} has been called.
*/
protected long mPeekDelay;
/**
* Scroller used for the peek drawer animation.
*/
protected Scroller mPeekScroller;
/**
* Velocity tracker used when animating the drawer open/closed after a drag.
*/
protected VelocityTracker mVelocityTracker;
/**
* Maximum velocity allowed when animating the drawer open/closed.
*/
protected int mMaxVelocity;
/**
* Indicates whether the menu should be offset when dragging the drawer.
*/
protected boolean mOffsetMenu = true;
/**
* Distance in px from closed position from where the drawer is considered closed with regards to touch events.
*/
protected int mCloseEnough;
/**
* Runnable used for first call to {@link #startPeek()} after {@link #peekDrawer()} has been called.
*/
private Runnable mPeekStartRunnable;
/**
* Scroller used when animating the drawer open/closed.
*/
private Scroller mScroller;
/**
* Indicates whether the current layer type is {@link android.view.View#LAYER_TYPE_HARDWARE}.
*/
protected boolean mLayerTypeHardware;
DraggableDrawer(Activity activity, int dragMode) {
super(activity, dragMode);
}
public DraggableDrawer(Context context) {
super(context);
}
public DraggableDrawer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DraggableDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
super.initDrawer(context, attrs, defStyle);
final ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledTouchSlop();
mMaxVelocity = configuration.getScaledMaximumFlingVelocity();
mScroller = new Scroller(context, MenuDrawer.SMOOTH_INTERPOLATOR);
mPeekScroller = new Scroller(context, DraggableDrawer.PEEK_INTERPOLATOR);
mCloseEnough = dpToPx(DraggableDrawer.CLOSE_ENOUGH);
}
public void toggleMenu(boolean animate) {
if (mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING) {
closeMenu(animate);
} else if (mDrawerState == STATE_CLOSED || mDrawerState == STATE_CLOSING) {
openMenu(animate);
}
}
public boolean isMenuVisible() {
return mMenuVisible;
}
public void setMenuSize(final int size) {
mMenuSize = size;
if (mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING) {
setOffsetPixels(mMenuSize);
}
requestLayout();
invalidate();
}
public void setOffsetMenuEnabled(boolean offsetMenu) {
if (offsetMenu != mOffsetMenu) {
mOffsetMenu = offsetMenu;
requestLayout();
invalidate();
}
}
public boolean getOffsetMenuEnabled() {
return mOffsetMenu;
}
public void peekDrawer() {
peekDrawer(DEFAULT_PEEK_START_DELAY, DEFAULT_PEEK_DELAY);
}
public void peekDrawer(long delay) {
peekDrawer(DEFAULT_PEEK_START_DELAY, delay);
}
public void peekDrawer(final long startDelay, final long delay) {
if (startDelay < 0) {
throw new IllegalArgumentException("startDelay must be zero or larger.");
}
if (delay < 0) {
throw new IllegalArgumentException("delay must be zero or larger");
}
removeCallbacks(mPeekRunnable);
removeCallbacks(mPeekStartRunnable);
mPeekDelay = delay;
mPeekStartRunnable = new Runnable() {
@Override
public void run() {
startPeek();
}
};
postDelayed(mPeekStartRunnable, startDelay);
}
public void setHardwareLayerEnabled(boolean enabled) {
if (enabled != mHardwareLayersEnabled) {
mHardwareLayersEnabled = enabled;
mMenuContainer.setHardwareLayersEnabled(enabled);
mContentContainer.setHardwareLayersEnabled(enabled);
stopLayerTranslation();
}
}
public int getTouchMode() {
return mTouchMode;
}
public void setTouchMode(int mode) {
if (mTouchMode != mode) {
mTouchMode = mode;
updateTouchAreaSize();
}
}
public void setTouchBezelSize(int size) {
mTouchBezelSize = size;
}
public int getTouchBezelSize() {
return mTouchBezelSize;
}
/**
* If possible, set the layer type to {@link android.view.View#LAYER_TYPE_HARDWARE}.
*/
protected void startLayerTranslation() {
if (USE_TRANSLATIONS && mHardwareLayersEnabled && !mLayerTypeHardware) {
mLayerTypeHardware = true;
mContentContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mMenuContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
}
/**
* If the current layer type is {@link android.view.View#LAYER_TYPE_HARDWARE}, this will set it to
* {@link View#LAYER_TYPE_NONE}.
*/
protected void stopLayerTranslation() {
if (mLayerTypeHardware) {
mLayerTypeHardware = false;
mContentContainer.setLayerType(View.LAYER_TYPE_NONE, null);
mMenuContainer.setLayerType(View.LAYER_TYPE_NONE, null);
}
}
/**
* Called when a drag has been ended.
*/
protected void endDrag() {
mIsDragging = false;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
/**
* Stops ongoing animation of the drawer.
*/
protected void stopAnimation() {
removeCallbacks(mDragRunnable);
mScroller.abortAnimation();
stopLayerTranslation();
}
/**
* Called when a drawer animation has successfully completed.
*/
private void completeAnimation() {
mScroller.abortAnimation();
final int finalX = mScroller.getFinalX();
setOffsetPixels(finalX);
setDrawerState(finalX == 0 ? STATE_CLOSED : STATE_OPEN);
stopLayerTranslation();
}
protected void cancelContentTouch() {
final long now = SystemClock.uptimeMillis();
final MotionEvent cancelEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).dispatchTouchEvent(cancelEvent);
}
mContentContainer.dispatchTouchEvent(cancelEvent);
cancelEvent.recycle();
}
/**
* Moves the drawer to the position passed.
*
* @param position The position the content is moved to.
* @param velocity Optional velocity if called by releasing a drag event.
* @param animate Whether the move is animated.
*/
protected void animateOffsetTo(int position, int velocity, boolean animate) {
endDrag();
endPeek();
final int startX = (int) mOffsetPixels;
final int dx = position - startX;
if (dx == 0 || !animate) {
setOffsetPixels(position);
setDrawerState(position == 0 ? STATE_CLOSED : STATE_OPEN);
stopLayerTranslation();
return;
}
int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000.f * Math.abs((float) dx / velocity));
} else {
duration = (int) (600.f * Math.abs((float) dx / mMenuSize));
}
duration = Math.min(duration, mMaxAnimationDuration);
animateOffsetTo(position, duration);
}
protected void animateOffsetTo(int position, int duration) {
final int startX = (int) mOffsetPixels;
final int dx = position - startX;
if (dx > 0) {
setDrawerState(STATE_OPENING);
mScroller.startScroll(startX, 0, dx, 0, duration);
} else {
setDrawerState(STATE_CLOSING);
mScroller.startScroll(startX, 0, dx, 0, duration);
}
startLayerTranslation();
postAnimationInvalidate();
}
/**
* Callback when each frame in the drawer animation should be drawn.
*/
private void postAnimationInvalidate() {
if (mScroller.computeScrollOffset()) {
final int oldX = (int) mOffsetPixels;
final int x = mScroller.getCurrX();
if (x != oldX) setOffsetPixels(x);
if (x != mScroller.getFinalX()) {
postOnAnimation(mDragRunnable);
return;
}
}
completeAnimation();
}
/**
* Starts peek drawer animation.
*/
protected void startPeek() {
initPeekScroller();
startLayerTranslation();
peekDrawerInvalidate();
}
protected abstract void initPeekScroller();
/**
* Callback when each frame in the peek drawer animation should be drawn.
*/
private void peekDrawerInvalidate() {
if (mPeekScroller.computeScrollOffset()) {
final int oldX = (int) mOffsetPixels;
final int x = mPeekScroller.getCurrX();
if (x != oldX) setOffsetPixels(x);
if (!mPeekScroller.isFinished()) {
postOnAnimation(mPeekRunnable);
return;
} else if (mPeekDelay > 0) {
mPeekStartRunnable = new Runnable() {
@Override
public void run() {
startPeek();
}
};
postDelayed(mPeekStartRunnable, mPeekDelay);
}
}
completePeek();
}
/**
* Called when the peek drawer animation has successfully completed.
*/
private void completePeek() {
mPeekScroller.abortAnimation();
setOffsetPixels(0);
setDrawerState(STATE_CLOSED);
stopLayerTranslation();
}
/**
* Stops ongoing peek drawer animation.
*/
protected void endPeek() {
removeCallbacks(mPeekStartRunnable);
removeCallbacks(mPeekRunnable);
stopLayerTranslation();
}
protected boolean isCloseEnough() {
return Math.abs(mOffsetPixels) <= mCloseEnough;
}
protected boolean canChildrenScroll(int dx, int dy, int x, int y) {
boolean canScroll = false;
switch (getPosition()) {
case LEFT:
case RIGHT:
if (!mMenuVisible) {
canScroll = canChildScrollHorizontally(mContentContainer, false, dx,
x - ViewHelper.getLeft(mContentContainer), y - ViewHelper.getTop(mContentContainer));
} else {
canScroll = canChildScrollHorizontally(mMenuContainer, false, dx,
x - ViewHelper.getLeft(mMenuContainer), y - ViewHelper.getTop(mContentContainer));
}
break;
case TOP:
case BOTTOM:
if (!mMenuVisible) {
canScroll = canChildScrollVertically(mContentContainer, false, dy,
x - ViewHelper.getLeft(mContentContainer), y - ViewHelper.getTop(mContentContainer));
} else {
canScroll = canChildScrollVertically(mMenuContainer, false, dy,
x - ViewHelper.getLeft(mMenuContainer), y - ViewHelper.getTop(mContentContainer));
}
}
return canScroll;
}
/**
* Tests scrollability within child views of v given a delta of dx.
*
* @param v View to test for horizontal scrollability
* @param checkV Whether the view should be checked for draggability
* @param dx Delta scrolled in pixels
* @param x X coordinate of the active touch point
* @param y Y coordinate of the active touch point
* @return true if child views of v can be scrolled by delta of dx.
*/
protected boolean canChildScrollHorizontally(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
final View child = group.getChildAt(i);
final int childLeft = child.getLeft() + supportGetTranslationX(child);
final int childRight = child.getRight() + supportGetTranslationX(child);
final int childTop = child.getTop() + supportGetTranslationY(child);
final int childBottom = child.getBottom() + supportGetTranslationY(child);
if (x >= childLeft && x < childRight && y >= childTop && y < childBottom
&& canChildScrollHorizontally(child, true, dx, x - childLeft, y - childTop)) {
return true;
}
}
}
return checkV && mOnInterceptMoveEventListener.isViewDraggable(v, dx, x, y);
}
/**
* Tests scrollability within child views of v given a delta of dx.
*
* @param v View to test for horizontal scrollability
* @param checkV Whether the view should be checked for draggability
* @param dx Delta scrolled in pixels
* @param x X coordinate of the active touch point
* @param y Y coordinate of the active touch point
* @return true if child views of v can be scrolled by delta of dx.
*/
protected boolean canChildScrollVertically(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
final View child = group.getChildAt(i);
final int childLeft = child.getLeft() + supportGetTranslationX(child);
final int childRight = child.getRight() + supportGetTranslationX(child);
final int childTop = child.getTop() + supportGetTranslationY(child);
final int childBottom = child.getBottom() + supportGetTranslationY(child);
if (x >= childLeft && x < childRight && y >= childTop && y < childBottom
&& canChildScrollVertically(child, true, dx, x - childLeft, y - childTop)) {
return true;
}
}
}
return checkV && mOnInterceptMoveEventListener.isViewDraggable(v, dx, x, y);
}
protected float getXVelocity(VelocityTracker velocityTracker) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
return velocityTracker.getXVelocity(mActivePointerId);
}
return velocityTracker.getXVelocity();
}
protected float getYVelocity(VelocityTracker velocityTracker) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
return velocityTracker.getYVelocity(mActivePointerId);
}
return velocityTracker.getYVelocity();
}
private int supportGetTranslationY(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
return (int) v.getTranslationY();
}
return 0;
}
private int supportGetTranslationX(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
return (int) v.getTranslationX();
}
return 0;
}
void saveState(Bundle state) {
final boolean menuVisible = mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING;
state.putBoolean(STATE_MENU_VISIBLE, menuVisible);
}
public void restoreState(Parcelable in) {
super.restoreState(in);
Bundle state = (Bundle) in;
final boolean menuOpen = state.getBoolean(STATE_MENU_VISIBLE);
if (menuOpen) {
openMenu(false);
} else {
setOffsetPixels(0);
}
mDrawerState = menuOpen ? STATE_OPEN : STATE_CLOSED;
}
}

View File

@ -1,175 +0,0 @@
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.simonvt.menudrawer;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
/**
* This class encapsulates scrolling. The duration of the scroll
* can be passed in the constructor and specifies the maximum time that
* the scrolling animation should take. Past this time, the scrolling is
* automatically moved to its final stage and computeScrollOffset()
* will always return false to indicate that scrolling is over.
*/
class FloatScroller {
private float mStart;
private float mFinal;
private float mCurr;
private long mStartTime;
private int mDuration;
private float mDurationReciprocal;
private float mDeltaX;
private boolean mFinished;
private Interpolator mInterpolator;
/**
* Create a Scroller with the specified interpolator. If the interpolator is
* null, the default (viscous) interpolator will be used. Specify whether or
* not to support progressive "flywheel" behavior in flinging.
*/
public FloatScroller(Interpolator interpolator) {
mFinished = true;
mInterpolator = interpolator;
}
/**
* Returns whether the scroller has finished scrolling.
*
* @return True if the scroller has finished scrolling, false otherwise.
*/
public final boolean isFinished() {
return mFinished;
}
/**
* Force the finished field to a particular value.
*
* @param finished The new finished value.
*/
public final void forceFinished(boolean finished) {
mFinished = finished;
}
/**
* Returns how long the scroll event will take, in milliseconds.
*
* @return The duration of the scroll in milliseconds.
*/
public final int getDuration() {
return mDuration;
}
/**
* Returns the current offset in the scroll.
*
* @return The new offset as an absolute distance from the origin.
*/
public final float getCurr() {
return mCurr;
}
/**
* Returns the start offset in the scroll.
*
* @return The start offset as an absolute distance from the origin.
*/
public final float getStart() {
return mStart;
}
/**
* Returns where the scroll will end. Valid only for "fling" scrolls.
*
* @return The final offset as an absolute distance from the origin.
*/
public final float getFinal() {
return mFinal;
}
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
float x = timePassed * mDurationReciprocal;
x = mInterpolator.getInterpolation(x);
mCurr = mStart + x * mDeltaX;
} else {
mCurr = mFinal;
mFinished = true;
}
return true;
}
public void startScroll(float start, float delta, int duration) {
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStart = start;
mFinal = start + delta;
mDeltaX = delta;
mDurationReciprocal = 1.0f / (float) mDuration;
}
/**
* Stops the animation. Contrary to {@link #forceFinished(boolean)},
* aborting the animating cause the scroller to move to the final x and y
* position
*
* @see #forceFinished(boolean)
*/
public void abortAnimation() {
mCurr = mFinal;
mFinished = true;
}
/**
* Extend the scroll animation. This allows a running animation to scroll
* further and longer, when used with {@link #setFinal(float)}.
*
* @param extend Additional time to scroll in milliseconds.
* @see #setFinal(float)
*/
public void extendDuration(int extend) {
int passed = timePassed();
mDuration = passed + extend;
mDurationReciprocal = 1.0f / mDuration;
mFinished = false;
}
/**
* Returns the time elapsed since the beginning of the scrolling.
*
* @return The elapsed time in milliseconds.
*/
public int timePassed() {
return (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
}
public void setFinal(float newVal) {
mFinal = newVal;
mDeltaX = mFinal - mStart;
mFinished = false;
}
}

View File

@ -1,28 +0,0 @@
package net.simonvt.menudrawer;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
/**
* FrameLayout which doesn't let touch events propagate to views positioned behind it in the view hierarchy.
*/
class NoClickThroughFrameLayout extends BuildLayerFrameLayout {
public NoClickThroughFrameLayout(Context context) {
super(context);
}
public NoClickThroughFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NoClickThroughFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
}

View File

@ -1,778 +0,0 @@
package net.simonvt.menudrawer;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
public class OverlayDrawer extends DraggableDrawer {
private int mPeekSize;
private Runnable mRevealRunnable = new Runnable() {
@Override
public void run() {
cancelContentTouch();
int animateTo = 0;
switch (getPosition()) {
case RIGHT:
case BOTTOM:
animateTo = -mPeekSize;
break;
default:
animateTo = mPeekSize;
break;
}
animateOffsetTo(animateTo, 250);
}
};
OverlayDrawer(Activity activity, int dragMode) {
super(activity, dragMode);
}
public OverlayDrawer(Context context) {
super(context);
}
public OverlayDrawer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public OverlayDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
super.initDrawer(context, attrs, defStyle);
super.addView(mContentContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
if (USE_TRANSLATIONS) {
mContentContainer.setLayerType(View.LAYER_TYPE_NONE, null);
}
mContentContainer.setHardwareLayersEnabled(false);
super.addView(mMenuContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
mPeekSize = dpToPx(20);
}
@Override
protected void drawOverlay(Canvas canvas) {
final int width = getWidth();
final int height = getHeight();
final int offsetPixels = (int) mOffsetPixels;
final float openRatio = Math.abs(mOffsetPixels) / mMenuSize;
switch (getPosition()) {
case LEFT:
mMenuOverlay.setBounds(offsetPixels, 0, width, height);
break;
case RIGHT:
mMenuOverlay.setBounds(0, 0, width + offsetPixels, height);
break;
case TOP:
mMenuOverlay.setBounds(0, offsetPixels, width, height);
break;
case BOTTOM:
mMenuOverlay.setBounds(0, 0, width, height + offsetPixels);
break;
}
mMenuOverlay.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * openRatio));
mMenuOverlay.draw(canvas);
}
@Override
public void openMenu(boolean animate) {
int animateTo = 0;
switch (getPosition()) {
case LEFT:
case TOP:
animateTo = mMenuSize;
break;
case RIGHT:
case BOTTOM:
animateTo = -mMenuSize;
break;
}
animateOffsetTo(animateTo, 0, animate);
}
@Override
public void closeMenu(boolean animate) {
animateOffsetTo(0, 0, animate);
}
@Override
protected void onOffsetPixelsChanged(int offsetPixels) {
if (USE_TRANSLATIONS) {
switch (getPosition()) {
case LEFT:
mMenuContainer.setTranslationX(offsetPixels - mMenuSize);
break;
case TOP:
mMenuContainer.setTranslationY(offsetPixels - mMenuSize);
break;
case RIGHT:
mMenuContainer.setTranslationX(offsetPixels + mMenuSize);
break;
case BOTTOM:
mMenuContainer.setTranslationY(offsetPixels + mMenuSize);
break;
}
} else {
switch (getPosition()) {
case TOP:
mMenuContainer.offsetTopAndBottom(offsetPixels - mMenuContainer.getBottom());
break;
case BOTTOM:
mMenuContainer.offsetTopAndBottom(offsetPixels - (mMenuContainer.getTop() - getHeight()));
break;
case LEFT:
mMenuContainer.offsetLeftAndRight(offsetPixels - mMenuContainer.getRight());
break;
case RIGHT:
mMenuContainer.offsetLeftAndRight(offsetPixels - (mMenuContainer.getLeft() - getWidth()));
break;
}
}
invalidate();
}
@Override
protected void initPeekScroller() {
switch (getPosition()) {
case RIGHT:
case BOTTOM: {
final int dx = -mPeekSize;
mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
break;
}
default: {
final int dx = mPeekSize;
mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
break;
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
onOffsetPixelsChanged((int) mOffsetPixels);
}
@Override
protected GradientDrawable.Orientation getDropShadowOrientation() {
switch (getPosition()) {
case TOP:
return GradientDrawable.Orientation.TOP_BOTTOM;
case RIGHT:
return GradientDrawable.Orientation.RIGHT_LEFT;
case BOTTOM:
return GradientDrawable.Orientation.BOTTOM_TOP;
default:
return GradientDrawable.Orientation.LEFT_RIGHT;
}
}
@Override
protected void updateDropShadowRect() {
final float openRatio = Math.abs(mOffsetPixels) / mMenuSize;
final int dropShadowSize = (int) (mDropShadowSize * openRatio);
switch (getPosition()) {
case LEFT:
mDropShadowRect.top = 0;
mDropShadowRect.bottom = getHeight();
mDropShadowRect.left = ViewHelper.getRight(mMenuContainer);
mDropShadowRect.right = mDropShadowRect.left + dropShadowSize;
break;
case TOP:
mDropShadowRect.left = 0;
mDropShadowRect.right = getWidth();
mDropShadowRect.top = ViewHelper.getBottom(mMenuContainer);
mDropShadowRect.bottom = mDropShadowRect.top + dropShadowSize;
break;
case RIGHT:
mDropShadowRect.top = 0;
mDropShadowRect.bottom = getHeight();
mDropShadowRect.right = ViewHelper.getLeft(mMenuContainer);
mDropShadowRect.left = mDropShadowRect.right - dropShadowSize;
break;
case BOTTOM:
mDropShadowRect.left = 0;
mDropShadowRect.right = getWidth();
mDropShadowRect.bottom = ViewHelper.getTop(mMenuContainer);
mDropShadowRect.top = mDropShadowRect.bottom - dropShadowSize;
break;
}
}
@Override
protected void startLayerTranslation() {
if (USE_TRANSLATIONS && mHardwareLayersEnabled && !mLayerTypeHardware) {
mLayerTypeHardware = true;
mMenuContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
}
@Override
protected void stopLayerTranslation() {
if (mLayerTypeHardware) {
mLayerTypeHardware = false;
mMenuContainer.setLayerType(View.LAYER_TYPE_NONE, null);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = r - l;
final int height = b - t;
mContentContainer.layout(0, 0, width, height);
if (USE_TRANSLATIONS) {
switch (getPosition()) {
case LEFT:
mMenuContainer.layout(0, 0, mMenuSize, height);
break;
case RIGHT:
mMenuContainer.layout(width - mMenuSize, 0, width, height);
break;
case TOP:
mMenuContainer.layout(0, 0, width, mMenuSize);
break;
case BOTTOM:
mMenuContainer.layout(0, height - mMenuSize, width, height);
break;
}
} else {
final int offsetPixels = (int) mOffsetPixels;
final int menuSize = mMenuSize;
switch (getPosition()) {
case LEFT:
mMenuContainer.layout(-menuSize + offsetPixels, 0, offsetPixels, height);
break;
case RIGHT:
mMenuContainer.layout(width + offsetPixels, 0, width + menuSize + offsetPixels, height);
break;
case TOP:
mMenuContainer.layout(0, -menuSize + offsetPixels, width, offsetPixels);
break;
case BOTTOM:
mMenuContainer.layout(0, height + offsetPixels, width, height + menuSize + offsetPixels);
break;
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
throw new IllegalStateException("Must measure with an exact size");
}
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int height = MeasureSpec.getSize(heightMeasureSpec);
if (mOffsetPixels == -1) openMenu(false);
int menuWidthMeasureSpec;
int menuHeightMeasureSpec;
switch (getPosition()) {
case TOP:
case BOTTOM:
menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
menuHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, mMenuSize);
break;
default:
// LEFT/RIGHT
menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, mMenuSize);
menuHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
}
mMenuContainer.measure(menuWidthMeasureSpec, menuHeightMeasureSpec);
final int contentWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
final int contentHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
mContentContainer.measure(contentWidthMeasureSpec, contentHeightMeasureSpec);
setMeasuredDimension(width, height);
updateTouchAreaSize();
}
private boolean isContentTouch(int x, int y) {
boolean contentTouch = false;
switch (getPosition()) {
case LEFT:
contentTouch = ViewHelper.getRight(mMenuContainer) < x;
break;
case RIGHT:
contentTouch = ViewHelper.getLeft(mMenuContainer) > x;
break;
case TOP:
contentTouch = ViewHelper.getBottom(mMenuContainer) < y;
break;
case BOTTOM:
contentTouch = ViewHelper.getTop(mMenuContainer) > y;
break;
}
return contentTouch;
}
protected boolean onDownAllowDrag(int x, int y) {
switch (getPosition()) {
case LEFT:
return (!mMenuVisible && mInitialMotionX <= mTouchSize)
|| (mMenuVisible && mInitialMotionX <= mOffsetPixels);
case RIGHT:
final int width = getWidth();
final int initialMotionX = (int) mInitialMotionX;
return (!mMenuVisible && initialMotionX >= width - mTouchSize)
|| (mMenuVisible && initialMotionX >= width + mOffsetPixels);
case TOP:
return (!mMenuVisible && mInitialMotionY <= mTouchSize)
|| (mMenuVisible && mInitialMotionY <= mOffsetPixels);
case BOTTOM:
final int height = getHeight();
return (!mMenuVisible && mInitialMotionY >= height - mTouchSize)
|| (mMenuVisible && mInitialMotionY >= height + mOffsetPixels);
}
return false;
}
protected boolean onMoveAllowDrag(int x, int y, float dx, float dy) {
if (mMenuVisible && mTouchMode == TOUCH_MODE_FULLSCREEN) {
return true;
}
switch (getPosition()) {
case LEFT:
return (!mMenuVisible && mInitialMotionX <= mTouchSize && (dx > 0)) // Drawer closed
|| (mMenuVisible && x <= mOffsetPixels) // Drawer open
|| (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible); // Drawer revealed
case RIGHT:
final int width = getWidth();
return (!mMenuVisible && mInitialMotionX >= width - mTouchSize && (dx < 0))
|| (mMenuVisible && x >= width - mOffsetPixels)
|| (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible);
case TOP:
return (!mMenuVisible && mInitialMotionY <= mTouchSize && (dy > 0))
|| (mMenuVisible && x <= mOffsetPixels)
|| (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible);
case BOTTOM:
final int height = getHeight();
return (!mMenuVisible && mInitialMotionY >= height - mTouchSize && (dy < 0))
|| (mMenuVisible && x >= height - mOffsetPixels)
|| (Math.abs(mOffsetPixels) <= mPeekSize && mMenuVisible);
}
return false;
}
protected void onMoveEvent(float dx, float dy) {
switch (getPosition()) {
case LEFT:
setOffsetPixels(Math.min(Math.max(mOffsetPixels + dx, 0), mMenuSize));
break;
case RIGHT:
setOffsetPixels(Math.max(Math.min(mOffsetPixels + dx, 0), -mMenuSize));
break;
case TOP:
setOffsetPixels(Math.min(Math.max(mOffsetPixels + dy, 0), mMenuSize));
break;
case BOTTOM:
setOffsetPixels(Math.max(Math.min(mOffsetPixels + dy, 0), -mMenuSize));
break;
}
}
protected void onUpEvent(int x, int y) {
final int offsetPixels = (int) mOffsetPixels;
switch (getPosition()) {
case LEFT: {
if (mIsDragging) {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final int initialVelocity = (int) getXVelocity(mVelocityTracker);
mLastMotionX = x;
animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true);
// Close the menu when content is clicked while the menu is visible.
} else if (mMenuVisible) {
closeMenu();
}
break;
}
case TOP: {
if (mIsDragging) {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final int initialVelocity = (int) getYVelocity(mVelocityTracker);
mLastMotionY = y;
animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true);
// Close the menu when content is clicked while the menu is visible.
} else if (mMenuVisible) {
closeMenu();
}
break;
}
case RIGHT: {
final int width = getWidth();
if (mIsDragging) {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final int initialVelocity = (int) getXVelocity(mVelocityTracker);
mLastMotionX = x;
animateOffsetTo(initialVelocity > 0 ? 0 : -mMenuSize, initialVelocity, true);
// Close the menu when content is clicked while the menu is visible.
} else if (mMenuVisible) {
closeMenu();
}
break;
}
case BOTTOM: {
if (mIsDragging) {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final int initialVelocity = (int) getYVelocity(mVelocityTracker);
mLastMotionY = y;
animateOffsetTo(initialVelocity < 0 ? -mMenuSize : 0, initialVelocity, true);
// Close the menu when content is clicked while the menu is visible.
} else if (mMenuVisible) {
closeMenu();
}
break;
}
}
}
protected boolean checkTouchSlop(float dx, float dy) {
switch (getPosition()) {
case TOP:
case BOTTOM:
return Math.abs(dy) > mTouchSlop && Math.abs(dy) > Math.abs(dx);
default:
return Math.abs(dx) > mTouchSlop && Math.abs(dx) > Math.abs(dy);
}
}
@Override
protected void stopAnimation() {
super.stopAnimation();
removeCallbacks(mRevealRunnable);
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
removeCallbacks(mRevealRunnable);
mActivePointerId = INVALID_POINTER;
mIsDragging = false;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
if (Math.abs(mOffsetPixels) > mMenuSize / 2) {
openMenu();
} else {
closeMenu();
}
return false;
}
if (action == MotionEvent.ACTION_DOWN && mMenuVisible && isCloseEnough()) {
setOffsetPixels(0);
stopAnimation();
endPeek();
setDrawerState(STATE_CLOSED);
mIsDragging = false;
}
// Always intercept events over the content while menu is visible.
if (mMenuVisible) {
int index = 0;
if (mActivePointerId != INVALID_POINTER) {
index = ev.findPointerIndex(mActivePointerId);
index = index == -1 ? 0 : index;
}
final int x = (int) ev.getX(index);
final int y = (int) ev.getY(index);
if (isContentTouch(x, y)) {
return true;
}
}
if (!mMenuVisible && !mIsDragging && mTouchMode == TOUCH_MODE_NONE) {
return false;
}
if (action != MotionEvent.ACTION_DOWN && mIsDragging) {
return true;
}
switch (action) {
case MotionEvent.ACTION_DOWN: {
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
final boolean allowDrag = onDownAllowDrag((int) mLastMotionX, (int) mLastMotionY);
mActivePointerId = ev.getPointerId(0);
if (allowDrag) {
setDrawerState(mMenuVisible ? STATE_OPEN : STATE_CLOSED);
stopAnimation();
endPeek();
if (!mMenuVisible && mInitialMotionX <= mPeekSize) {
postDelayed(mRevealRunnable, 160);
}
mIsDragging = false;
}
break;
}
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
mIsDragging = false;
mActivePointerId = INVALID_POINTER;
endDrag();
closeMenu(true);
return false;
}
final float x = ev.getX(pointerIndex);
final float dx = x - mLastMotionX;
final float y = ev.getY(pointerIndex);
final float dy = y - mLastMotionY;
if (Math.abs(dx) >= mTouchSlop || Math.abs(dy) >= mTouchSlop) {
removeCallbacks(mRevealRunnable);
endPeek();
}
if (checkTouchSlop(dx, dy)) {
if (mOnInterceptMoveEventListener != null && (mTouchMode == TOUCH_MODE_FULLSCREEN || mMenuVisible)
&& canChildrenScroll((int) dx, (int) dy, (int) x, (int) y)) {
endDrag(); // Release the velocity tracker
requestDisallowInterceptTouchEvent(true);
return false;
}
final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy);
if (allowDrag) {
endPeek();
stopAnimation();
setDrawerState(STATE_DRAGGING);
mIsDragging = true;
mLastMotionX = x;
mLastMotionY = y;
}
}
break;
}
case MotionEvent.ACTION_POINTER_UP:
onPointerUp(ev);
mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
return mIsDragging;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mMenuVisible && !mIsDragging && mTouchMode == TOUCH_MODE_NONE) {
return false;
}
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
final boolean allowDrag = onDownAllowDrag((int) mLastMotionX, (int) mLastMotionY);
mActivePointerId = ev.getPointerId(0);
if (allowDrag) {
stopAnimation();
endPeek();
if (!mMenuVisible && mLastMotionX <= mPeekSize) {
postDelayed(mRevealRunnable, 160);
}
startLayerTranslation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
mIsDragging = false;
mActivePointerId = INVALID_POINTER;
endDrag();
closeMenu(true);
return false;
}
if (!mIsDragging) {
final float x = ev.getX(pointerIndex);
final float dx = x - mLastMotionX;
final float y = ev.getY(pointerIndex);
final float dy = y - mLastMotionY;
if (checkTouchSlop(dx, dy)) {
final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy);
if (allowDrag) {
endPeek();
stopAnimation();
setDrawerState(STATE_DRAGGING);
mIsDragging = true;
mLastMotionX = x;
mLastMotionY = y;
} else {
mInitialMotionX = x;
mInitialMotionY = y;
}
}
}
if (mIsDragging) {
startLayerTranslation();
final float x = ev.getX(pointerIndex);
final float dx = x - mLastMotionX;
final float y = ev.getY(pointerIndex);
final float dy = y - mLastMotionY;
mLastMotionX = x;
mLastMotionY = y;
onMoveEvent(dx, dy);
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
removeCallbacks(mRevealRunnable);
int index = ev.findPointerIndex(mActivePointerId);
index = index == -1 ? 0 : index;
final int x = (int) ev.getX(index);
final int y = (int) ev.getY(index);
onUpEvent(x, y);
mActivePointerId = INVALID_POINTER;
mIsDragging = false;
break;
}
case MotionEvent.ACTION_POINTER_DOWN:
final int index = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
mLastMotionX = ev.getX(index);
mLastMotionY = ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
case MotionEvent.ACTION_POINTER_UP:
onPointerUp(ev);
mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
return true;
}
private void onPointerUp(MotionEvent ev) {
final int pointerIndex = ev.getActionIndex();
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionX = ev.getX(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
}

View File

@ -1,25 +0,0 @@
package net.simonvt.menudrawer;
import android.view.animation.Interpolator;
class PeekInterpolator implements Interpolator {
private static final SinusoidalInterpolator SINUSOIDAL_INTERPOLATOR = new SinusoidalInterpolator();
@Override
public float getInterpolation(float input) {
float result;
if (input < 1.f / 3.f) {
result = SINUSOIDAL_INTERPOLATOR.getInterpolation(input * 3);
} else if (input > 2.f / 3.f) {
final float val = ((input + 1.f / 3.f) - 1.f) * 3.f;
result = 1.f - SINUSOIDAL_INTERPOLATOR.getInterpolation(val);
} else {
result = 1.f;
}
return result;
}
}

View File

@ -1,50 +0,0 @@
package net.simonvt.menudrawer;
import android.util.SparseArray;
/**
* Enums used for positioning the drawer.
*/
public enum Position {
// Positions the drawer to the left of the content.
LEFT(0),
// Positions the drawer above the content.
TOP(1),
// Positions the drawer to the right of the content.
RIGHT(2),
// Positions the drawer below the content.
BOTTOM(3),
/**
* Position the drawer at the start edge. This will position the drawer to the {@link #LEFT} with LTR languages and
* {@link #RIGHT} with RTL languages.
*/
START(4),
/**
* Position the drawer at the end edge. This will position the drawer to the {@link #RIGHT} with LTR languages and
* {@link #LEFT} with RTL languages.
*/
END(5);
final int mValue;
Position(int value) {
mValue = value;
}
private static final SparseArray<Position> STRING_MAPPING = new SparseArray<Position>();
static {
for (Position via : Position.values()) {
STRING_MAPPING.put(via.mValue, via);
}
}
public static Position fromValue(int value) {
return STRING_MAPPING.get(value);
}
}

View File

@ -1,504 +0,0 @@
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.simonvt.menudrawer;
import android.content.Context;
import android.hardware.SensorManager;
import android.os.Build;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
/**
* This class encapsulates scrolling. The duration of the scroll
* can be passed in the constructor and specifies the maximum time that
* the scrolling animation should take. Past this time, the scrolling is
* automatically moved to its final stage and computeScrollOffset()
* will always return false to indicate that scrolling is over.
*/
class Scroller {
private int mMode;
private int mStartX;
private int mStartY;
private int mFinalX;
private int mFinalY;
private int mMinX;
private int mMaxX;
private int mMinY;
private int mMaxY;
private int mCurrX;
private int mCurrY;
private long mStartTime;
private int mDuration;
private float mDurationReciprocal;
private float mDeltaX;
private float mDeltaY;
private boolean mFinished;
private Interpolator mInterpolator;
private boolean mFlywheel;
private float mVelocity;
private static final int DEFAULT_DURATION = 250;
private static final int SCROLL_MODE = 0;
private static final int FLING_MODE = 1;
private static final float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9));
private static final float ALPHA = 800; // pixels / seconds
private static final float START_TENSION = 0.4f; // Tension at start: (0.4 * total T, 1.0 * Distance)
private static final float END_TENSION = 1.0f - START_TENSION;
private static final int NB_SAMPLES = 100;
private static final float[] SPLINE = new float[NB_SAMPLES + 1];
private float mDeceleration;
private final float mPpi;
static {
float xMin = 0.0f;
for (int i = 0; i <= NB_SAMPLES; i++) {
final float t = (float) i / NB_SAMPLES;
float xMax = 1.0f;
float x, tx, coef;
while (true) {
x = xMin + (xMax - xMin) / 2.0f;
coef = 3.0f * x * (1.0f - x);
tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x;
if (Math.abs(tx - t) < 1E-5) break;
if (tx > t) xMax = x;
else xMin = x;
}
final float d = coef + x * x * x;
SPLINE[i] = d;
}
SPLINE[NB_SAMPLES] = 1.0f;
// This controls the viscous fluid effect (how much of it)
sViscousFluidScale = 8.0f;
// must be set to 1.0 (used in viscousFluid())
sViscousFluidNormalize = 1.0f;
sViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
}
private static float sViscousFluidScale;
private static float sViscousFluidNormalize;
/**
* Create a Scroller with the default duration and interpolator.
*/
public Scroller(Context context) {
this(context, null);
}
/**
* Create a Scroller with the specified interpolator. If the interpolator is
* null, the default (viscous) interpolator will be used. "Flywheel" behavior will
* be in effect for apps targeting Honeycomb or newer.
*/
public Scroller(Context context, Interpolator interpolator) {
this(context, interpolator,
context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}
/**
* Create a Scroller with the specified interpolator. If the interpolator is
* null, the default (viscous) interpolator will be used. Specify whether or
* not to support progressive "flywheel" behavior in flinging.
*/
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
mFinished = true;
mInterpolator = interpolator;
mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
mFlywheel = flywheel;
}
/**
* The amount of friction applied to flings. The default value
* is {@link android.view.ViewConfiguration#getScrollFriction}.
*
* @param friction A scalar dimension-less value representing the coefficient of
* friction.
*/
public final void setFriction(float friction) {
mDeceleration = computeDeceleration(friction);
}
private float computeDeceleration(float friction) {
return SensorManager.GRAVITY_EARTH // g (m/s^2)
* 39.37f // inch/meter
* mPpi // pixels per inch
* friction;
}
/**
*
* Returns whether the scroller has finished scrolling.
*
* @return True if the scroller has finished scrolling, false otherwise.
*/
public final boolean isFinished() {
return mFinished;
}
/**
* Force the finished field to a particular value.
*
* @param finished The new finished value.
*/
public final void forceFinished(boolean finished) {
mFinished = finished;
}
/**
* Returns how long the scroll event will take, in milliseconds.
*
* @return The duration of the scroll in milliseconds.
*/
public final int getDuration() {
return mDuration;
}
/**
* Returns the current X offset in the scroll.
*
* @return The new X offset as an absolute distance from the origin.
*/
public final int getCurrX() {
return mCurrX;
}
/**
* Returns the current Y offset in the scroll.
*
* @return The new Y offset as an absolute distance from the origin.
*/
public final int getCurrY() {
return mCurrY;
}
/**
* Returns the current velocity.
*
* @return The original velocity less the deceleration. Result may be
* negative.
*/
public float getCurrVelocity() {
return mVelocity - mDeceleration * timePassed() / 2000.0f;
}
/**
* Returns the start X offset in the scroll.
*
* @return The start X offset as an absolute distance from the origin.
*/
public final int getStartX() {
return mStartX;
}
/**
* Returns the start Y offset in the scroll.
*
* @return The start Y offset as an absolute distance from the origin.
*/
public final int getStartY() {
return mStartY;
}
/**
* Returns where the scroll will end. Valid only for "fling" scrolls.
*
* @return The final X offset as an absolute distance from the origin.
*/
public final int getFinalX() {
return mFinalX;
}
/**
* Returns where the scroll will end. Valid only for "fling" scrolls.
*
* @return The final Y offset as an absolute distance from the origin.
*/
public final int getFinalY() {
return mFinalY;
}
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished. loc will be altered to provide the
* new location.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
float x = timePassed * mDurationReciprocal;
if (mInterpolator == null)
x = viscousFluid(x);
else
x = mInterpolator.getInterpolation(x);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
final float tInf = (float) index / NB_SAMPLES;
final float tSup = (float) (index + 1) / NB_SAMPLES;
final float dInf = SPLINE[index];
final float dSup = SPLINE[index + 1];
final float distanceCoef = dInf + (t - tInf) / (tSup - tInf) * (dSup - dInf);
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
} else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
/**
* Start scrolling by providing a starting point and the distance to travel.
* The scroll will use the default value of 250 milliseconds for the
* duration.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
*/
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
/**
* Start scrolling by providing a starting point and the distance to travel.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
/**
* Start scrolling based on a fling gesture. The distance travelled will
* depend on the initial velocity of the fling.
*
* @param startX Starting point of the scroll (X)
* @param startY Starting point of the scroll (Y)
* @param velocityX Initial velocity of the fling (X) measured in pixels per
* second.
* @param velocityY Initial velocity of the fling (Y) measured in pixels per
* second
* @param minX Minimum X value. The scroller will not scroll past this
* point.
* @param maxX Maximum X value. The scroller will not scroll past this
* point.
* @param minY Minimum Y value. The scroller will not scroll past this
* point.
* @param maxY Maximum Y value. The scroller will not scroll past this
* point.
*/
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
// Continue a scroll or fling in progress
if (mFlywheel && !mFinished) {
float oldVel = getCurrVelocity();
float dx = (float) (mFinalX - mStartX);
float dy = (float) (mFinalY - mStartY);
float hyp = (float) Math.sqrt(dx * dx + dy * dy);
float ndx = dx / hyp;
float ndy = dy / hyp;
float oldVelocityX = ndx * oldVel;
float oldVelocityY = ndy * oldVel;
if (Math.signum(velocityX) == Math.signum(oldVelocityX)
&& Math.signum(velocityY) == Math.signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
}
mMode = FLING_MODE;
mFinished = false;
float velocity = (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY);
mVelocity = velocity;
final double l = Math.log(START_TENSION * velocity / ALPHA);
mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0)));
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
int totalDistance =
(int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l));
mMinX = minX;
mMaxX = maxX;
mMinY = minY;
mMaxY = maxY;
mFinalX = startX + Math.round(totalDistance * coeffX);
// Pin to mMinX <= mFinalX <= mMaxX
mFinalX = Math.min(mFinalX, mMaxX);
mFinalX = Math.max(mFinalX, mMinX);
mFinalY = startY + Math.round(totalDistance * coeffY);
// Pin to mMinY <= mFinalY <= mMaxY
mFinalY = Math.min(mFinalY, mMaxY);
mFinalY = Math.max(mFinalY, mMinY);
}
static float viscousFluid(float x) {
x *= sViscousFluidScale;
if (x < 1.0f) {
x -= (1.0f - (float) Math.exp(-x));
} else {
float start = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - (float) Math.exp(1.0f - x);
x = start + x * (1.0f - start);
}
x *= sViscousFluidNormalize;
return x;
}
/**
* Stops the animation. Contrary to {@link #forceFinished(boolean)},
* aborting the animating cause the scroller to move to the final x and y
* position
*
* @see #forceFinished(boolean)
*/
public void abortAnimation() {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
/**
* Extend the scroll animation. This allows a running animation to scroll
* further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
*
* @param extend Additional time to scroll in milliseconds.
* @see #setFinalX(int)
* @see #setFinalY(int)
*/
public void extendDuration(int extend) {
int passed = timePassed();
mDuration = passed + extend;
mDurationReciprocal = 1.0f / mDuration;
mFinished = false;
}
/**
* Returns the time elapsed since the beginning of the scrolling.
*
* @return The elapsed time in milliseconds.
*/
public int timePassed() {
return (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);
}
/**
* Sets the final position (X) for this scroller.
*
* @param newX The new X offset as an absolute distance from the origin.
* @see #extendDuration(int)
* @see #setFinalY(int)
*/
public void setFinalX(int newX) {
mFinalX = newX;
mDeltaX = mFinalX - mStartX;
mFinished = false;
}
/**
* Sets the final position (Y) for this scroller.
*
* @param newY The new Y offset as an absolute distance from the origin.
* @see #extendDuration(int)
* @see #setFinalX(int)
*/
public void setFinalY(int newY) {
mFinalY = newY;
mDeltaY = mFinalY - mStartY;
mFinished = false;
}
/**
* @hide
*/
public boolean isScrollingInDirection(float xvel, float yvel) {
return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX)
&& Math.signum(yvel) == Math.signum(mFinalY - mStartY);
}
}

View File

@ -1,15 +0,0 @@
package net.simonvt.menudrawer;
import android.view.animation.Interpolator;
/**
* Interpolator which, when drawn from 0 to 1, looks like half a sine-wave. Used for smoother opening/closing when
* peeking at the drawer.
*/
class SinusoidalInterpolator implements Interpolator {
@Override
public float getInterpolation(float input) {
return (float) (0.5f + 0.5f * Math.sin(input * Math.PI - Math.PI / 2.f));
}
}

View File

@ -1,187 +0,0 @@
package net.simonvt.menudrawer;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
public class SlideDrawable extends Drawable implements Drawable.Callback {
private Drawable mWrapped;
private float mOffset;
private final Rect mTmpRect = new Rect();
private boolean mIsRtl;
public SlideDrawable(Drawable wrapped) {
mWrapped = wrapped;
}
public void setOffset(float offset) {
mOffset = offset;
invalidateSelf();
}
public float getOffset() {
return mOffset;
}
void setIsRtl(boolean isRtl) {
mIsRtl = isRtl;
invalidateSelf();
}
@Override
public void draw(Canvas canvas) {
mWrapped.copyBounds(mTmpRect);
canvas.save();
if (mIsRtl) {
canvas.translate(1.f / 3 * mTmpRect.width() * mOffset, 0);
} else {
canvas.translate(1.f / 3 * mTmpRect.width() * -mOffset, 0);
}
mWrapped.draw(canvas);
canvas.restore();
}
@Override
public void setChangingConfigurations(int configs) {
mWrapped.setChangingConfigurations(configs);
}
@Override
public int getChangingConfigurations() {
return mWrapped.getChangingConfigurations();
}
@Override
public void setDither(boolean dither) {
mWrapped.setDither(dither);
}
@Override
public void setFilterBitmap(boolean filter) {
mWrapped.setFilterBitmap(filter);
}
@Override
public void setAlpha(int alpha) {
mWrapped.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
mWrapped.setColorFilter(cf);
}
@Override
public void setColorFilter(int color, PorterDuff.Mode mode) {
mWrapped.setColorFilter(color, mode);
}
@Override
public void clearColorFilter() {
mWrapped.clearColorFilter();
}
@Override
public boolean isStateful() {
return mWrapped.isStateful();
}
@Override
public boolean setState(int[] stateSet) {
return mWrapped.setState(stateSet);
}
@Override
public int[] getState() {
return mWrapped.getState();
}
@Override
public Drawable getCurrent() {
return mWrapped.getCurrent();
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
return super.setVisible(visible, restart);
}
@Override
public int getOpacity() {
return mWrapped.getOpacity();
}
@Override
public Region getTransparentRegion() {
return mWrapped.getTransparentRegion();
}
@Override
protected boolean onStateChange(int[] state) {
mWrapped.setState(state);
return super.onStateChange(state);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mWrapped.setBounds(bounds);
}
@Override
public int getIntrinsicWidth() {
return mWrapped.getIntrinsicWidth();
}
@Override
public int getIntrinsicHeight() {
return mWrapped.getIntrinsicHeight();
}
@Override
public int getMinimumWidth() {
return mWrapped.getMinimumWidth();
}
@Override
public int getMinimumHeight() {
return mWrapped.getMinimumHeight();
}
@Override
public boolean getPadding(Rect padding) {
return mWrapped.getPadding(padding);
}
@Override
public ConstantState getConstantState() {
return super.getConstantState();
}
@Override
public void invalidateDrawable(Drawable who) {
if (who == mWrapped) {
invalidateSelf();
}
}
@Override
public void scheduleDrawable(Drawable who, Runnable what, long when) {
if (who == mWrapped) {
scheduleSelf(what, when);
}
}
@Override
public void unscheduleDrawable(Drawable who, Runnable what) {
if (who == mWrapped) {
unscheduleSelf(what);
}
}
}

View File

@ -1,704 +0,0 @@
package net.simonvt.menudrawer;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
public class SlidingDrawer extends DraggableDrawer {
SlidingDrawer(Activity activity, int dragMode) {
super(activity, dragMode);
}
public SlidingDrawer(Context context) {
super(context);
}
public SlidingDrawer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlidingDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
super.initDrawer(context, attrs, defStyle);
super.addView(mMenuContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
super.addView(mContentContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
}
@Override
public void openMenu(boolean animate) {
int animateTo = 0;
switch (getPosition()) {
case LEFT:
case TOP:
animateTo = mMenuSize;
break;
case RIGHT:
case BOTTOM:
animateTo = -mMenuSize;
break;
}
animateOffsetTo(animateTo, 0, animate);
}
@Override
public void closeMenu(boolean animate) {
animateOffsetTo(0, 0, animate);
}
@Override
protected void onOffsetPixelsChanged(int offsetPixels) {
if (USE_TRANSLATIONS) {
switch (getPosition()) {
case TOP:
case BOTTOM:
mContentContainer.setTranslationY(offsetPixels);
break;
default:
mContentContainer.setTranslationX(offsetPixels);
break;
}
} else {
switch (getPosition()) {
case TOP:
case BOTTOM:
mContentContainer.offsetTopAndBottom(offsetPixels - mContentContainer.getTop());
break;
default:
mContentContainer.offsetLeftAndRight(offsetPixels - mContentContainer.getLeft());
break;
}
}
offsetMenu(offsetPixels);
invalidate();
}
@Override
protected void initPeekScroller() {
switch (getPosition()) {
case RIGHT:
case BOTTOM: {
final int dx = -mMenuSize / 3;
mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
break;
}
default: {
final int dx = mMenuSize / 3;
mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
break;
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
onOffsetPixelsChanged((int) mOffsetPixels);
}
@Override
protected void drawOverlay(Canvas canvas) {
final int width = getWidth();
final int height = getHeight();
final int offsetPixels = (int) mOffsetPixels;
final float openRatio = Math.abs(mOffsetPixels) / mMenuSize;
switch (getPosition()) {
case LEFT:
mMenuOverlay.setBounds(0, 0, offsetPixels, height);
break;
case RIGHT:
mMenuOverlay.setBounds(width + offsetPixels, 0, width, height);
break;
case TOP:
mMenuOverlay.setBounds(0, 0, width, offsetPixels);
break;
case BOTTOM:
mMenuOverlay.setBounds(0, height + offsetPixels, width, height);
break;
}
mMenuOverlay.setAlpha((int) (MAX_MENU_OVERLAY_ALPHA * (1.f - openRatio)));
mMenuOverlay.draw(canvas);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = r - l;
final int height = b - t;
if (USE_TRANSLATIONS) {
mContentContainer.layout(0, 0, width, height);
} else {
final int offsetPixels = (int) mOffsetPixels;
if (getPosition() == Position.LEFT || getPosition() == Position.RIGHT) {
mContentContainer.layout(offsetPixels, 0, width + offsetPixels, height);
} else {
mContentContainer.layout(0, offsetPixels, width, height + offsetPixels);
}
}
switch (getPosition()) {
case LEFT:
mMenuContainer.layout(0, 0, mMenuSize, height);
break;
case RIGHT:
mMenuContainer.layout(width - mMenuSize, 0, width, height);
break;
case TOP:
mMenuContainer.layout(0, 0, width, mMenuSize);
break;
case BOTTOM:
mMenuContainer.layout(0, height - mMenuSize, width, height);
break;
}
}
/**
* Offsets the menu relative to its original position based on the position of the content.
*
* @param offsetPixels The number of pixels the content if offset.
*/
private void offsetMenu(int offsetPixels) {
if (!mOffsetMenu || mMenuSize == 0) {
return;
}
final int width = getWidth();
final int height = getHeight();
final int menuSize = mMenuSize;
final int sign = (int) (mOffsetPixels / Math.abs(mOffsetPixels));
final float openRatio = Math.abs(mOffsetPixels) / menuSize;
final int offset = (int) (-0.25f * ((1.0f - openRatio) * menuSize) * sign);
switch (getPosition()) {
case LEFT: {
if (USE_TRANSLATIONS) {
if (offsetPixels > 0) {
mMenuContainer.setTranslationX(offset);
} else {
mMenuContainer.setTranslationX(-menuSize);
}
} else {
mMenuContainer.offsetLeftAndRight(offset - mMenuContainer.getLeft());
mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
}
break;
}
case RIGHT: {
if (USE_TRANSLATIONS) {
if (offsetPixels != 0) {
mMenuContainer.setTranslationX(offset);
} else {
mMenuContainer.setTranslationX(menuSize);
}
} else {
final int oldOffset = mMenuContainer.getRight() - width;
final int offsetBy = offset - oldOffset;
mMenuContainer.offsetLeftAndRight(offsetBy);
mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
}
break;
}
case TOP: {
if (USE_TRANSLATIONS) {
if (offsetPixels > 0) {
mMenuContainer.setTranslationY(offset);
} else {
mMenuContainer.setTranslationY(-menuSize);
}
} else {
mMenuContainer.offsetTopAndBottom(offset - mMenuContainer.getTop());
mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
}
break;
}
case BOTTOM: {
if (USE_TRANSLATIONS) {
if (offsetPixels != 0) {
mMenuContainer.setTranslationY(offset);
} else {
mMenuContainer.setTranslationY(menuSize);
}
} else {
final int oldOffset = mMenuContainer.getBottom() - height;
final int offsetBy = offset - oldOffset;
mMenuContainer.offsetTopAndBottom(offsetBy);
mMenuContainer.setVisibility(offsetPixels == 0 ? INVISIBLE : VISIBLE);
}
break;
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
throw new IllegalStateException("Must measure with an exact size");
}
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int height = MeasureSpec.getSize(heightMeasureSpec);
if (mOffsetPixels == -1) openMenu(false);
int menuWidthMeasureSpec;
int menuHeightMeasureSpec;
switch (getPosition()) {
case TOP:
case BOTTOM:
menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
menuHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, mMenuSize);
break;
default:
// LEFT/RIGHT
menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, mMenuSize);
menuHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
}
mMenuContainer.measure(menuWidthMeasureSpec, menuHeightMeasureSpec);
final int contentWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
final int contentHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
mContentContainer.measure(contentWidthMeasureSpec, contentHeightMeasureSpec);
setMeasuredDimension(width, height);
updateTouchAreaSize();
}
private boolean isContentTouch(int x, int y) {
boolean contentTouch = false;
switch (getPosition()) {
case LEFT:
contentTouch = ViewHelper.getLeft(mContentContainer) < x;
break;
case RIGHT:
contentTouch = ViewHelper.getRight(mContentContainer) > x;
break;
case TOP:
contentTouch = ViewHelper.getTop(mContentContainer) < y;
break;
case BOTTOM:
contentTouch = ViewHelper.getBottom(mContentContainer) > y;
break;
}
return contentTouch;
}
protected boolean onDownAllowDrag(int x, int y) {
switch (getPosition()) {
case LEFT:
return (!mMenuVisible && mInitialMotionX <= mTouchSize)
|| (mMenuVisible && mInitialMotionX >= mOffsetPixels);
case RIGHT:
final int width = getWidth();
final int initialMotionX = (int) mInitialMotionX;
return (!mMenuVisible && initialMotionX >= width - mTouchSize)
|| (mMenuVisible && initialMotionX <= width + mOffsetPixels);
case TOP:
return (!mMenuVisible && mInitialMotionY <= mTouchSize)
|| (mMenuVisible && mInitialMotionY >= mOffsetPixels);
case BOTTOM:
final int height = getHeight();
return (!mMenuVisible && mInitialMotionY >= height - mTouchSize)
|| (mMenuVisible && mInitialMotionY <= height + mOffsetPixels);
}
return false;
}
protected boolean onMoveAllowDrag(int x, int y, float dx, float dy) {
switch (getPosition()) {
case LEFT:
return (!mMenuVisible && mInitialMotionX <= mTouchSize && (dx > 0))
|| (mMenuVisible && x >= mOffsetPixels);
case RIGHT:
final int width = getWidth();
return (!mMenuVisible && mInitialMotionX >= width - mTouchSize && (dx < 0))
|| (mMenuVisible && x <= width + mOffsetPixels);
case TOP:
return (!mMenuVisible && mInitialMotionY <= mTouchSize && (dy > 0))
|| (mMenuVisible && y >= mOffsetPixels);
case BOTTOM:
final int height = getHeight();
return (!mMenuVisible && mInitialMotionY >= height - mTouchSize && (dy < 0))
|| (mMenuVisible && y <= height + mOffsetPixels);
}
return false;
}
protected void onMoveEvent(float dx, float dy) {
switch (getPosition()) {
case LEFT:
setOffsetPixels(Math.min(Math.max(mOffsetPixels + dx, 0), mMenuSize));
break;
case RIGHT:
setOffsetPixels(Math.max(Math.min(mOffsetPixels + dx, 0), -mMenuSize));
break;
case TOP:
setOffsetPixels(Math.min(Math.max(mOffsetPixels + dy, 0), mMenuSize));
break;
case BOTTOM:
setOffsetPixels(Math.max(Math.min(mOffsetPixels + dy, 0), -mMenuSize));
break;
}
}
protected void onUpEvent(int x, int y) {
final int offsetPixels = (int) mOffsetPixels;
switch (getPosition()) {
case LEFT: {
if (mIsDragging) {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final int initialVelocity = (int) getXVelocity(mVelocityTracker);
mLastMotionX = x;
animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true);
// Close the menu when content is clicked while the menu is visible.
} else if (mMenuVisible && x > offsetPixels) {
closeMenu();
}
break;
}
case TOP: {
if (mIsDragging) {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final int initialVelocity = (int) getYVelocity(mVelocityTracker);
mLastMotionY = y;
animateOffsetTo(initialVelocity > 0 ? mMenuSize : 0, initialVelocity, true);
// Close the menu when content is clicked while the menu is visible.
} else if (mMenuVisible && y > offsetPixels) {
closeMenu();
}
break;
}
case RIGHT: {
final int width = getWidth();
if (mIsDragging) {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final int initialVelocity = (int) getXVelocity(mVelocityTracker);
mLastMotionX = x;
animateOffsetTo(initialVelocity > 0 ? 0 : -mMenuSize, initialVelocity, true);
// Close the menu when content is clicked while the menu is visible.
} else if (mMenuVisible && x < width + offsetPixels) {
closeMenu();
}
break;
}
case BOTTOM: {
if (mIsDragging) {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final int initialVelocity = (int) getYVelocity(mVelocityTracker);
mLastMotionY = y;
animateOffsetTo(initialVelocity < 0 ? -mMenuSize : 0, initialVelocity, true);
// Close the menu when content is clicked while the menu is visible.
} else if (mMenuVisible && y < getHeight() + offsetPixels) {
closeMenu();
}
break;
}
}
}
protected boolean checkTouchSlop(float dx, float dy) {
switch (getPosition()) {
case TOP:
case BOTTOM:
return Math.abs(dy) > mTouchSlop && Math.abs(dy) > Math.abs(dx);
default:
return Math.abs(dx) > mTouchSlop && Math.abs(dx) > Math.abs(dy);
}
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mActivePointerId = INVALID_POINTER;
mIsDragging = false;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
if (Math.abs(mOffsetPixels) > mMenuSize / 2) {
openMenu();
} else {
closeMenu();
}
return false;
}
if (action == MotionEvent.ACTION_DOWN && mMenuVisible && isCloseEnough()) {
setOffsetPixels(0);
stopAnimation();
endPeek();
setDrawerState(STATE_CLOSED);
mIsDragging = false;
}
// Always intercept events over the content while menu is visible.
if (mMenuVisible) {
int index = 0;
if (mActivePointerId != INVALID_POINTER) {
index = ev.findPointerIndex(mActivePointerId);
index = index == -1 ? 0 : index;
}
final int x = (int) ev.getX(index);
final int y = (int) ev.getY(index);
if (isContentTouch(x, y)) {
return true;
}
}
if (!mMenuVisible && !mIsDragging && mTouchMode == TOUCH_MODE_NONE) {
return false;
}
if (action != MotionEvent.ACTION_DOWN && mIsDragging) {
return true;
}
switch (action) {
case MotionEvent.ACTION_DOWN: {
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
final boolean allowDrag = onDownAllowDrag((int) mLastMotionX, (int) mLastMotionY);
mActivePointerId = ev.getPointerId(0);
if (allowDrag) {
setDrawerState(mMenuVisible ? STATE_OPEN : STATE_CLOSED);
stopAnimation();
endPeek();
mIsDragging = false;
}
break;
}
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
mIsDragging = false;
mActivePointerId = INVALID_POINTER;
endDrag();
closeMenu(true);
return false;
}
final float x = ev.getX(pointerIndex);
final float dx = x - mLastMotionX;
final float y = ev.getY(pointerIndex);
final float dy = y - mLastMotionY;
if (checkTouchSlop(dx, dy)) {
if (mOnInterceptMoveEventListener != null && (mTouchMode == TOUCH_MODE_FULLSCREEN || mMenuVisible)
&& canChildrenScroll((int) dx, (int) dy, (int) x, (int) y)) {
endDrag(); // Release the velocity tracker
requestDisallowInterceptTouchEvent(true);
return false;
}
final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy);
if (allowDrag) {
setDrawerState(STATE_DRAGGING);
mIsDragging = true;
mLastMotionX = x;
mLastMotionY = y;
}
}
break;
}
case MotionEvent.ACTION_POINTER_UP:
onPointerUp(ev);
mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
return mIsDragging;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mMenuVisible && !mIsDragging && mTouchMode == TOUCH_MODE_NONE) {
return false;
}
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
final boolean allowDrag = onDownAllowDrag((int) mLastMotionX, (int) mLastMotionY);
mActivePointerId = ev.getPointerId(0);
if (allowDrag) {
stopAnimation();
endPeek();
startLayerTranslation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
mIsDragging = false;
mActivePointerId = INVALID_POINTER;
endDrag();
closeMenu(true);
return false;
}
if (!mIsDragging) {
final float x = ev.getX(pointerIndex);
final float dx = x - mLastMotionX;
final float y = ev.getY(pointerIndex);
final float dy = y - mLastMotionY;
if (checkTouchSlop(dx, dy)) {
final boolean allowDrag = onMoveAllowDrag((int) x, (int) y, dx, dy);
if (allowDrag) {
setDrawerState(STATE_DRAGGING);
mIsDragging = true;
mLastMotionX = x;
mLastMotionY = y;
} else {
mInitialMotionX = x;
mInitialMotionY = y;
}
}
}
if (mIsDragging) {
startLayerTranslation();
final float x = ev.getX(pointerIndex);
final float dx = x - mLastMotionX;
final float y = ev.getY(pointerIndex);
final float dy = y - mLastMotionY;
mLastMotionX = x;
mLastMotionY = y;
onMoveEvent(dx, dy);
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
int index = ev.findPointerIndex(mActivePointerId);
index = index == -1 ? 0 : index;
final int x = (int) ev.getX(index);
final int y = (int) ev.getY(index);
onUpEvent(x, y);
mActivePointerId = INVALID_POINTER;
mIsDragging = false;
break;
}
case MotionEvent.ACTION_POINTER_DOWN:
final int index = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
>> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
mLastMotionX = ev.getX(index);
mLastMotionY = ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
case MotionEvent.ACTION_POINTER_UP:
onPointerUp(ev);
mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
return true;
}
private void onPointerUp(MotionEvent ev) {
final int pointerIndex = ev.getActionIndex();
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionX = ev.getX(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
}

View File

@ -1,12 +0,0 @@
package net.simonvt.menudrawer;
import android.view.animation.Interpolator;
class SmoothInterpolator implements Interpolator {
@Override
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
}

View File

@ -1,218 +0,0 @@
package net.simonvt.menudrawer;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
public class StaticDrawer extends MenuDrawer {
public StaticDrawer(Context context) {
super(context);
}
public StaticDrawer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public StaticDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void initDrawer(Context context, AttributeSet attrs, int defStyle) {
super.initDrawer(context, attrs, defStyle);
super.addView(mMenuContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
super.addView(mContentContainer, -1, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
mIsStatic = true;
}
@Override
protected void drawOverlay(Canvas canvas) {
// NO-OP
}
@Override
protected void onOffsetPixelsChanged(int offsetPixels) {
// NO-OP
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = r - l;
final int height = b - t;
switch (getPosition()) {
case LEFT:
mMenuContainer.layout(0, 0, mMenuSize, height);
mContentContainer.layout(mMenuSize, 0, width, height);
break;
case RIGHT:
mMenuContainer.layout(width - mMenuSize, 0, width, height);
mContentContainer.layout(0, 0, width - mMenuSize, height);
break;
case TOP:
mMenuContainer.layout(0, 0, width, mMenuSize);
mContentContainer.layout(0, mMenuSize, width, height);
break;
case BOTTOM:
mMenuContainer.layout(0, height - mMenuSize, width, height);
mContentContainer.layout(0, 0, width, height - mMenuSize);
break;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
throw new IllegalStateException("Must measure with an exact size");
}
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int height = MeasureSpec.getSize(heightMeasureSpec);
switch (getPosition()) {
case LEFT:
case RIGHT: {
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
final int menuWidth = mMenuSize;
final int menuWidthMeasureSpec = MeasureSpec.makeMeasureSpec(menuWidth, MeasureSpec.EXACTLY);
final int contentWidth = width - menuWidth;
final int contentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY);
mContentContainer.measure(contentWidthMeasureSpec, childHeightMeasureSpec);
mMenuContainer.measure(menuWidthMeasureSpec, childHeightMeasureSpec);
break;
}
case TOP:
case BOTTOM: {
final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
final int menuHeight = mMenuSize;
final int menuHeightMeasureSpec = MeasureSpec.makeMeasureSpec(menuHeight, MeasureSpec.EXACTLY);
final int contentHeight = height - menuHeight;
final int contentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY);
mContentContainer.measure(childWidthMeasureSpec, contentHeightMeasureSpec);
mMenuContainer.measure(childWidthMeasureSpec, menuHeightMeasureSpec);
break;
}
}
setMeasuredDimension(width, height);
}
@Override
public void toggleMenu(boolean animate) {
// NO-OP
}
@Override
public void openMenu(boolean animate) {
// NO-OP
}
@Override
public void closeMenu(boolean animate) {
// NO-OP
}
@Override
public boolean isMenuVisible() {
return true;
}
@Override
public void setMenuSize(int size) {
mMenuSize = size;
requestLayout();
invalidate();
}
@Override
public void setOffsetMenuEnabled(boolean offsetMenu) {
// NO-OP
}
@Override
public boolean getOffsetMenuEnabled() {
return false;
}
@Override
public void peekDrawer() {
// NO-OP
}
@Override
public void peekDrawer(long delay) {
// NO-OP
}
@Override
public void peekDrawer(long startDelay, long delay) {
// NO-OP
}
@Override
public void setHardwareLayerEnabled(boolean enabled) {
// NO-OP
}
@Override
public int getTouchMode() {
return TOUCH_MODE_NONE;
}
@Override
public void setTouchMode(int mode) {
// NO-OP
}
@Override
public void setTouchBezelSize(int size) {
// NO-OP
}
@Override
public int getTouchBezelSize() {
return 0;
}
@Override
public void setSlideDrawable(int drawableRes) {
// NO-OP
}
@Override
public void setSlideDrawable(Drawable drawable) {
// NO-OP
}
@Override
public void setupUpIndicator(Activity activity) {
// NO-OP
}
@Override
public void setDrawerIndicatorEnabled(boolean enabled) {
// NO-OP
}
@Override
public boolean isDrawerIndicatorEnabled() {
return false;
}
}

View File

@ -1,50 +0,0 @@
package net.simonvt.menudrawer;
import android.os.Build;
import android.view.View;
final class ViewHelper {
private ViewHelper() {
}
public static int getLeft(View v) {
if (MenuDrawer.USE_TRANSLATIONS) {
return (int) (v.getLeft() + v.getTranslationX());
}
return v.getLeft();
}
public static int getTop(View v) {
if (MenuDrawer.USE_TRANSLATIONS) {
return (int) (v.getTop() + v.getTranslationY());
}
return v.getTop();
}
public static int getRight(View v) {
if (MenuDrawer.USE_TRANSLATIONS) {
return (int) (v.getRight() + v.getTranslationX());
}
return v.getRight();
}
public static int getBottom(View v) {
if (MenuDrawer.USE_TRANSLATIONS) {
return (int) (v.getBottom() + v.getTranslationY());
}
return v.getBottom();
}
public static int getLayoutDirection(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return v.getLayoutDirection();
}
return View.LAYOUT_DIRECTION_LTR;
}
}

View File

@ -1,79 +0,0 @@
package net.simonvt.menudrawer.compat;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.os.Build;
import timber.log.Timber;
import java.lang.reflect.Method;
public final class ActionBarHelper {
static final boolean DEBUG = false;
private Activity mActivity;
private Object mIndicatorInfo;
private boolean mUsesCompat;
public ActionBarHelper(Activity activity) {
mActivity = activity;
try {
Class clazz = activity.getClass();
Method m = clazz.getMethod("getSupportActionBar");
mUsesCompat = true;
} catch (NoSuchMethodException e) {
if (DEBUG) {
Timber.e(e,
"Activity " + activity.getClass().getSimpleName() + " does not use a compatibility action bar");
}
}
mIndicatorInfo = getIndicatorInfo();
}
private Object getIndicatorInfo() {
if (mUsesCompat && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return ActionBarHelperCompat.getIndicatorInfo(mActivity);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
return ActionBarHelperNative.getIndicatorInfo(mActivity);
}
return null;
}
public void setActionBarUpIndicator(Drawable drawable, int contentDesc) {
if (mUsesCompat && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ActionBarHelperCompat.setActionBarUpIndicator(mIndicatorInfo, mActivity, drawable, contentDesc);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
ActionBarHelperNative.setActionBarUpIndicator(mIndicatorInfo, mActivity, drawable, contentDesc);
}
}
public void setActionBarDescription(int contentDesc) {
if (mUsesCompat && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ActionBarHelperCompat.setActionBarDescription(mIndicatorInfo, mActivity, contentDesc);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
ActionBarHelperNative.setActionBarDescription(mIndicatorInfo, mActivity, contentDesc);
}
}
public Drawable getThemeUpIndicator() {
if (mUsesCompat && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return ActionBarHelperCompat.getThemeUpIndicator(mIndicatorInfo);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
return ActionBarHelperNative.getThemeUpIndicator(mIndicatorInfo, mActivity);
}
return null;
}
public void setDisplayShowHomeAsUpEnabled(boolean enabled) {
if (mUsesCompat && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ActionBarHelperCompat.setDisplayHomeAsUpEnabled(mIndicatorInfo, enabled);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
ActionBarHelperNative.setDisplayHomeAsUpEnabled(mActivity, enabled);
}
}
}

View File

@ -1,105 +0,0 @@
package net.simonvt.menudrawer.compat;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import timber.log.Timber;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import java.lang.reflect.Method;
final class ActionBarHelperCompat {
private ActionBarHelperCompat() {
}
public static void setActionBarUpIndicator(Object info, Activity activity, Drawable drawable, int contentDescRes) {
final SetIndicatorInfo sii = (SetIndicatorInfo) info;
if (sii.mUpIndicatorView != null) {
sii.mUpIndicatorView.setImageDrawable(drawable);
final String contentDescription = contentDescRes == 0 ? null : activity.getString(contentDescRes);
sii.mUpIndicatorView.setContentDescription(contentDescription);
}
}
public static void setActionBarDescription(Object info, Activity activity, int contentDescRes) {
final SetIndicatorInfo sii = (SetIndicatorInfo) info;
if (sii.mUpIndicatorView != null) {
final String contentDescription = contentDescRes == 0 ? null : activity.getString(contentDescRes);
sii.mUpIndicatorView.setContentDescription(contentDescription);
}
}
public static Drawable getThemeUpIndicator(Object info) {
final SetIndicatorInfo sii = (SetIndicatorInfo) info;
if (sii.mUpIndicatorView != null) {
return sii.mUpIndicatorView.getDrawable();
}
return null;
}
public static Object getIndicatorInfo(Activity activity) {
return new SetIndicatorInfo(activity);
}
public static void setDisplayHomeAsUpEnabled(Object info, boolean enabled) {
final SetIndicatorInfo sii = (SetIndicatorInfo) info;
if (sii.mHomeAsUpEnabled != null) {
try {
sii.mHomeAsUpEnabled.invoke(sii.mActionBar, enabled);
} catch (Throwable t) {
if (ActionBarHelper.DEBUG) {
Timber.e(t, "Unable to call setHomeAsUpEnabled");
}
}
}
}
private static class SetIndicatorInfo {
public ImageView mUpIndicatorView;
public Object mActionBar;
public Method mHomeAsUpEnabled;
SetIndicatorInfo(Activity activity) {
try {
String appPackage = activity.getPackageName();
try {
// Attempt to find ActionBarSherlock up indicator
final int homeId = activity.getResources().getIdentifier("abs__home", "id", appPackage);
View v = activity.findViewById(homeId);
ViewGroup parent = (ViewGroup) v.getParent();
final int upId = activity.getResources().getIdentifier("abs__up", "id", appPackage);
mUpIndicatorView = (ImageView) parent.findViewById(upId);
} catch (Throwable t) {
if (ActionBarHelper.DEBUG) {
Timber.e(t,"ABS action bar not found");
}
}
if (mUpIndicatorView == null) {
// Attempt to find AppCompat up indicator
final int homeId = activity.getResources().getIdentifier("home", "id", appPackage);
View v = activity.findViewById(homeId);
ViewGroup parent = (ViewGroup) v.getParent();
final int upId = activity.getResources().getIdentifier("up", "id", appPackage);
mUpIndicatorView = (ImageView) parent.findViewById(upId);
}
Class supportActivity = activity.getClass();
Method getActionBar = supportActivity.getMethod("getSupportActionBar");
mActionBar = getActionBar.invoke(activity, (Object)null);
Class supportActionBar = mActionBar.getClass();
mHomeAsUpEnabled = supportActionBar.getMethod("setDisplayHomeAsUpEnabled", Boolean.TYPE);
} catch (Throwable t) {
if (ActionBarHelper.DEBUG) {
Timber.e(t, "Unable to init SetIndicatorInfo for ABS");
}
}
}
}
}

View File

@ -1,112 +0,0 @@
package net.simonvt.menudrawer.compat;
import android.app.ActionBar;
import android.app.Activity;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import timber.log.Timber;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import java.lang.reflect.Method;
final class ActionBarHelperNative {
private ActionBarHelperNative() {
}
private static final int[] THEME_ATTRS = new int[] {
android.R.attr.homeAsUpIndicator
};
public static void setActionBarUpIndicator(Object info, Activity activity, Drawable drawable, int contentDescRes) {
final SetIndicatorInfo sii = (SetIndicatorInfo) info;
if (sii.setHomeAsUpIndicator != null) {
try {
final ActionBar actionBar = activity.getActionBar();
sii.setHomeAsUpIndicator.invoke(actionBar, drawable);
sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes);
} catch (Throwable t) {
if (ActionBarHelper.DEBUG) Timber.e(t, "Couldn't set home-as-up indicator via JB-MR2 API");
}
} else if (sii.upIndicatorView != null) {
sii.upIndicatorView.setImageDrawable(drawable);
} else {
if (ActionBarHelper.DEBUG) Timber.e("Couldn't set home-as-up indicator");
}
}
public static void setActionBarDescription(Object info, Activity activity, int contentDescRes) {
final SetIndicatorInfo sii = (SetIndicatorInfo) info;
if (sii.setHomeAsUpIndicator != null) {
try {
final ActionBar actionBar = activity.getActionBar();
sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes);
} catch (Throwable t) {
if (ActionBarHelper.DEBUG) Timber.e(t, "Couldn't set content description via JB-MR2 API");
}
}
}
public static Drawable getThemeUpIndicator(Object info, Activity activity) {
final TypedArray a = activity.obtainStyledAttributes(THEME_ATTRS);
final Drawable result = a.getDrawable(0);
a.recycle();
return result;
}
public static Object getIndicatorInfo(Activity activity) {
return new SetIndicatorInfo(activity);
}
public static void setDisplayHomeAsUpEnabled(Activity activity, boolean b) {
ActionBar actionBar = activity.getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(b);
}
}
private static class SetIndicatorInfo {
public Method setHomeAsUpIndicator;
public Method setHomeActionContentDescription;
public ImageView upIndicatorView;
SetIndicatorInfo(Activity activity) {
try {
setHomeAsUpIndicator = ActionBar.class.getDeclaredMethod("setHomeAsUpIndicator", Drawable.class);
setHomeActionContentDescription = ActionBar.class.getDeclaredMethod(
"setHomeActionContentDescription", Integer.TYPE);
// If we got the method we won't need the stuff below.
return;
} catch (Throwable t) {
// Oh well. We'll use the other mechanism below instead.
}
final View home = activity.findViewById(android.R.id.home);
if (home == null) {
// Action bar doesn't have a known configuration, an OEM messed with things.
return;
}
final ViewGroup parent = (ViewGroup) home.getParent();
final int childCount = parent.getChildCount();
if (childCount != 2) {
// No idea which one will be the right one, an OEM messed with things.
return;
}
final View first = parent.getChildAt(0);
final View second = parent.getChildAt(1);
final View up = first.getId() == android.R.id.home ? second : first;
if (up instanceof ImageView) {
// Jackpot! (Probably...)
upIndicatorView = (ImageView) up;
}
}
}
}

View File

@ -1,65 +0,0 @@
<resources>
<!-- Reference to a style for the menu drawer. -->
<attr name="menuDrawerStyle" format="reference" />
<!-- Styleables used for styling the menu drawer. -->
<declare-styleable name="MenuDrawer">
<!-- Drawable to use for the background of the content. -->
<attr name="mdContentBackground" format="reference" />
<!-- Drawable to use for the background of the menu. -->
<attr name="mdMenuBackground" format="reference" />
<!-- The size of the menu. -->
<attr name="mdMenuSize" format="dimension" />
<!-- Drawable used as indicator for the active view. -->
<attr name="mdActiveIndicator" format="reference" />
<!-- Defines whether the content will have a dropshadow onto the menu. Default is true. -->
<attr name="mdDropShadowEnabled" format="boolean" />
<!-- The size of the drop shadow. Default is 6dp -->
<attr name="mdDropShadowSize" format="dimension" />
<!-- The color of the drop shadow. Default is #FF000000. -->
<attr name="mdDropShadowColor" format="color" />
<!-- Drawable used for the drop shadow. -->
<attr name="mdDropShadow" format="reference" />
<!-- The touch bezel size. -->
<attr name="mdTouchBezelSize" format="dimension" />
<!-- Whether the indicator should be animated between active views. -->
<attr name="mdAllowIndicatorAnimation" format="boolean" />
<!-- The maximum animation duration -->
<attr name="mdMaxAnimationDuration" format="integer" />
<!-- Drawable that replaces the up indicator -->
<attr name="mdSlideDrawable" format="reference" />
<!-- String to use as the up indicators content description when the drawer is open -->
<attr name="mdDrawerOpenUpContentDescription" format="string" />
<!-- String to use as the up indicators content description when the drawer is closed -->
<attr name="mdDrawerClosedUpContentDescription" format="string" />
<!-- Whether an overlay should be drawn as the drawer is opened and closed -->
<attr name="mdDrawOverlay" format="boolean" />
<!-- The position of the drawer -->
<attr name="mdPosition" format="enum">
<enum name="left" value="0" />
<enum name="top" value="1" />
<enum name="right" value="2" />
<enum name="bottom" value="3" />
<enum name="start" value="4" />
<enum name="end" value="5" />
</attr>
</declare-styleable>
</resources>

View File

@ -1,6 +0,0 @@
<resources>
<!-- The default background of the menu. -->
<color name="md__defaultBackground">#FF555555</color>
</resources>

View File

@ -1,24 +0,0 @@
<resources>
<!-- ID used when defining the content layout in XML. -->
<item name="mdContent" type="id" />
<!-- ID used when defining the menu layout in XML. -->
<item name="mdMenu" type="id" />
<!-- The ID of the content container. -->
<item name="md__content" type="id" />
<!-- The ID of the menu container. -->
<item name="md__menu" type="id" />
<!-- The ID of the drawer. -->
<item name="md__drawer" type="id" />
<!-- Used with View#setTag(int) to specify a position for the active view. -->
<item name="mdActiveViewPosition" type="id" />
<item name="md__translationX" type="id" />
<item name="md__translationY" type="id" />
</resources>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="md__drawerOpenIndicatorDesc" tools:ignore="MissingTranslation">Close drawer</string>
<string name="md__drawerClosedIndicatorDesc" tools:ignore="MissingTranslation">Open drawer</string>
</resources>

View File

@ -1,13 +0,0 @@
<resources>
<style name="Widget" />
<!-- Base theme for the menu drawer. -->
<style name="Widget.MenuDrawer">
<item name="mdMenuBackground">@color/md__defaultBackground</item>
<item name="mdContentBackground">?android:attr/windowBackground</item>
<item name="mdDrawerOpenUpContentDescription">@string/md__drawerOpenIndicatorDesc</item>
<item name="mdDrawerClosedUpContentDescription">@string/md__drawerClosedIndicatorDesc</item>
</style>
</resources>

View File

@ -4,11 +4,13 @@ ext.versions = [
compileSdk : 29,
gradle : '6.5',
navigation : "2.3.2",
androidTools : "4.0.0",
ktlint : "0.37.1",
ktlintGradle : "9.2.1",
detekt : "1.0.0.RC6-4",
jacoco : "0.8.5",
preferences : "1.1.1",
androidSupport : "28.0.0",
androidLegacySupport : "1.0.0",
@ -25,7 +27,7 @@ ext.versions = [
okhttp : "3.10.0",
semver : "1.0.0",
twitterSerial : "0.1.6",
koin : "2.1.6",
koin : "2.2.2",
picasso : "2.71828",
junit4 : "4.12",
@ -42,23 +44,29 @@ ext.versions = [
]
ext.gradlePlugins = [
androidTools : "com.android.tools.build:gradle:$versions.androidTools",
kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
ktlintGradle : "org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle",
detekt : "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt",
jacoco : "org.jacoco:org.jacoco.core:$versions.jacoco"
androidTools : "com.android.tools.build:gradle:$versions.androidTools",
kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
ktlintGradle : "org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle",
detekt : "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt",
jacoco : "org.jacoco:org.jacoco.core:$versions.jacoco",
]
ext.androidSupport = [
support : "androidx.legacy:legacy-support-v4:$versions.androidLegacySupport",
design : "com.google.android.material:material:$versions.androidSupportDesign",
annotations : "com.android.support:support-annotations:$versions.androidSupport",
multidex : "androidx.multidex:multidex:$versions.multidex",
constraintLayout : "androidx.constraintlayout:constraintlayout:$versions.constraintLayout",
room : "androidx.room:room-compiler:$versions.room",
roomRuntime : "androidx.room:room-runtime:$versions.room",
roomKtx : "androidx.room:room-ktx:$versions.room",
viewModelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.viewModelKtx"
support : "androidx.legacy:legacy-support-v4:$versions.androidLegacySupport",
design : "com.google.android.material:material:$versions.androidSupportDesign",
annotations : "com.android.support:support-annotations:$versions.androidSupport",
multidex : "androidx.multidex:multidex:$versions.multidex",
constraintLayout : "androidx.constraintlayout:constraintlayout:$versions.constraintLayout",
room : "androidx.room:room-compiler:$versions.room",
roomRuntime : "androidx.room:room-runtime:$versions.room",
roomKtx : "androidx.room:room-ktx:$versions.room",
viewModelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.viewModelKtx",
navigationFragment : "androidx.navigation:navigation-fragment:$versions.navigation",
navigationUi : "androidx.navigation:navigation-ui:$versions.navigation",
navigationFragmentKtx : "androidx.navigation:navigation-fragment-ktx:$versions.navigation",
navigationUiKtx : "androidx.navigation:navigation-ui-ktx:$versions.navigation",
navigationFeature : "androidx.navigation:navigation-dynamic-features-fragment:$versions.navigation",
preferences : "androidx.preference:preference:$versions.preferences",
]
ext.other = [

View File

@ -3,5 +3,4 @@ include ':core:domain'
include ':core:subsonic-api'
include ':core:subsonic-api-image-loader'
include ':core:cache'
include ':core:menudrawer'
include ':ultrasonic'

View File

@ -50,6 +50,10 @@ android {
warning 'MissingTranslation'
abortOnError true
}
kotlinOptions {
jvmTarget = "1.8"
}
}
tasks.withType(Test) {
@ -57,7 +61,6 @@ tasks.withType(Test) {
}
dependencies {
implementation project(':core:menudrawer')
implementation project(':core:library')
implementation project(':core:domain')
implementation project(':core:subsonic-api')
@ -71,6 +74,13 @@ dependencies {
implementation androidSupport.roomKtx
implementation androidSupport.viewModelKtx
implementation androidSupport.constraintLayout
implementation androidSupport.preferences
implementation androidSupport.navigationFragment
implementation androidSupport.navigationUi
implementation androidSupport.navigationFragmentKtx
implementation androidSupport.navigationUiKtx
implementation androidSupport.navigationFeature
implementation other.kotlinStdlib
implementation other.kotlinxCoroutines

View File

@ -23,108 +23,31 @@
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.AppCompat"
android:theme="@style/NoActionBar"
android:name=".app.UApp"
android:label="@string/common.appname"
android:usesCleartextTraffic="true">
<activity
android:name=".activity.MainActivity"
<activity android:name=".activity.NavigationActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/common.appname"
android:launchMode="standard">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".activity.SelectArtistActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="standard"/>
<activity
android:name=".activity.SelectAlbumActivity"
android:configChanges="orientation|keyboardHidden"/>
<activity
android:name=".activity.SearchActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/search.label"
android:launchMode="singleTask"/>
<activity
android:name=".activity.SelectPlaylistActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/playlist.label"
android:launchMode="standard"/>
<activity
android:name=".activity.PodcastsActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/podcasts.label"
android:launchMode="standard"/>
<activity
android:name=".activity.BookmarkActivity"
android:configChanges="orientation|keyboardHidden"/>
<activity
android:name=".activity.ShareActivity"
android:configChanges="orientation|keyboardHidden"/>
<activity
android:name=".activity.ChatActivity"
android:configChanges="orientation|keyboardHidden"/>
<activity
android:name=".activity.DownloadActivity"
android:configChanges="keyboardHidden"
android:launchMode="singleTask"
android:exported="true" />
<activity
android:name=".activity.SettingsActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="singleTask"/>
<activity
android:name=".activity.HelpActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="singleTask"/>
<activity
android:name=".activity.LyricsActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="singleTask"/>
<activity
android:name=".activity.EqualizerActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/equalizer.label"
android:launchMode="singleTask"/>
<activity
android:name=".activity.SelectGenreActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="standard"/>
<activity
android:name=".activity.VoiceQueryReceiverActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".activity.QueryReceiverActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SEARCH"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<nav-graph android:value="@navigation/navigation_graph" />
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
<activity
android:name=".activity.ServerSelectorActivity"
android:label="@string/server_selector.label" />
<activity
android:name=".activity.EditServerActivity"
android:label="@string/server_editor.label" />
<service
android:name=".service.MediaPlayerService"
android:label="Ultrasonic Download Service"
android:label="Ultrasonic Media Player Service"
android:exported="false">
</service>
@ -202,10 +125,6 @@
android:name=".provider.SearchSuggestionProvider"
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/>
<meta-data
android:name="android.app.default_searchable"
android:value="org.moire.ultrasonic.activity.QueryReceiverActivity"/>
<receiver
android:name=".receiver.A2dpIntentReceiver"
android:exported="false">

View File

@ -1,485 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Pair;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.EntryAdapter;
import java.util.ArrayList;
import java.util.List;
public class BookmarkActivity extends SubsonicTabActivity
{
private SwipeRefreshLayout refreshAlbumListView;
private ListView albumListView;
private View albumButtons;
private View emptyView;
private ImageView playNowButton;
private ImageView pinButton;
private ImageView unpinButton;
private ImageView downloadButton;
private ImageView deleteButton;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.select_album);
albumButtons = findViewById(R.id.menu_album);
refreshAlbumListView = findViewById(R.id.select_album_entries_refresh);
albumListView = findViewById(R.id.select_album_entries_list);
refreshAlbumListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
albumListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
albumListView.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (position >= 0)
{
Entry entry = (Entry) parent.getItemAtPosition(position);
if (entry != null)
{
if (entry.isVideo())
{
playVideo(entry);
}
else
{
enableButtons();
}
}
}
}
});
ImageView selectButton = (ImageView) findViewById(R.id.select_album_select);
playNowButton = (ImageView) findViewById(R.id.select_album_play_now);
ImageView playNextButton = (ImageView) findViewById(R.id.select_album_play_next);
ImageView playLastButton = (ImageView) findViewById(R.id.select_album_play_last);
pinButton = (ImageView) findViewById(R.id.select_album_pin);
unpinButton = (ImageView) findViewById(R.id.select_album_unpin);
downloadButton = (ImageView) findViewById(R.id.select_album_download);
deleteButton = (ImageView) findViewById(R.id.select_album_delete);
ImageView oreButton = (ImageView) findViewById(R.id.select_album_more);
emptyView = findViewById(R.id.select_album_empty);
selectButton.setVisibility(View.GONE);
playNextButton.setVisibility(View.GONE);
playLastButton.setVisibility(View.GONE);
oreButton.setVisibility(View.GONE);
playNowButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
playNow(getSelectedSongs(albumListView));
}
});
selectButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
selectAllOrNone();
}
});
pinButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
downloadBackground(true);
selectAll(false, false);
}
});
unpinButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
unpin();
selectAll(false, false);
}
});
downloadButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
downloadBackground(false);
selectAll(false, false);
}
});
deleteButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
delete();
selectAll(false, false);
}
});
registerForContextMenu(albumListView);
enableButtons();
View browseMenuItem = findViewById(R.id.menu_bookmarks);
menuDrawer.setActiveView(browseMenuItem);
getBookmarks();
}
private void getBookmarks()
{
setActionBarSubtitle(R.string.button_bar_bookmarks);
new LoadTask()
{
@Override
protected MusicDirectory load(MusicService service) throws Exception
{
return Util.getSongsFromBookmarks(service.getBookmarks(BookmarkActivity.this, this));
}
}.execute();
}
private void playNow(List<Entry> songs)
{
if (!getSelectedSongs(albumListView).isEmpty())
{
int position = songs.get(0).getBookmarkPosition();
if (getMediaPlayerController() == null) return;
getMediaPlayerController().restore(songs, 0, position, true, true);
selectAll(false, false);
}
}
private static List<MusicDirectory.Entry> getSelectedSongs(ListView albumListView)
{
List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>(10);
if (albumListView != null)
{
int count = albumListView.getCount();
for (int i = 0; i < count; i++)
{
if (albumListView.isItemChecked(i))
{
songs.add((MusicDirectory.Entry) albumListView.getItemAtPosition(i));
}
}
}
return songs;
}
private void refresh()
{
finish();
Intent intent = getIntent();
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
}
return false;
}
private void selectAllOrNone()
{
boolean someUnselected = false;
int count = albumListView.getCount();
for (int i = 0; i < count; i++)
{
if (!albumListView.isItemChecked(i) && albumListView.getItemAtPosition(i) instanceof MusicDirectory.Entry)
{
someUnselected = true;
break;
}
}
selectAll(someUnselected, true);
}
private void selectAll(boolean selected, boolean toast)
{
int count = albumListView.getCount();
int selectedCount = 0;
for (int i = 0; i < count; i++)
{
MusicDirectory.Entry entry = (MusicDirectory.Entry) albumListView.getItemAtPosition(i);
if (entry != null && !entry.isDirectory() && !entry.isVideo())
{
albumListView.setItemChecked(i, selected);
selectedCount++;
}
}
// Display toast: N tracks selected / N tracks unselected
if (toast)
{
int toastResId = selected ? R.string.select_album_n_selected : R.string.select_album_n_unselected;
Util.toast(this, getString(toastResId, selectedCount));
}
enableButtons();
}
private void enableButtons()
{
MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (mediaPlayerController == null)
{
return;
}
List<MusicDirectory.Entry> selection = getSelectedSongs(albumListView);
boolean enabled = !selection.isEmpty();
boolean unpinEnabled = false;
boolean deleteEnabled = false;
int pinnedCount = 0;
for (MusicDirectory.Entry song : selection)
{
DownloadFile downloadFile = mediaPlayerController.getDownloadFileForSong(song);
if (downloadFile.isWorkDone())
{
deleteEnabled = true;
}
if (downloadFile.isSaved())
{
pinnedCount++;
unpinEnabled = true;
}
}
playNowButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE);
pinButton.setVisibility((enabled && !ActiveServerProvider.Companion.isOffline(this) && selection.size() > pinnedCount) ? View.VISIBLE : View.GONE);
unpinButton.setVisibility(enabled && unpinEnabled ? View.VISIBLE : View.GONE);
downloadButton.setVisibility(enabled && !deleteEnabled && !ActiveServerProvider.Companion.isOffline(this) ? View.VISIBLE : View.GONE);
deleteButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE);
}
private void downloadBackground(final boolean save)
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
if (songs.isEmpty())
{
selectAll(true, false);
songs = getSelectedSongs(albumListView);
}
downloadBackground(save, songs);
}
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{
if (getMediaPlayerController() == null)
{
return;
}
Runnable onValid = new Runnable()
{
@Override
public void run()
{
warnIfNetworkOrStorageUnavailable();
getMediaPlayerController().downloadBackground(songs, save);
if (save)
{
Util.toast(BookmarkActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
}
else
{
Util.toast(BookmarkActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
}
}
};
checkLicenseAndTrialPeriod(onValid);
}
private void delete()
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
if (songs.isEmpty())
{
selectAll(true, false);
songs = getSelectedSongs(albumListView);
}
if (getMediaPlayerController() != null)
{
getMediaPlayerController().delete(songs);
}
}
private void unpin()
{
if (getMediaPlayerController() != null)
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
Util.toast(BookmarkActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
getMediaPlayerController().unpin(songs);
}
}
private abstract class LoadTask extends TabActivityBackgroundTask<Pair<MusicDirectory, Boolean>>
{
public LoadTask()
{
super(BookmarkActivity.this, true);
}
protected abstract MusicDirectory load(MusicService service) throws Exception;
@Override
protected Pair<MusicDirectory, Boolean> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(BookmarkActivity.this);
MusicDirectory dir = load(musicService);
boolean valid = musicService.isLicenseValid(BookmarkActivity.this, this);
return new Pair<MusicDirectory, Boolean>(dir, valid);
}
@Override
protected void done(Pair<MusicDirectory, Boolean> result)
{
MusicDirectory musicDirectory = result.getFirst();
List<MusicDirectory.Entry> entries = musicDirectory.getChildren();
int songCount = 0;
for (MusicDirectory.Entry entry : entries)
{
if (!entry.isDirectory())
{
songCount++;
}
}
final int listSize = getIntent().getIntExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
if (songCount > 0)
{
pinButton.setVisibility(View.VISIBLE);
unpinButton.setVisibility(View.VISIBLE);
downloadButton.setVisibility(View.VISIBLE);
deleteButton.setVisibility(View.VISIBLE);
playNowButton.setVisibility(View.VISIBLE);
}
else
{
pinButton.setVisibility(View.GONE);
unpinButton.setVisibility(View.GONE);
downloadButton.setVisibility(View.GONE);
deleteButton.setVisibility(View.GONE);
playNowButton.setVisibility(View.GONE);
if (listSize == 0 || result.getFirst().getChildren().size() < listSize)
{
albumButtons.setVisibility(View.GONE);
}
}
enableButtons();
emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE);
albumListView.setAdapter(new EntryAdapter(BookmarkActivity.this, getImageLoader(), entries, true));
licenseValid = result.getSecond();
}
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -1,310 +0,0 @@
package org.moire.ultrasonic.activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.ChatMessage;
import org.moire.ultrasonic.service.JukeboxMediaPlayer;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ChatAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* @author Joshua Bahnsen
*/
public final class ChatActivity extends SubsonicTabActivity
{
private ListView chatListView;
private EditText messageEditText;
private ImageButton sendButton;
private Timer timer;
private volatile static Long lastChatMessageTime = (long) 0;
private volatile static ArrayList<ChatMessage> messageList = new ArrayList<ChatMessage>();
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
@Override
protected void onCreate(Bundle bundle)
{
super.onCreate(bundle);
setContentView(R.layout.chat);
messageEditText = (EditText) findViewById(R.id.chat_edittext);
sendButton = (ImageButton) findViewById(R.id.chat_send);
sendButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
sendMessage();
}
});
chatListView = findViewById(R.id.chat_entries_list);
chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
chatListView.setStackFromBottom(true);
String serverName = activeServerProvider.getValue().getActiveServer().getName();
String userName = activeServerProvider.getValue().getActiveServer().getUserName();
String title = String.format("%s [%s@%s]", getResources().getString(R.string.button_bar_chat), userName, serverName);
setActionBarSubtitle(title);
messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER);
messageEditText.addTextChangedListener(new TextWatcher()
{
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
}
@Override
public void afterTextChanged(Editable editable)
{
sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString()));
}
});
messageEditText.setOnEditorActionListener(new TextView.OnEditorActionListener()
{
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event)
{
if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN))
{
sendMessage();
return true;
}
return false;
}
});
View chatMenuItem = findViewById(R.id.menu_chat);
menuDrawer.setActiveView(chatMenuItem);
load();
}
@Override
protected void onPostCreate(Bundle bundle)
{
super.onPostCreate(bundle);
timerMethod();
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.chat, menu);
super.onCreateOptionsMenu(menu);
return true;
}
/*
* Listen for option item selections so that we receive a notification
* when the user requests a refresh by selecting the refresh action bar item.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Check if user triggered a refresh:
case R.id.menu_refresh:
// Start the refresh background task.
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return true;
}
// User didn't trigger a refresh, let the superclass handle this action
return super.onOptionsItemSelected(item);
}
@Override
protected void onResume()
{
super.onResume();
if (!messageList.isEmpty())
{
ListAdapter chatAdapter = new ChatAdapter(ChatActivity.this, messageList);
chatListView.setAdapter(chatAdapter);
}
if (timer == null)
{
timerMethod();
}
}
@Override
protected void onPause()
{
super.onPause();
if (timer != null)
{
timer.cancel();
timer = null;
}
}
private void timerMethod()
{
int refreshInterval = Util.getChatRefreshInterval(this);
if (refreshInterval > 0)
{
timer = new Timer();
timer.schedule(new TimerTask()
{
@Override
public void run()
{
ChatActivity.this.runOnUiThread(new Runnable()
{
@Override
public void run()
{
load();
}
});
}
}, refreshInterval, refreshInterval);
}
}
private void sendMessage()
{
if (messageEditText != null)
{
final String message;
Editable text = messageEditText.getText();
if (text == null)
{
return;
}
message = text.toString();
if (!Util.isNullOrWhiteSpace(message))
{
messageEditText.setText("");
BackgroundTask<Void> task = new TabActivityBackgroundTask<Void>(ChatActivity.this, false)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(ChatActivity.this);
musicService.addChatMessage(message, ChatActivity.this, this);
return null;
}
@Override
protected void done(Void result)
{
load();
}
};
task.execute();
}
}
}
private synchronized void load()
{
BackgroundTask<List<ChatMessage>> task = new TabActivityBackgroundTask<List<ChatMessage>>(this, false)
{
@Override
protected List<ChatMessage> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(ChatActivity.this);
return musicService.getChatMessages(lastChatMessageTime, ChatActivity.this, this);
}
@Override
protected void done(List<ChatMessage> result)
{
if (result != null && !result.isEmpty())
{
// Reset lastChatMessageTime if we have a newer message
for (ChatMessage message : result)
{
if (message.getTime() > lastChatMessageTime)
{
lastChatMessageTime = message.getTime();
}
}
// Reverse results to show them on the bottom
Collections.reverse(result);
messageList.addAll(result);
ListAdapter chatAdapter = new ChatAdapter(ChatActivity.this, messageList);
chatListView.setAdapter(chatAdapter);
}
}
};
task.execute();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
load();
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
return null;
}
}
}

View File

@ -1,280 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.media.audiofx.Equalizer;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.lifecycle.Observer;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.audiofx.EqualizerController;
import org.moire.ultrasonic.service.MediaPlayerController;
import java.util.HashMap;
import java.util.Map;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Equalizer controls.
*
* @author Sindre Mehus
* @version $Id$
*/
public class EqualizerActivity extends ResultActivity
{
private static final int MENU_GROUP_PRESET = 100;
private final Map<Short, SeekBar> bars = new HashMap<Short, SeekBar>();
private EqualizerController equalizerController;
private Equalizer equalizer;
@Override
public void onCreate(Bundle bundle)
{
super.onCreate(bundle);
setContentView(R.layout.equalizer);
EqualizerController.get().observe(this, new Observer<EqualizerController>() {
@Override
public void onChanged(EqualizerController controller) {
if (controller != null) {
Timber.d("EqualizerController Observer.onChanged received controller");
equalizerController = controller;
equalizer = controller.equalizer;
setup();
} else {
Timber.d("EqualizerController Observer.onChanged has no controller");
equalizerController = null;
equalizer = null;
}
}
});
}
@Override
protected void onPause()
{
super.onPause();
if (equalizerController == null) return;
equalizerController.saveSettings();
}
@Override
protected void onResume()
{
super.onResume();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
if (equalizer == null) return;
short currentPreset;
try
{
currentPreset = equalizer.getCurrentPreset();
}
catch (Exception x)
{
currentPreset = -1;
}
for (short preset = 0; preset < equalizer.getNumberOfPresets(); preset++)
{
MenuItem menuItem = menu.add(MENU_GROUP_PRESET, preset, preset, equalizer.getPresetName(preset));
if (preset == currentPreset)
{
menuItem.setChecked(true);
}
}
menu.setGroupCheckable(MENU_GROUP_PRESET, true, true);
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
if (equalizer == null) return true;
try
{
short preset = (short) menuItem.getItemId();
equalizer.usePreset(preset);
updateBars();
}
catch (Exception ex)
{
//TODO: Show a dialog
}
return true;
}
private void setup()
{
initEqualizer();
final View presetButton = findViewById(R.id.equalizer_preset);
registerForContextMenu(presetButton);
presetButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
presetButton.showContextMenu();
}
});
CheckBox enabledCheckBox = (CheckBox) findViewById(R.id.equalizer_enabled);
enabledCheckBox.setChecked(equalizer.getEnabled());
enabledCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
{
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b)
{
setEqualizerEnabled(b);
}
});
}
private void setEqualizerEnabled(boolean enabled)
{
if (equalizer == null) return;
equalizer.setEnabled(enabled);
updateBars();
}
private void updateBars()
{
if (equalizer == null) return;
try
{
for (Map.Entry<Short, SeekBar> entry : bars.entrySet())
{
short band = entry.getKey();
SeekBar bar = entry.getValue();
bar.setEnabled(equalizer.getEnabled());
short minEQLevel = equalizer.getBandLevelRange()[0];
bar.setProgress(equalizer.getBandLevel(band) - minEQLevel);
}
}
catch (Exception ex)
{
//TODO: Show a dialog
}
}
private void initEqualizer()
{
if (equalizer == null) return;
LinearLayout layout = (LinearLayout) findViewById(R.id.equalizer_layout);
try
{
short[] bandLevelRange = equalizer.getBandLevelRange();
short numberOfBands = equalizer.getNumberOfBands();
final short minEQLevel = bandLevelRange[0];
final short maxEQLevel = bandLevelRange[1];
for (short i = 0; i < numberOfBands; i++)
{
final short band = i;
View bandBar = LayoutInflater.from(this).inflate(R.layout.equalizer_bar, null);
TextView freqTextView;
if (bandBar != null)
{
freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
freqTextView.setText((equalizer.getCenterFreq(band) / 1000) + " Hz");
bars.put(band, bar);
bar.setMax(maxEQLevel - minEQLevel);
short level = equalizer.getBandLevel(band);
bar.setProgress(level - minEQLevel);
bar.setEnabled(equalizer.getEnabled());
updateLevelText(levelTextView, level);
bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener()
{
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
{
short level = (short) (progress + minEQLevel);
if (fromUser)
{
try
{
equalizer.setBandLevel(band, level);
}
catch (Exception ex)
{
//TODO: Show a dialog
}
}
updateLevelText(levelTextView, level);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar)
{
}
@Override
public void onStopTrackingTouch(SeekBar seekBar)
{
}
});
layout.addView(bandBar);
}
}
}
catch (Exception ex)
{
//TODO: Show a dialog
}
}
private static void updateLevelText(TextView levelTextView, short level)
{
if (levelTextView != null)
{
levelTextView.setText(String.format("%s%d dB", level > 0 ? "+" : "", level / 100));
}
}
}

View File

@ -1,325 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.ImageView;
import net.simonvt.menudrawer.MenuDrawer;
import net.simonvt.menudrawer.Position;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util;
/**
* An HTML-based help screen with Back and Done buttons at the bottom.
*
* @author Sindre Mehus
*/
public final class HelpActivity extends ResultActivity implements OnClickListener
{
private WebView webView;
private ImageView backButton;
private ImageView forwardButton;
private static final String STATE_MENUDRAWER = "org.moire.ultrasonic.menuDrawer";
private static final String STATE_ACTIVE_VIEW_ID = "org.moire.ultrasonic.activeViewId";
private static final String STATE_ACTIVE_POSITION = "org.moire.ultrasonic.activePosition";
public MenuDrawer menuDrawer;
private int activePosition = 1;
private int menuActiveViewId;
View chatMenuItem;
View bookmarksMenuItem;
View sharesMenuItem;
@Override
protected void onCreate(Bundle bundle)
{
Util.applyTheme(this);
getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
super.onCreate(bundle);
setContentView(R.layout.help);
if (bundle != null)
{
activePosition = bundle.getInt(STATE_ACTIVE_POSITION);
menuActiveViewId = bundle.getInt(STATE_ACTIVE_VIEW_ID);
}
menuDrawer = MenuDrawer.attach(this, MenuDrawer.Type.BEHIND, Position.LEFT, MenuDrawer.MENU_DRAG_WINDOW);
menuDrawer.setMenuView(R.layout.menu_main);
chatMenuItem = findViewById(R.id.menu_chat);
bookmarksMenuItem = findViewById(R.id.menu_bookmarks);
sharesMenuItem = findViewById(R.id.menu_shares);
View aboutMenuItem = findViewById(R.id.menu_about);
findViewById(R.id.menu_home).setOnClickListener(this);
findViewById(R.id.menu_browse).setOnClickListener(this);
findViewById(R.id.menu_search).setOnClickListener(this);
findViewById(R.id.menu_playlists).setOnClickListener(this);
sharesMenuItem.setOnClickListener(this);
chatMenuItem.setOnClickListener(this);
bookmarksMenuItem.setOnClickListener(this);
findViewById(R.id.menu_now_playing).setOnClickListener(this);
findViewById(R.id.menu_settings).setOnClickListener(this);
aboutMenuItem.setOnClickListener(this);
findViewById(R.id.menu_exit).setOnClickListener(this);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null)
{
actionBar.setDisplayHomeAsUpEnabled(true);
}
menuDrawer.setActiveView(aboutMenuItem);
webView = (WebView) findViewById(R.id.help_contents);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new HelpClient());
if (bundle != null)
{
webView.restoreState(bundle);
}
else
{
webView.loadUrl(getResources().getString(R.string.help_url));
}
backButton = (ImageView) findViewById(R.id.help_back);
backButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.goBack();
}
});
ImageView stopButton = (ImageView) findViewById(R.id.help_stop);
stopButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.stopLoading();
setProgressBarIndeterminateVisibility(false);
}
});
forwardButton = (ImageView) findViewById(R.id.help_forward);
forwardButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.goForward();
}
});
}
@Override
protected void onPostCreate(Bundle bundle)
{
super.onPostCreate(bundle);
int visibility = ActiveServerProvider.Companion.isOffline(this) ? View.GONE : View.VISIBLE;
chatMenuItem.setVisibility(visibility);
bookmarksMenuItem.setVisibility(visibility);
sharesMenuItem.setVisibility(visibility);
}
@Override
public void onResume()
{
super.onResume();
}
@Override
protected void onSaveInstanceState(Bundle state)
{
webView.saveState(state);
super.onSaveInstanceState(state);
state.putParcelable(STATE_MENUDRAWER, menuDrawer.saveState());
state.putInt(STATE_ACTIVE_VIEW_ID, menuActiveViewId);
state.putInt(STATE_ACTIVE_POSITION, activePosition);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
if (keyCode == KeyEvent.KEYCODE_BACK)
{
if (webView.canGoBack())
{
webView.goBack();
return true;
}
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onRestoreInstanceState(Bundle state)
{
super.onRestoreInstanceState(state);
menuDrawer.restoreState(state.getParcelable(STATE_MENUDRAWER));
}
@Override
public void onBackPressed()
{
final int drawerState = menuDrawer.getDrawerState();
if (drawerState == MenuDrawer.STATE_OPEN || drawerState == MenuDrawer.STATE_OPENING)
{
menuDrawer.closeMenu(true);
return;
}
finish();
super.onBackPressed();
}
@Override
public void onClick(View v)
{
menuActiveViewId = v.getId();
menuDrawer.setActiveView(v);
Intent intent;
switch (menuActiveViewId)
{
case R.id.menu_home:
intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(this, intent);
break;
case R.id.menu_browse:
intent = new Intent(this, SelectArtistActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(this, intent);
break;
case R.id.menu_search:
intent = new Intent(this, SearchActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, true);
startActivityForResultWithoutTransition(this, intent);
break;
case R.id.menu_playlists:
intent = new Intent(this, SelectPlaylistActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(this, intent);
break;
case R.id.menu_shares:
intent = new Intent(this, ShareActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(this, intent);
break;
case R.id.menu_chat:
startActivityForResultWithoutTransition(this, ChatActivity.class);
break;
case R.id.menu_bookmarks:
startActivityForResultWithoutTransition(this, BookmarkActivity.class);
break;
case R.id.menu_now_playing:
startActivityForResultWithoutTransition(this, DownloadActivity.class);
break;
case R.id.menu_settings:
startActivityForResultWithoutTransition(this, SettingsActivity.class);
break;
case R.id.menu_about:
startActivityForResultWithoutTransition(this, HelpActivity.class);
break;
case R.id.menu_exit:
intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(Constants.INTENT_EXTRA_NAME_EXIT, true);
startActivityForResultWithoutTransition(this, intent);
break;
}
menuDrawer.closeMenu(true);
}
private final class HelpClient extends WebViewClient
{
@Override
public void onLoadResource(WebView webView, String url)
{
setProgressBarIndeterminateVisibility(true);
super.onLoadResource(webView, url);
}
@Override
public void onPageFinished(WebView view, String url)
{
setProgressBarIndeterminateVisibility(false);
String versionName = Util.getVersionName(HelpActivity.this);
String title = String.format("%s (%s)", view.getTitle(), versionName);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null)
{
actionBar.setSubtitle(title);
}
backButton.setEnabled(view.canGoBack());
forwardButton.setEnabled(view.canGoForward());
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
{
Util.toast(HelpActivity.this, description);
}
}
}

View File

@ -1,89 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Lyrics;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
/**
* Displays song lyrics.
*
* @author Sindre Mehus
*/
public final class LyricsActivity extends SubsonicTabActivity
{
@Override
protected void onCreate(Bundle bundle)
{
super.onCreate(bundle);
setContentView(R.layout.lyrics);
View nowPlayingMenuItem = findViewById(R.id.menu_now_playing);
menuDrawer.setActiveView(nowPlayingMenuItem);
load();
}
private void load()
{
BackgroundTask<Lyrics> task = new TabActivityBackgroundTask<Lyrics>(this, true)
{
@Override
protected Lyrics doInBackground() throws Throwable
{
String artist = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_ARTIST);
String title = getIntent().getStringExtra(Constants.INTENT_EXTRA_NAME_TITLE);
MusicService musicService = MusicServiceFactory.getMusicService(LyricsActivity.this);
return musicService.getLyrics(artist, title, LyricsActivity.this, this);
}
@Override
protected void done(Lyrics result)
{
TextView artistView = (TextView) findViewById(R.id.lyrics_artist);
TextView titleView = (TextView) findViewById(R.id.lyrics_title);
TextView textView = (TextView) findViewById(R.id.lyrics_text);
if (result != null && result.getArtist() != null)
{
artistView.setText(result.getArtist());
titleView.setText(result.getTitle());
textView.setText(result.getText());
}
else
{
artistView.setText(R.string.lyrics_nomatch);
}
}
};
task.execute();
}
}

View File

@ -1,382 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.data.ServerSetting;
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.MergeAdapter;
import org.moire.ultrasonic.util.Util;
import java.util.Collections;
import kotlin.Lazy;
import static java.util.Arrays.asList;
import static org.koin.android.viewmodel.compat.ViewModelCompat.viewModel;
import static org.koin.java.KoinJavaComponent.inject;
public class MainActivity extends SubsonicTabActivity
{
private static boolean infoDialogDisplayed;
private static boolean shouldUseId3;
private static String lastActiveServerProperties;
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private Lazy<ServerSettingsModel> serverSettingsModel = viewModel(this, ServerSettingsModel.class);
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// Determine first run and migrate server settings to DB as early as possible
boolean showWelcomeScreen = Util.isFirstRun(this);
boolean areServersMigrated = serverSettingsModel.getValue().migrateFromPreferences();
// If there are any servers in the DB, do not show the welcome screen
showWelcomeScreen &= !areServersMigrated;
if (getIntent().hasExtra(Constants.INTENT_EXTRA_NAME_EXIT))
{
setResult(Constants.RESULT_CLOSE_ALL);
if (getMediaPlayerController() != null)
{
getMediaPlayerController().stopJukeboxService();
}
if (getImageLoader() != null)
{
getImageLoader().stopImageLoader();
}
finish();
exit();
return;
}
setContentView(R.layout.main);
loadSettings();
final View buttons = LayoutInflater.from(this).inflate(R.layout.main_buttons, null);
final View serverButton = buttons.findViewById(R.id.main_select_server);
final TextView serverTextView = serverButton.findViewById(R.id.main_select_server_2);
final View musicTitle = buttons.findViewById(R.id.main_music);
final View artistsButton = buttons.findViewById(R.id.main_artists_button);
final View albumsButton = buttons.findViewById(R.id.main_albums_button);
final View genresButton = buttons.findViewById(R.id.main_genres_button);
final View videosTitle = buttons.findViewById(R.id.main_videos_title);
final View songsTitle = buttons.findViewById(R.id.main_songs);
final View randomSongsButton = buttons.findViewById(R.id.main_songs_button);
final View songsStarredButton = buttons.findViewById(R.id.main_songs_starred);
final View albumsTitle = buttons.findViewById(R.id.main_albums);
final View albumsNewestButton = buttons.findViewById(R.id.main_albums_newest);
final View albumsRandomButton = buttons.findViewById(R.id.main_albums_random);
final View albumsHighestButton = buttons.findViewById(R.id.main_albums_highest);
final View albumsStarredButton = buttons.findViewById(R.id.main_albums_starred);
final View albumsRecentButton = buttons.findViewById(R.id.main_albums_recent);
final View albumsFrequentButton = buttons.findViewById(R.id.main_albums_frequent);
final View albumsAlphaByNameButton = buttons.findViewById(R.id.main_albums_alphaByName);
final View albumsAlphaByArtistButton = buttons.findViewById(R.id.main_albums_alphaByArtist);
final View videosButton = buttons.findViewById(R.id.main_videos);
lastActiveServerProperties = getActiveServerProperties();
String name = activeServerProvider.getValue().getActiveServer().getName();
serverTextView.setText(name);
final ListView list = findViewById(R.id.main_list);
final MergeAdapter adapter = new MergeAdapter();
adapter.addViews(Collections.singletonList(serverButton), true);
if (!ActiveServerProvider.Companion.isOffline(this))
{
adapter.addView(musicTitle, false);
adapter.addViews(asList(artistsButton, albumsButton, genresButton), true);
adapter.addView(songsTitle, false);
adapter.addViews(asList(randomSongsButton, songsStarredButton), true);
adapter.addView(albumsTitle, false);
if (Util.getShouldUseId3Tags(MainActivity.this))
{
shouldUseId3 = true;
adapter.addViews(asList(albumsNewestButton, albumsRecentButton, albumsFrequentButton, albumsRandomButton, albumsStarredButton, albumsAlphaByNameButton, albumsAlphaByArtistButton), true);
}
else
{
shouldUseId3 = false;
adapter.addViews(asList(albumsNewestButton, albumsRecentButton, albumsFrequentButton, albumsHighestButton, albumsRandomButton, albumsStarredButton, albumsAlphaByNameButton, albumsAlphaByArtistButton), true);
}
adapter.addView(videosTitle, false);
adapter.addViews(Collections.singletonList(videosButton), true);
}
list.setAdapter(adapter);
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (view == serverButton)
{
showServers();
}
else if (view == albumsNewestButton)
{
showAlbumList("newest", R.string.main_albums_newest);
}
else if (view == albumsRandomButton)
{
showAlbumList("random", R.string.main_albums_random);
}
else if (view == albumsHighestButton)
{
showAlbumList("highest", R.string.main_albums_highest);
}
else if (view == albumsRecentButton)
{
showAlbumList("recent", R.string.main_albums_recent);
}
else if (view == albumsFrequentButton)
{
showAlbumList("frequent", R.string.main_albums_frequent);
}
else if (view == albumsStarredButton)
{
showAlbumList(Constants.STARRED, R.string.main_albums_starred);
}
else if (view == albumsAlphaByNameButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName);
}
else if (view == albumsAlphaByArtistButton)
{
showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist);
}
else if (view == songsStarredButton)
{
showStarredSongs();
}
else if (view == artistsButton)
{
showArtists();
}
else if (view == albumsButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title);
}
else if (view == randomSongsButton)
{
showRandomSongs();
}
else if (view == genresButton)
{
showGenres();
}
else if (view == videosButton)
{
showVideos();
}
}
});
final View homeMenuItem = findViewById(R.id.menu_home);
menuDrawer.setActiveView(homeMenuItem);
setActionBarTitle(R.string.common_appname);
setTitle(R.string.common_appname);
// Remember the current theme.
theme = Util.getTheme(this);
showInfoDialog(showWelcomeScreen);
}
private void loadSettings()
{
PreferenceManager.setDefaultValues(this, R.xml.settings, false);
final SharedPreferences preferences = Util.getPreferences(this);
if (!preferences.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION))
{
final SharedPreferences.Editor editor = preferences.edit();
editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory(this).getPath());
editor.apply();
}
}
@Override
protected void onResume()
{
super.onResume();
boolean shouldRestart = false;
boolean id3 = Util.getShouldUseId3Tags(MainActivity.this);
String currentActiveServerProperties = getActiveServerProperties();
if (id3 != shouldUseId3)
{
shouldUseId3 = id3;
shouldRestart = true;
}
if (!currentActiveServerProperties.equals(lastActiveServerProperties))
{
lastActiveServerProperties = currentActiveServerProperties;
shouldRestart = true;
}
if (shouldRestart) restart();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu)
{
final MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(R.menu.main, menu);
super.onCreateOptionsMenu(menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
case R.id.main_shuffle:
final Intent intent1 = new Intent(this, DownloadActivity.class);
intent1.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
startActivityForResultWithoutTransition(this, intent1);
return true;
}
return false;
}
private void exit()
{
lifecycleSupport.getValue().onDestroy();
Util.unregisterMediaButtonEventReceiver(this, false);
finish();
}
private void showInfoDialog(final boolean show)
{
if (!infoDialogDisplayed)
{
infoDialogDisplayed = true;
if (show)
{
Util.showWelcomeDialog(this, this, R.string.main_welcome_title, R.string.main_welcome_text);
}
}
}
private void showAlbumList(final String type, final int title)
{
final Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxAlbums(this));
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
startActivityForResultWithoutTransition(this, intent);
}
private void showStarredSongs()
{
final Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_STARRED, 1);
startActivityForResultWithoutTransition(this, intent);
}
private void showRandomSongs()
{
final Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_RANDOM, 1);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs(this));
startActivityForResultWithoutTransition(this, intent);
}
private void showArtists()
{
final Intent intent = new Intent(this, SelectArtistActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, getResources().getString(R.string.main_artists_title));
startActivityForResultWithoutTransition(this, intent);
}
private void showGenres()
{
final Intent intent = new Intent(this, SelectGenreActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResultWithoutTransition(this, intent);
}
private void showVideos()
{
final Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_VIDEOS, 1);
startActivityForResultWithoutTransition(this, intent);
}
private void showServers()
{
final Intent intent = new Intent(this, ServerSelectorActivity.class);
startActivityForResult(intent, 0);
}
private String getActiveServerProperties()
{
ServerSetting currentSetting = activeServerProvider.getValue().getActiveServer();
return String.format("%s;%s;%s;%s;%s;%s", currentSetting.getUrl(), currentSetting.getUserName(),
currentSetting.getPassword(), currentSetting.getAllowSelfSignedCertificate(),
currentSetting.getLdapSupport(), currentSetting.getMinimumApiVersion());
}
}

View File

@ -1,106 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.PodcastsChannel;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.view.PodcastsChannelsAdapter;
import java.util.List;
public class PodcastsActivity extends SubsonicTabActivity {
private View emptyTextView;
SubsonicTabActivity currentActivity = null;
ListView channelItemsListView = null;
Context currentContext = (Context)this;
@Override
public void onCreate(Bundle savedInstanceState)
{
this.currentActivity = this;
super.onCreate(savedInstanceState);
setContentView(R.layout.podcasts);
emptyTextView = findViewById(R.id.select_podcasts_empty);
channelItemsListView = (ListView)findViewById(R.id.podcasts_channels_items_list);
channelItemsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
PodcastsChannel pc = (PodcastsChannel) parent.getItemAtPosition(position);
if (pc == null) {
return;
}
Intent intent = new Intent(currentContext, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID, pc.getId());
startActivityForResultWithoutTransition(PodcastsActivity.this, intent);
}
});
load();
}
private void load()
{
BackgroundTask<List<PodcastsChannel>> task = new TabActivityBackgroundTask<List<PodcastsChannel>>(this, true)
{
@Override
protected List<PodcastsChannel> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(PodcastsActivity.this);
List<PodcastsChannel> channels = musicService.getPodcastsChannels(false,PodcastsActivity.this, this);
/* TODO c'est quoi ce nettoyage de cache ?
if (!Util.isOffline(PodcastsActivity.this))
new CacheCleaner(PodcastsActivity.this, getDownloadService()).cleanPlaylists(playlists);
*/
return channels;
}
@Override
protected void done(List<PodcastsChannel> result)
{
channelItemsListView.setAdapter(new PodcastsChannelsAdapter(currentActivity, result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
}

View File

@ -1,58 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.app.SearchManager;
import android.content.Intent;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import org.moire.ultrasonic.provider.SearchSuggestionProvider;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util;
/**
* Receives search queries and forwards to the SelectAlbumActivity.
*
* @author Sindre Mehus
*/
public class QueryReceiverActivity extends ResultActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
String query = getIntent().getStringExtra(SearchManager.QUERY);
if (query != null)
{
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE);
suggestions.saveRecentQuery(query, null);
Intent intent = new Intent(QueryReceiverActivity.this, SearchActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
startActivityForResultWithoutTransition(QueryReceiverActivity.this, intent);
}
finish();
Util.disablePendingTransition(this);
}
}

View File

@ -1,37 +0,0 @@
package org.moire.ultrasonic.activity;
import android.app.Activity;
import android.content.Intent;
import androidx.appcompat.app.AppCompatActivity;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util;
/**
* Created by Joshua Bahnsen on 12/30/13.
*/
public class ResultActivity extends AppCompatActivity
{
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (resultCode)
{
case Constants.RESULT_CLOSE_ALL:
setResult(Constants.RESULT_CLOSE_ALL);
finish();
}
super.onActivityResult(requestCode, resultCode, data);
}
public void startActivityForResultWithoutTransition(Activity currentActivity, Class<? extends Activity> newActivity)
{
startActivityForResultWithoutTransition(currentActivity, new Intent(currentActivity, newActivity));
}
public void startActivityForResultWithoutTransition(Activity currentActivity, Intent intent)
{
startActivityForResult(intent, 0);
Util.disablePendingTransition(currentActivity);
}
}

View File

@ -1,547 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Artist;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.domain.SearchCriteria;
import org.moire.ultrasonic.domain.SearchResult;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.MergeAdapter;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ArtistAdapter;
import org.moire.ultrasonic.view.EntryAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Performs searches and displays the matching artists, albums and songs.
*
* @author Sindre Mehus
*/
public class SearchActivity extends SubsonicTabActivity
{
private static int DEFAULT_ARTISTS;
private static int DEFAULT_ALBUMS;
private static int DEFAULT_SONGS;
private ListView list;
private View artistsHeading;
private View albumsHeading;
private View songsHeading;
private TextView searchButton;
private View moreArtistsButton;
private View moreAlbumsButton;
private View moreSongsButton;
private SearchResult searchResult;
private MergeAdapter mergeAdapter;
private ArtistAdapter artistAdapter;
private ListAdapter moreArtistsAdapter;
private EntryAdapter albumAdapter;
private ListAdapter moreAlbumsAdapter;
private ListAdapter moreSongsAdapter;
private EntryAdapter songAdapter;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.search);
setActionBarTitle(R.string.common_appname);
setActionBarSubtitle(R.string.search_title);
View searchMenuItem = findViewById(R.id.menu_search);
menuDrawer.setActiveView(searchMenuItem);
DEFAULT_ARTISTS = Util.getDefaultArtists(this);
DEFAULT_ALBUMS = Util.getDefaultAlbums(this);
DEFAULT_SONGS = Util.getDefaultSongs(this);
View buttons = LayoutInflater.from(this).inflate(R.layout.search_buttons, null);
if (buttons != null)
{
artistsHeading = buttons.findViewById(R.id.search_artists);
albumsHeading = buttons.findViewById(R.id.search_albums);
songsHeading = buttons.findViewById(R.id.search_songs);
searchButton = (TextView) buttons.findViewById(R.id.search_search);
moreArtistsButton = buttons.findViewById(R.id.search_more_artists);
moreAlbumsButton = buttons.findViewById(R.id.search_more_albums);
moreSongsButton = buttons.findViewById(R.id.search_more_songs);
}
list = (ListView) findViewById(R.id.search_list);
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (view == searchButton)
{
onSearchRequested();
}
else if (view == moreArtistsButton)
{
expandArtists();
}
else if (view == moreAlbumsButton)
{
expandAlbums();
}
else if (view == moreSongsButton)
{
expandSongs();
}
else
{
Object item = parent.getItemAtPosition(position);
if (item instanceof Artist)
{
onArtistSelected((Artist) item);
}
else if (item instanceof MusicDirectory.Entry)
{
MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
if (entry.isDirectory())
{
onAlbumSelected(entry, false);
}
else if (entry.isVideo())
{
onVideoSelected(entry);
}
else
{
onSongSelected(entry, false, true, true, false);
}
}
}
}
});
registerForContextMenu(list);
onNewIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent)
{
super.onNewIntent(intent);
String query = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_QUERY);
boolean autoPlay = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
boolean requestSearch = intent.getBooleanExtra(Constants.INTENT_EXTRA_REQUEST_SEARCH, false);
if (query != null)
{
mergeAdapter = new MergeAdapter();
list.setAdapter(mergeAdapter);
search(query, autoPlay);
}
else
{
populateList();
if (requestSearch) onSearchRequested();
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
Object selectedItem = list.getItemAtPosition(info.position);
boolean isArtist = selectedItem instanceof Artist;
boolean isAlbum = selectedItem instanceof MusicDirectory.Entry && ((MusicDirectory.Entry) selectedItem).isDirectory();
if (!isArtist && !isAlbum)
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.select_song_context, menu);
}
else
{
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.select_album_context, menu);
}
MenuItem shareButton = menu.findItem(R.id.menu_item_share);
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
if (downloadMenuItem != null)
{
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this));
}
if (ActiveServerProvider.Companion.isOffline(this) || isArtist)
{
if (shareButton != null)
{
shareButton.setVisible(false);
}
}
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return true;
}
Object selectedItem = list.getItemAtPosition(info.position);
Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null;
Entry entry = selectedItem instanceof Entry ? (Entry) selectedItem : null;
String entryId = null;
if (entry != null)
{
entryId = entry.getId();
}
String id = artist != null ? artist.getId() : entryId;
if (id == null)
{
return true;
}
List<Entry> songs = new ArrayList<Entry>(1);
switch (menuItem.getItemId())
{
case R.id.album_menu_play_now:
downloadRecursively(id, false, false, true, false, false, false, false, false);
break;
case R.id.album_menu_play_next:
downloadRecursively(id, false, true, false, true, false, true, false, false);
break;
case R.id.album_menu_play_last:
downloadRecursively(id, false, true, false, false, false, false, false, false);
break;
case R.id.album_menu_pin:
downloadRecursively(id, true, true, false, false, false, false, false, false);
break;
case R.id.album_menu_unpin:
downloadRecursively(id, false, false, false, false, false, false, true, false);
break;
case R.id.album_menu_download:
downloadRecursively(id, false, false, false, false, true, false, false, false);
break;
case R.id.song_menu_play_now:
if (entry != null)
{
songs = new ArrayList<MusicDirectory.Entry>(1);
songs.add(entry);
download(false, false, true, false, false, songs);
}
break;
case R.id.song_menu_play_next:
if (entry != null)
{
songs = new ArrayList<MusicDirectory.Entry>(1);
songs.add(entry);
download(true, false, false, true, false, songs);
}
break;
case R.id.song_menu_play_last:
if (entry != null)
{
songs = new ArrayList<MusicDirectory.Entry>(1);
songs.add(entry);
download(true, false, false, false, false, songs);
}
break;
case R.id.song_menu_pin:
if (entry != null)
{
songs.add(entry);
Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
downloadBackground(true, songs);
}
break;
case R.id.song_menu_download:
if (entry != null)
{
songs.add(entry);
Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
downloadBackground(false, songs);
}
break;
case R.id.song_menu_unpin:
if (entry != null)
{
songs.add(entry);
Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
getMediaPlayerController().unpin(songs);
}
break;
case R.id.menu_item_share:
if (entry != null)
{
songs = new ArrayList<MusicDirectory.Entry>(1);
songs.add(entry);
createShare(songs);
}
default:
return super.onContextItemSelected(menuItem);
}
return true;
}
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{
if (getMediaPlayerController() == null)
{
return;
}
Runnable onValid = new Runnable()
{
@Override
public void run()
{
warnIfNetworkOrStorageUnavailable();
getMediaPlayerController().downloadBackground(songs, save);
}
};
checkLicenseAndTrialPeriod(onValid);
}
private void search(final String query, final boolean autoplay)
{
final int maxArtists = Util.getMaxArtists(this);
final int maxAlbums = Util.getMaxAlbums(this);
final int maxSongs = Util.getMaxSongs(this);
BackgroundTask<SearchResult> task = new TabActivityBackgroundTask<SearchResult>(this, true)
{
@Override
protected SearchResult doInBackground() throws Throwable
{
SearchCriteria criteria = new SearchCriteria(query, maxArtists, maxAlbums, maxSongs);
MusicService service = MusicServiceFactory.getMusicService(SearchActivity.this);
licenseValid = service.isLicenseValid(SearchActivity.this, this);
return service.search(criteria, SearchActivity.this, this);
}
@Override
protected void done(SearchResult result)
{
searchResult = result;
populateList();
if (autoplay)
{
autoplay();
}
}
};
task.execute();
}
private void populateList()
{
mergeAdapter = new MergeAdapter();
mergeAdapter.addView(searchButton, true);
if (searchResult != null)
{
List<Artist> artists = searchResult.getArtists();
if (!artists.isEmpty())
{
mergeAdapter.addView(artistsHeading);
List<Artist> displayedArtists = new ArrayList<Artist>(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size())));
artistAdapter = new ArtistAdapter(this, displayedArtists);
mergeAdapter.addAdapter(artistAdapter);
if (artists.size() > DEFAULT_ARTISTS)
{
moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true);
}
}
List<MusicDirectory.Entry> albums = searchResult.getAlbums();
if (!albums.isEmpty())
{
mergeAdapter.addView(albumsHeading);
List<MusicDirectory.Entry> displayedAlbums = new ArrayList<MusicDirectory.Entry>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
albumAdapter = new EntryAdapter(this, getImageLoader(), displayedAlbums, false);
mergeAdapter.addAdapter(albumAdapter);
if (albums.size() > DEFAULT_ALBUMS)
{
moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true);
}
}
List<MusicDirectory.Entry> songs = searchResult.getSongs();
if (!songs.isEmpty())
{
mergeAdapter.addView(songsHeading);
List<MusicDirectory.Entry> displayedSongs = new ArrayList<MusicDirectory.Entry>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
songAdapter = new EntryAdapter(this, getImageLoader(), displayedSongs, false);
mergeAdapter.addAdapter(songAdapter);
if (songs.size() > DEFAULT_SONGS)
{
moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true);
}
}
boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty();
searchButton.setText(empty ? R.string.search_no_match : R.string.search_search);
}
list.setAdapter(mergeAdapter);
}
private void expandArtists()
{
artistAdapter.clear();
for (Artist artist : searchResult.getArtists())
{
artistAdapter.add(artist);
}
artistAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreArtistsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void expandAlbums()
{
albumAdapter.clear();
for (MusicDirectory.Entry album : searchResult.getAlbums())
{
albumAdapter.add(album);
}
albumAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreAlbumsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void expandSongs()
{
songAdapter.clear();
for (MusicDirectory.Entry song : searchResult.getSongs())
{
songAdapter.add(song);
}
songAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreSongsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void onArtistSelected(Artist artist)
{
Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.getName());
startActivityForResultWithoutTransition(this, intent);
}
private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay)
{
Intent intent = new Intent(SearchActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, album.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle());
intent.putExtra(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory());
intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay);
startActivityForResultWithoutTransition(SearchActivity.this, intent);
}
private void onSongSelected(MusicDirectory.Entry song, boolean save, boolean append, boolean autoplay, boolean playNext)
{
MediaPlayerController mediaPlayerController = getMediaPlayerController();
if (mediaPlayerController != null)
{
if (!append && !playNext)
{
mediaPlayerController.clear();
}
mediaPlayerController.download(Collections.singletonList(song), save, false, playNext, false, false);
if (autoplay)
{
mediaPlayerController.play(mediaPlayerController.getPlaylistSize() - 1);
}
Util.toast(SearchActivity.this, getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));
}
}
private void onVideoSelected(MusicDirectory.Entry entry)
{
playVideo(entry);
}
private void autoplay()
{
if (!searchResult.getSongs().isEmpty())
{
onSongSelected(searchResult.getSongs().get(0), false, false, true, false);
}
else if (!searchResult.getAlbums().isEmpty())
{
onAlbumSelected(searchResult.getAlbums().get(0), true);
}
}
}

View File

@ -1,191 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import timber.log.Timber;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Genre;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.GenreAdapter;
import java.util.ArrayList;
import java.util.List;
public class SelectGenreActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener
{
private SwipeRefreshLayout refreshGenreListView;
private ListView genreListView;
private View emptyView;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.select_genre);
refreshGenreListView = findViewById(R.id.select_genre_refresh);
genreListView = findViewById(R.id.select_genre_list);
refreshGenreListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
genreListView.setOnItemClickListener(this);
emptyView = findViewById(R.id.select_genre_empty);
registerForContextMenu(genreListView);
View browseMenuItem = findViewById(R.id.menu_browse);
menuDrawer.setActiveView(browseMenuItem);
setActionBarSubtitle(R.string.main_genres_title);
load();
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
return true;
}
private void refresh()
{
finish();
Intent intent = getIntent();
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
}
private void load()
{
BackgroundTask<List<Genre>> task = new TabActivityBackgroundTask<List<Genre>>(this, true)
{
@Override
protected List<Genre> doInBackground() throws Throwable
{
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
MusicService musicService = MusicServiceFactory.getMusicService(SelectGenreActivity.this);
List<Genre> genres = new ArrayList<Genre>();
try
{
genres = musicService.getGenres(refresh, SelectGenreActivity.this, this);
}
catch (Exception x)
{
Timber.e(x, "Failed to load genres");
}
return genres;
}
@Override
protected void done(List<Genre> result)
{
emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE);
if (result != null)
{
genreListView.setAdapter(new GenreAdapter(SelectGenreActivity.this, result));
}
}
};
task.execute();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
Genre genre = (Genre) parent.getItemAtPosition(position);
if (genre != null)
{
Intent intent = new Intent(this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_GENRE_NAME, genre.getName());
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs(this));
intent.putExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
startActivityForResultWithoutTransition(this, intent);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
case R.id.main_shuffle:
Intent intent = new Intent(this, DownloadActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
startActivityForResultWithoutTransition(this, intent);
return true;
}
return false;
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -1,391 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Playlist;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.service.OfflineException;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CacheCleaner;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.LoadingTask;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.PlaylistAdapter;
import java.util.List;
public class SelectPlaylistActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener
{
private SwipeRefreshLayout refreshPlaylistsListView;
private ListView playlistsListView;
private View emptyTextView;
private PlaylistAdapter playlistAdapter;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.select_playlist);
refreshPlaylistsListView = findViewById(R.id.select_playlist_refresh);
playlistsListView = findViewById(R.id.select_playlist_list);
refreshPlaylistsListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
emptyTextView = findViewById(R.id.select_playlist_empty);
playlistsListView.setOnItemClickListener(this);
registerForContextMenu(playlistsListView);
View playlistsMenuItem = findViewById(R.id.menu_playlists);
menuDrawer.setActiveView(playlistsMenuItem);
setActionBarTitle(R.string.common_appname);
setActionBarSubtitle(R.string.playlist_label);
load();
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
return true;
}
private void refresh()
{
finish();
Intent intent = new Intent(this, SelectPlaylistActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
}
private void load()
{
BackgroundTask<List<Playlist>> task = new TabActivityBackgroundTask<List<Playlist>>(this, true)
{
@Override
protected List<Playlist> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
List<Playlist> playlists = musicService.getPlaylists(refresh, SelectPlaylistActivity.this, this);
if (!ActiveServerProvider.Companion.isOffline(SelectPlaylistActivity.this))
new CacheCleaner(SelectPlaylistActivity.this).cleanPlaylists(playlists);
return playlists;
}
@Override
protected void done(List<Playlist> result)
{
playlistsListView.setAdapter(playlistAdapter = new PlaylistAdapter(SelectPlaylistActivity.this, result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
MenuInflater inflater = getMenuInflater();
if (ActiveServerProvider.Companion.isOffline(this)) inflater.inflate(R.menu.select_playlist_context_offline, menu);
else inflater.inflate(R.menu.select_playlist_context, menu);
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
if (downloadMenuItem != null)
{
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(this));
}
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return false;
}
Playlist playlist = (Playlist) playlistsListView.getItemAtPosition(info.position);
if (playlist == null)
{
return false;
}
Intent intent;
switch (menuItem.getItemId())
{
case R.id.playlist_menu_pin:
downloadPlaylist(playlist.getId(), playlist.getName(), true, true, false, false, true, false, false);
break;
case R.id.playlist_menu_unpin:
downloadPlaylist(playlist.getId(), playlist.getName(), false, false, false, false, true, false, true);
break;
case R.id.playlist_menu_download:
downloadPlaylist(playlist.getId(), playlist.getName(), false, false, false, false, true, false, false);
break;
case R.id.playlist_menu_play_now:
intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
startActivityForResultWithoutTransition(SelectPlaylistActivity.this, intent);
break;
case R.id.playlist_menu_play_shuffled:
intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
startActivityForResultWithoutTransition(SelectPlaylistActivity.this, intent);
break;
case R.id.playlist_menu_delete:
deletePlaylist(playlist);
break;
case R.id.playlist_info:
displayPlaylistInfo(playlist);
break;
case R.id.playlist_update_info:
updatePlaylistInfo(playlist);
break;
default:
return super.onContextItemSelected(menuItem);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
}
return false;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
Playlist playlist = (Playlist) parent.getItemAtPosition(position);
if (playlist == null)
{
return;
}
Intent intent = new Intent(SelectPlaylistActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, playlist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
startActivityForResultWithoutTransition(SelectPlaylistActivity.this, intent);
}
private void deletePlaylist(final Playlist playlist)
{
new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_alert).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, playlist.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(SelectPlaylistActivity.this, false)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
musicService.deletePlaylist(playlist.getId(), SelectPlaylistActivity.this, null);
return null;
}
@Override
protected void done(Void result)
{
playlistAdapter.remove(playlist);
playlistAdapter.notifyDataSetChanged();
Util.toast(SelectPlaylistActivity.this, getResources().getString(R.string.menu_deleted_playlist, playlist.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()), getErrorMessage(error));
Util.toast(SelectPlaylistActivity.this, msg, false);
}
}.execute();
}
}).setNegativeButton(R.string.common_cancel, null).show();
}
private void displayPlaylistInfo(final Playlist playlist)
{
final TextView textView = new TextView(this);
textView.setPadding(5, 5, 5, 5);
final Spannable message = new SpannableString("Owner: " + playlist.getOwner() + "\nComments: " +
((playlist.getComment() == null) ? "" : playlist.getComment()) +
"\nSong Count: " + playlist.getSongCount() +
((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic()) + ((playlist.getCreated() == null) ? "" : ("\nCreation Date: " + playlist.getCreated().replace('T', ' ')))));
Linkify.addLinks(message, Linkify.WEB_URLS);
textView.setText(message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(this).setTitle(playlist.getName()).setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setView(textView).show();
}
private void updatePlaylistInfo(final Playlist playlist)
{
View dialogView = getLayoutInflater().inflate(R.layout.update_playlist, null);
if (dialogView == null)
{
return;
}
final EditText nameBox = (EditText) dialogView.findViewById(R.id.get_playlist_name);
final EditText commentBox = (EditText) dialogView.findViewById(R.id.get_playlist_comment);
final CheckBox publicBox = (CheckBox) dialogView.findViewById(R.id.get_playlist_public);
nameBox.setText(playlist.getName());
commentBox.setText(playlist.getComment());
Boolean pub = playlist.getPublic();
if (pub == null)
{
publicBox.setEnabled(false);
}
else
{
publicBox.setChecked(pub);
}
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setIcon(android.R.drawable.ic_dialog_alert);
alertDialog.setTitle(R.string.playlist_update_info);
alertDialog.setView(dialogView);
alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(SelectPlaylistActivity.this, false)
{
@Override
protected Void doInBackground() throws Throwable
{
Editable nameBoxText = nameBox.getText();
Editable commentBoxText = commentBox.getText();
String name = nameBoxText != null ? nameBoxText.toString() : null;
String comment = commentBoxText != null ? commentBoxText.toString() : null;
MusicService musicService = MusicServiceFactory.getMusicService(SelectPlaylistActivity.this);
musicService.updatePlaylist(playlist.getId(), name, comment, publicBox.isChecked(), SelectPlaylistActivity.this, null);
return null;
}
@Override
protected void done(Void result)
{
refresh();
Util.toast(SelectPlaylistActivity.this, getResources().getString(R.string.playlist_updated_info, playlist.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, playlist.getName()), getErrorMessage(error));
Util.toast(SelectPlaylistActivity.this, msg, false);
}
}.execute();
}
});
alertDialog.setNegativeButton(R.string.common_cancel, null);
alertDialog.show();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -1,43 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.fragment.SettingsFragment;
public class SettingsActivity extends SubsonicTabActivity {
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new SettingsFragment())
.commit();
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null)
{
actionBar.setSubtitle(R.string.menu_settings);
}
}
}

View File

@ -1,385 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.service.OfflineException;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.LoadingTask;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.TimeSpan;
import org.moire.ultrasonic.util.TimeSpanPicker;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ShareAdapter;
import java.util.List;
public class ShareActivity extends SubsonicTabActivity implements AdapterView.OnItemClickListener
{
private SwipeRefreshLayout refreshSharesListView;
private ListView sharesListView;
private View emptyTextView;
private ShareAdapter shareAdapter;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.select_share);
refreshSharesListView = findViewById(R.id.select_share_refresh);
sharesListView = findViewById(R.id.select_share_list);
refreshSharesListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
new GetDataTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
emptyTextView = findViewById(R.id.select_share_empty);
sharesListView.setOnItemClickListener(this);
registerForContextMenu(sharesListView);
View sharesMenuItem = findViewById(R.id.menu_shares);
menuDrawer.setActiveView(sharesMenuItem);
setActionBarTitle(R.string.common_appname);
setActionBarSubtitle(R.string.button_bar_shares);
load();
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
return true;
}
private void refresh()
{
finish();
Intent intent = new Intent(this, ShareActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_REFRESH, true);
startActivityForResultWithoutTransition(this, intent);
}
private void load()
{
BackgroundTask<List<Share>> task = new TabActivityBackgroundTask<List<Share>>(this, true)
{
@Override
protected List<Share> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(ShareActivity.this);
boolean refresh = getIntent().getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false);
return musicService.getShares(refresh, ShareActivity.this, this);
}
@Override
protected void done(List<Share> result)
{
sharesListView.setAdapter(shareAdapter = new ShareAdapter(ShareActivity.this, result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.select_share_context, menu);
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return false;
}
Share share = (Share) sharesListView.getItemAtPosition(info.position);
if (share == null)
{
return false;
}
switch (menuItem.getItemId())
{
case R.id.share_menu_pin:
downloadShare(share.getId(), share.getName(), true, true, false, false, true, false, false);
break;
case R.id.share_menu_unpin:
downloadShare(share.getId(), share.getName(), false, false, false, false, true, false, true);
break;
case R.id.share_menu_download:
downloadShare(share.getId(), share.getName(), false, false, false, false, true, false, false);
break;
case R.id.share_menu_play_now:
downloadShare(share.getId(), share.getName(), false, false, true, false, false, false, false);
break;
case R.id.share_menu_play_shuffled:
downloadShare(share.getId(), share.getName(), false, false, true, true, false, false, false);
break;
case R.id.share_menu_delete:
deleteShare(share);
break;
case R.id.share_info:
displayShareInfo(share);
break;
case R.id.share_update_info:
updateShareInfo(share);
break;
default:
return super.onContextItemSelected(menuItem);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case android.R.id.home:
menuDrawer.toggleMenu();
return true;
}
return false;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
Share share = (Share) parent.getItemAtPosition(position);
if (share == null)
{
return;
}
Intent intent = new Intent(ShareActivity.this, SelectAlbumActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHARE_ID, share.getId());
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHARE_NAME, share.getName());
startActivityForResultWithoutTransition(ShareActivity.this, intent);
}
private void deleteShare(final Share share)
{
new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_alert).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, share.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(ShareActivity.this, false)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(ShareActivity.this);
musicService.deleteShare(share.getId(), ShareActivity.this, null);
return null;
}
@Override
protected void done(Void result)
{
shareAdapter.remove(share);
shareAdapter.notifyDataSetChanged();
Util.toast(ShareActivity.this, getResources().getString(R.string.menu_deleted_share, share.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_share_error, share.getName()), getErrorMessage(error));
Util.toast(ShareActivity.this, msg, false);
}
}.execute();
}
}).setNegativeButton(R.string.common_cancel, null).show();
}
private void displayShareInfo(final Share share)
{
final TextView textView = new TextView(this);
textView.setPadding(5, 5, 5, 5);
final Spannable message = new SpannableString("Owner: " + share.getUsername() +
"\nComments: " + ((share.getDescription() == null) ? "" : share.getDescription()) +
"\nURL: " + share.getUrl() +
"\nEntry Count: " + share.getEntries().size() +
"\nVisit Count: " + share.getVisitCount() +
((share.getCreated() == null) ? "" : ("\nCreation Date: " + share.getCreated().replace('T', ' '))) +
((share.getLastVisited() == null) ? "" : ("\nLast Visited Date: " + share.getLastVisited().replace('T', ' '))) +
((share.getExpires() == null) ? "" : ("\nExpiration Date: " + share.getExpires().replace('T', ' '))));
Linkify.addLinks(message, Linkify.WEB_URLS);
textView.setText(message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(this).setTitle("Share Details").setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setView(textView).show();
}
private void updateShareInfo(final Share share)
{
View dialogView = getLayoutInflater().inflate(R.layout.share_details, null);
if (dialogView == null)
{
return;
}
final EditText shareDescription = (EditText) dialogView.findViewById(R.id.share_description);
final TimeSpanPicker timeSpanPicker = (TimeSpanPicker) dialogView.findViewById(R.id.date_picker);
shareDescription.setText(share.getDescription());
CheckBox hideDialogCheckBox = (CheckBox) dialogView.findViewById(R.id.hide_dialog);
CheckBox saveAsDefaultsCheckBox = (CheckBox) dialogView.findViewById(R.id.save_as_defaults);
CheckBox noExpirationCheckBox = (CheckBox) dialogView.findViewById(R.id.timeSpanDisableCheckBox);
noExpirationCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
{
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b)
{
timeSpanPicker.setEnabled(!b);
}
});
noExpirationCheckBox.setChecked(true);
timeSpanPicker.setTimeSpanDisableText(getResources().getText(R.string.no_expiration));
hideDialogCheckBox.setVisibility(View.GONE);
saveAsDefaultsCheckBox.setVisibility(View.GONE);
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setIcon(android.R.drawable.ic_dialog_alert);
alertDialog.setTitle(R.string.playlist_update_info);
alertDialog.setView(dialogView);
alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(ShareActivity.this, false)
{
@Override
protected Void doInBackground() throws Throwable
{
long millis = timeSpanPicker.getTimeSpan().getTotalMilliseconds();
if (millis > 0)
{
millis = TimeSpan.getCurrentTime().add(millis).getTotalMilliseconds();
}
Editable shareDescriptionText = shareDescription.getText();
String description = shareDescriptionText != null ? shareDescriptionText.toString() : null;
MusicService musicService = MusicServiceFactory.getMusicService(ShareActivity.this);
musicService.updateShare(share.getId(), description, millis, ShareActivity.this, null);
return null;
}
@Override
protected void done(Void result)
{
refresh();
Util.toast(ShareActivity.this, getResources().getString(R.string.playlist_updated_info, share.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, share.getName()), getErrorMessage(error));
Util.toast(ShareActivity.this, msg, false);
}
}.execute();
}
});
alertDialog.setNegativeButton(R.string.common_cancel, null);
alertDialog.show();
}
private class GetDataTask extends AsyncTask<Void, Void, String[]>
{
@Override
protected void onPostExecute(String[] result)
{
super.onPostExecute(result);
}
@Override
protected String[] doInBackground(Void... params)
{
refresh();
return null;
}
}
}

View File

@ -1,61 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.activity;
import android.app.SearchManager;
import android.content.Intent;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import org.moire.ultrasonic.provider.SearchSuggestionProvider;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util;
/**
* Receives voice search queries and forwards to the SearchActivity.
* <p/>
* http://android-developers.blogspot.com/2010/09/supporting-new-music-voice-action.html
*
* @author Sindre Mehus
*/
public class VoiceQueryReceiverActivity extends ResultActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
String query = getIntent().getStringExtra(SearchManager.QUERY);
if (query != null)
{
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE);
suggestions.saveRecentQuery(query, null);
Intent intent = new Intent(VoiceQueryReceiverActivity.this, SearchActivity.class);
intent.putExtra(Constants.INTENT_EXTRA_NAME_QUERY, query);
intent.putExtra(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
startActivityForResultWithoutTransition(VoiceQueryReceiverActivity.this, intent);
}
finish();
Util.disablePendingTransition(this);
}
}

View File

@ -40,7 +40,7 @@ import java.io.Serializable;
public class EqualizerController
{
private static Boolean available = null;
private static MutableLiveData<EqualizerController> instance = new MutableLiveData<>();
private static final MutableLiveData<EqualizerController> instance = new MutableLiveData<>();
private Context context;
public Equalizer equalizer;

View File

@ -36,7 +36,7 @@ public class VisualizerController
{
private static final int PREFERRED_CAPTURE_SIZE = 128; // Must be a power of two.
private static Boolean available = null;
private static MutableLiveData<VisualizerController> instance = new MutableLiveData<>();
private static final MutableLiveData<VisualizerController> instance = new MutableLiveData<>();
public Visualizer visualizer;
private int audioSessionId;

View File

@ -0,0 +1,149 @@
package org.moire.ultrasonic.fragment;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.util.Util;
/**
* Displays online help and about information in a webWiew
*/
public class AboutFragment extends Fragment {
private WebView webView;
private ImageView backButton;
private ImageView forwardButton;
private SwipeRefreshLayout swipeRefresh;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.help, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
swipeRefresh = view.findViewById(R.id.help_refresh);
swipeRefresh.setEnabled(false);
webView = view.findViewById(R.id.help_contents);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new HelpClient());
if (savedInstanceState != null)
{
webView.restoreState(savedInstanceState);
}
else
{
webView.loadUrl(getResources().getString(R.string.help_url));
}
backButton = view.findViewById(R.id.help_back);
backButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.goBack();
}
});
ImageView stopButton = view.findViewById(R.id.help_stop);
stopButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.stopLoading();
swipeRefresh.setRefreshing(false);
}
});
forwardButton = view.findViewById(R.id.help_forward);
forwardButton.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View view)
{
webView.goForward();
}
});
// TODO: Nicer Back key handling?
webView.setFocusableInTouchMode(true);
webView.requestFocus();
webView.setOnKeyListener( new View.OnKeyListener()
{
@Override
public boolean onKey( View v, int keyCode, KeyEvent event )
{
if (keyCode == KeyEvent.KEYCODE_BACK)
{
if (webView.canGoBack())
{
webView.goBack();
return true;
}
}
return false;
}
} );
}
@Override
public void onSaveInstanceState(@NotNull Bundle state)
{
webView.saveState(state);
super.onSaveInstanceState(state);
}
private final class HelpClient extends WebViewClient
{
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
swipeRefresh.setRefreshing(true);
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, String url)
{
swipeRefresh.setRefreshing(false);
String versionName = Util.getVersionName(getContext());
String title = String.format("%s (%s)", view.getTitle(), versionName);
FragmentTitle.Companion.setTitle(AboutFragment.this, title);
backButton.setEnabled(view.canGoBack());
forwardButton.setEnabled(view.canGoForward());
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
{
Util.toast(getContext(), description);
}
}
}

View File

@ -0,0 +1,436 @@
package org.moire.ultrasonic.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker;
import org.moire.ultrasonic.subsonic.VideoPlayer;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Pair;
import org.moire.ultrasonic.util.FragmentBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.EntryAdapter;
import java.util.ArrayList;
import java.util.List;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Lists the Bookmarks available on the server
*/
public class BookmarksFragment extends Fragment {
private SwipeRefreshLayout refreshAlbumListView;
private ListView albumListView;
private View albumButtons;
private View emptyView;
private ImageView playNowButton;
private ImageView pinButton;
private ImageView unpinButton;
private ImageView downloadButton;
private ImageView deleteButton;
private final Lazy<VideoPlayer> videoPlayer = inject(VideoPlayer.class);
private final Lazy<MediaPlayerController> mediaPlayerController = inject(MediaPlayerController.class);
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
private final Lazy<NetworkAndStorageChecker> networkAndStorageChecker = inject(NetworkAndStorageChecker.class);
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.select_album, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
cancellationToken = new CancellationToken();
albumButtons = view.findViewById(R.id.menu_album);
super.onViewCreated(view, savedInstanceState);
refreshAlbumListView = view.findViewById(R.id.select_album_entries_refresh);
albumListView = view.findViewById(R.id.select_album_entries_list);
refreshAlbumListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
enableButtons();
getBookmarks();
}
});
albumListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
albumListView.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (position >= 0)
{
MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position);
if (entry != null)
{
if (entry.isVideo())
{
videoPlayer.getValue().playVideo(entry);
}
else
{
enableButtons();
}
}
}
}
});
ImageView selectButton = view.findViewById(R.id.select_album_select);
playNowButton = view.findViewById(R.id.select_album_play_now);
ImageView playNextButton = view.findViewById(R.id.select_album_play_next);
ImageView playLastButton = view.findViewById(R.id.select_album_play_last);
pinButton = view.findViewById(R.id.select_album_pin);
unpinButton = view.findViewById(R.id.select_album_unpin);
downloadButton = view.findViewById(R.id.select_album_download);
deleteButton = view.findViewById(R.id.select_album_delete);
ImageView oreButton = view.findViewById(R.id.select_album_more);
emptyView = view.findViewById(R.id.select_album_empty);
selectButton.setVisibility(View.GONE);
playNextButton.setVisibility(View.GONE);
playLastButton.setVisibility(View.GONE);
oreButton.setVisibility(View.GONE);
playNowButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
playNow(getSelectedSongs(albumListView));
}
});
selectButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
selectAllOrNone();
}
});
pinButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
downloadBackground(true);
selectAll(false, false);
}
});
unpinButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
unpin();
selectAll(false, false);
}
});
downloadButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
downloadBackground(false);
selectAll(false, false);
}
});
deleteButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
delete();
selectAll(false, false);
}
});
registerForContextMenu(albumListView);
FragmentTitle.Companion.setTitle(this, R.string.button_bar_bookmarks);
enableButtons();
getBookmarks();
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void getBookmarks()
{
new LoadTask()
{
@Override
protected MusicDirectory load(MusicService service) throws Exception
{
return Util.getSongsFromBookmarks(service.getBookmarks(getContext()));
}
}.execute();
}
private void playNow(List<MusicDirectory.Entry> songs)
{
if (!getSelectedSongs(albumListView).isEmpty())
{
int position = songs.get(0).getBookmarkPosition();
mediaPlayerController.getValue().restore(songs, 0, position, true, true);
selectAll(false, false);
}
}
private static List<MusicDirectory.Entry> getSelectedSongs(ListView albumListView)
{
List<MusicDirectory.Entry> songs = new ArrayList<>(10);
if (albumListView != null)
{
int count = albumListView.getCount();
for (int i = 0; i < count; i++)
{
if (albumListView.isItemChecked(i))
{
songs.add((MusicDirectory.Entry) albumListView.getItemAtPosition(i));
}
}
}
return songs;
}
private void selectAllOrNone()
{
boolean someUnselected = false;
int count = albumListView.getCount();
for (int i = 0; i < count; i++)
{
if (!albumListView.isItemChecked(i) && albumListView.getItemAtPosition(i) instanceof MusicDirectory.Entry)
{
someUnselected = true;
break;
}
}
selectAll(someUnselected, true);
}
private void selectAll(boolean selected, boolean toast)
{
int count = albumListView.getCount();
int selectedCount = 0;
for (int i = 0; i < count; i++)
{
MusicDirectory.Entry entry = (MusicDirectory.Entry) albumListView.getItemAtPosition(i);
if (entry != null && !entry.isDirectory() && !entry.isVideo())
{
albumListView.setItemChecked(i, selected);
selectedCount++;
}
}
// Display toast: N tracks selected / N tracks unselected
if (toast)
{
int toastResId = selected ? R.string.select_album_n_selected : R.string.select_album_n_unselected;
Util.toast(getContext(), getString(toastResId, selectedCount));
}
enableButtons();
}
private void enableButtons()
{
List<MusicDirectory.Entry> selection = getSelectedSongs(albumListView);
boolean enabled = !selection.isEmpty();
boolean unpinEnabled = false;
boolean deleteEnabled = false;
int pinnedCount = 0;
for (MusicDirectory.Entry song : selection)
{
DownloadFile downloadFile = mediaPlayerController.getValue().getDownloadFileForSong(song);
if (downloadFile.isWorkDone())
{
deleteEnabled = true;
}
if (downloadFile.isSaved())
{
pinnedCount++;
unpinEnabled = true;
}
}
playNowButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE);
pinButton.setVisibility((enabled && !ActiveServerProvider.Companion.isOffline(getContext()) && selection.size() > pinnedCount) ? View.VISIBLE : View.GONE);
unpinButton.setVisibility(enabled && unpinEnabled ? View.VISIBLE : View.GONE);
downloadButton.setVisibility(enabled && !deleteEnabled && !ActiveServerProvider.Companion.isOffline(getContext()) ? View.VISIBLE : View.GONE);
deleteButton.setVisibility(enabled && deleteEnabled ? View.VISIBLE : View.GONE);
}
private void downloadBackground(final boolean save)
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
if (songs.isEmpty())
{
selectAll(true, false);
songs = getSelectedSongs(albumListView);
}
downloadBackground(save, songs);
}
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{
Runnable onValid = new Runnable()
{
@Override
public void run()
{
networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable();
mediaPlayerController.getValue().downloadBackground(songs, save);
if (save)
{
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
}
else
{
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
}
}
};
onValid.run();
}
private void delete()
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
if (songs.isEmpty())
{
selectAll(true, false);
songs = getSelectedSongs(albumListView);
}
mediaPlayerController.getValue().delete(songs);
}
private void unpin()
{
List<MusicDirectory.Entry> songs = getSelectedSongs(albumListView);
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
mediaPlayerController.getValue().unpin(songs);
}
private abstract class LoadTask extends FragmentBackgroundTask<Pair<MusicDirectory, Boolean>>
{
public LoadTask()
{
super(BookmarksFragment.this.getActivity(), true, refreshAlbumListView, cancellationToken);
}
protected abstract MusicDirectory load(MusicService service) throws Exception;
@Override
protected Pair<MusicDirectory, Boolean> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
MusicDirectory dir = load(musicService);
boolean valid = musicService.isLicenseValid(getContext());
return new Pair<>(dir, valid);
}
@Override
protected void done(Pair<MusicDirectory, Boolean> result)
{
MusicDirectory musicDirectory = result.getFirst();
List<MusicDirectory.Entry> entries = musicDirectory.getChildren();
int songCount = 0;
for (MusicDirectory.Entry entry : entries)
{
if (!entry.isDirectory())
{
songCount++;
}
}
final int listSize = getArguments() == null? 0 : getArguments().getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0);
if (songCount > 0)
{
pinButton.setVisibility(View.VISIBLE);
unpinButton.setVisibility(View.VISIBLE);
downloadButton.setVisibility(View.VISIBLE);
deleteButton.setVisibility(View.VISIBLE);
playNowButton.setVisibility(View.VISIBLE);
}
else
{
pinButton.setVisibility(View.GONE);
unpinButton.setVisibility(View.GONE);
downloadButton.setVisibility(View.GONE);
deleteButton.setVisibility(View.GONE);
playNowButton.setVisibility(View.GONE);
if (listSize == 0 || result.getFirst().getChildren().size() < listSize)
{
albumButtons.setVisibility(View.GONE);
}
}
enableButtons();
emptyView.setVisibility(entries.isEmpty() ? View.VISIBLE : View.GONE);
albumListView.setAdapter(new EntryAdapter(getContext(), imageLoader.getValue().getImageLoader(), entries, true));
}
}
}

View File

@ -0,0 +1,317 @@
package org.moire.ultrasonic.fragment;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.ChatMessage;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.FragmentBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ChatAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Provides online chat functionality
*/
public class ChatFragment extends Fragment {
private ListView chatListView;
private EditText messageEditText;
private ImageButton sendButton;
private Timer timer;
private volatile static Long lastChatMessageTime = (long) 0;
private static final ArrayList<ChatMessage> messageList = new ArrayList<>();
private CancellationToken cancellationToken;
private SwipeRefreshLayout swipeRefresh;
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.chat, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
swipeRefresh = view.findViewById(R.id.chat_refresh);
swipeRefresh.setEnabled(false);
cancellationToken = new CancellationToken();
messageEditText = view.findViewById(R.id.chat_edittext);
sendButton = view.findViewById(R.id.chat_send);
sendButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
sendMessage();
}
});
chatListView = view.findViewById(R.id.chat_entries_list);
chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
chatListView.setStackFromBottom(true);
String serverName = activeServerProvider.getValue().getActiveServer().getName();
String userName = activeServerProvider.getValue().getActiveServer().getUserName();
String title = String.format("%s [%s@%s]", getResources().getString(R.string.button_bar_chat), userName, serverName);
FragmentTitle.Companion.setTitle(this, title);
setHasOptionsMenu(true);
messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER);
messageEditText.addTextChangedListener(new TextWatcher()
{
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
{
}
@Override
public void afterTextChanged(Editable editable)
{
sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString()));
}
});
messageEditText.setOnEditorActionListener(new TextView.OnEditorActionListener()
{
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event)
{
if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN))
{
sendMessage();
return true;
}
return false;
}
});
load();
timerMethod();
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.chat, menu);
super.onCreateOptionsMenu(menu, inflater);
}
/*
* Listen for option item selections so that we receive a notification
* when the user requests a refresh by selecting the refresh action bar item.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Check if user triggered a refresh:
if (item.getItemId() == R.id.menu_refresh) {
// Start the refresh background task.
load();
return true;
}
// User didn't trigger a refresh, let the superclass handle this action
return super.onOptionsItemSelected(item);
}
@Override
public void onResume()
{
super.onResume();
if (!messageList.isEmpty())
{
ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList);
chatListView.setAdapter(chatAdapter);
}
if (timer == null)
{
timerMethod();
}
}
@Override
public void onPause()
{
super.onPause();
if (timer != null)
{
timer.cancel();
timer = null;
}
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void timerMethod()
{
int refreshInterval = Util.getChatRefreshInterval(getContext());
if (refreshInterval > 0)
{
timer = new Timer();
timer.schedule(new TimerTask()
{
@Override
public void run()
{
getActivity().runOnUiThread(new Runnable()
{
@Override
public void run()
{
load();
}
});
}
}, refreshInterval, refreshInterval);
}
}
private void sendMessage()
{
if (messageEditText != null)
{
final String message;
Editable text = messageEditText.getText();
if (text == null)
{
return;
}
message = text.toString();
if (!Util.isNullOrWhiteSpace(message))
{
messageEditText.setText("");
BackgroundTask<Void> task = new FragmentBackgroundTask<Void>(getActivity(), false, swipeRefresh, cancellationToken)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
musicService.addChatMessage(message, getContext());
return null;
}
@Override
protected void done(Void result)
{
load();
}
};
task.execute();
}
}
}
private synchronized void load()
{
BackgroundTask<List<ChatMessage>> task = new FragmentBackgroundTask<List<ChatMessage>>(getActivity(), false, swipeRefresh, cancellationToken)
{
@Override
protected List<ChatMessage> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
return musicService.getChatMessages(lastChatMessageTime, getContext());
}
@Override
protected void done(List<ChatMessage> result)
{
if (result != null && !result.isEmpty())
{
// Reset lastChatMessageTime if we have a newer message
for (ChatMessage message : result)
{
if (message.getTime() > lastChatMessageTime)
{
lastChatMessageTime = message.getTime();
}
}
// Reverse results to show them on the bottom
Collections.reverse(result);
messageList.addAll(result);
ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList);
chatListView.setAdapter(chatAdapter);
}
}
@Override
protected void error(Throwable error) {
// Stop the timer in case of an error, otherwise it may repeat the error message forever
if (timer != null)
{
timer.cancel();
timer = null;
}
super.error(error);
}
};
task.execute();
}
}

View File

@ -0,0 +1,275 @@
package org.moire.ultrasonic.fragment;
import android.media.audiofx.Equalizer;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.audiofx.EqualizerController;
import org.moire.ultrasonic.util.Util;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import timber.log.Timber;
/**
* Displays the Equalizer
*/
public class EqualizerFragment extends Fragment {
private static final int MENU_GROUP_PRESET = 100;
private final Map<Short, SeekBar> bars = new HashMap<>();
private EqualizerController equalizerController;
private Equalizer equalizer;
private LinearLayout equalizerLayout;
private View presetButton;
private CheckBox enabledCheckBox;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.equalizer, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
FragmentTitle.Companion.setTitle(this, R.string.equalizer_label);
equalizerLayout = view.findViewById(R.id.equalizer_layout);
presetButton = view.findViewById(R.id.equalizer_preset);
enabledCheckBox = view.findViewById(R.id.equalizer_enabled);
EqualizerController.get().observe(getViewLifecycleOwner(), new Observer<EqualizerController>() {
@Override
public void onChanged(EqualizerController controller) {
if (controller != null) {
Timber.d("EqualizerController Observer.onChanged received controller");
equalizerController = controller;
equalizer = controller.equalizer;
setup();
} else {
Timber.d("EqualizerController Observer.onChanged has no controller");
equalizerController = null;
equalizer = null;
}
}
});
}
@Override
public void onPause()
{
super.onPause();
if (equalizerController == null) return;
equalizerController.saveSettings();
}
@Override
public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
if (equalizer == null) return;
short currentPreset;
try
{
currentPreset = equalizer.getCurrentPreset();
}
catch (Exception x)
{
currentPreset = -1;
}
for (short preset = 0; preset < equalizer.getNumberOfPresets(); preset++)
{
MenuItem menuItem = menu.add(MENU_GROUP_PRESET, preset, preset, equalizer.getPresetName(preset));
if (preset == currentPreset)
{
menuItem.setChecked(true);
}
}
menu.setGroupCheckable(MENU_GROUP_PRESET, true, true);
}
@Override
public boolean onContextItemSelected(@NotNull MenuItem menuItem)
{
if (equalizer == null) return true;
try
{
short preset = (short) menuItem.getItemId();
equalizer.usePreset(preset);
updateBars();
}
catch (Exception ex)
{
//TODO: Show a dialog?
Timber.i(ex, "An exception has occurred in EqualizerFragment onContextItemSelected");
}
return true;
}
private void setup()
{
initEqualizer();
registerForContextMenu(presetButton);
presetButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
presetButton.showContextMenu();
}
});
enabledCheckBox.setChecked(equalizer.getEnabled());
enabledCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
{
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b)
{
setEqualizerEnabled(b);
}
});
}
private void setEqualizerEnabled(boolean enabled)
{
if (equalizer == null) return;
equalizer.setEnabled(enabled);
updateBars();
}
private void updateBars()
{
if (equalizer == null) return;
try
{
for (Map.Entry<Short, SeekBar> entry : bars.entrySet())
{
short band = entry.getKey();
SeekBar bar = entry.getValue();
bar.setEnabled(equalizer.getEnabled());
short minEQLevel = equalizer.getBandLevelRange()[0];
bar.setProgress(equalizer.getBandLevel(band) - minEQLevel);
}
}
catch (Exception ex)
{
//TODO: Show a dialog?
Timber.i(ex, "An exception has occurred in EqualizerFragment updateBars");
}
}
private void initEqualizer()
{
if (equalizer == null) return;
try
{
short[] bandLevelRange = equalizer.getBandLevelRange();
short numberOfBands = equalizer.getNumberOfBands();
final short minEQLevel = bandLevelRange[0];
final short maxEQLevel = bandLevelRange[1];
for (short i = 0; i < numberOfBands; i++)
{
final short band = i;
View bandBar = LayoutInflater.from(getContext()).inflate(R.layout.equalizer_bar, equalizerLayout, false);
TextView freqTextView;
if (bandBar != null)
{
freqTextView = (TextView) bandBar.findViewById(R.id.equalizer_frequency);
final TextView levelTextView = (TextView) bandBar.findViewById(R.id.equalizer_level);
SeekBar bar = (SeekBar) bandBar.findViewById(R.id.equalizer_bar);
freqTextView.setText(String.format(Locale.getDefault(), "%d Hz", equalizer.getCenterFreq(band) / 1000));
bars.put(band, bar);
bar.setMax(maxEQLevel - minEQLevel);
short level = equalizer.getBandLevel(band);
bar.setProgress(level - minEQLevel);
bar.setEnabled(equalizer.getEnabled());
updateLevelText(levelTextView, level);
bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener()
{
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
{
short level = (short) (progress + minEQLevel);
if (fromUser)
{
try
{
equalizer.setBandLevel(band, level);
}
catch (Exception ex)
{
//TODO: Show a dialog?
Timber.i(ex, "An exception has occurred in Equalizer onProgressChanged");
}
}
updateLevelText(levelTextView, level);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar)
{
}
@Override
public void onStopTrackingTouch(SeekBar seekBar)
{
}
});
equalizerLayout.addView(bandBar);
}
}
}
catch (Exception ex)
{
//TODO: Show a dialog?
Timber.i(ex, "An exception has occurred while initializing Equalizer");
}
}
private static void updateLevelText(TextView levelTextView, short level)
{
if (levelTextView != null)
{
levelTextView.setText(String.format(Locale.getDefault(), "%s%d dB", level > 0 ? "+" : "", level / 100));
}
}
}

View File

@ -0,0 +1,103 @@
package org.moire.ultrasonic.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Lyrics;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FragmentBackgroundTask;
import org.moire.ultrasonic.util.Util;
import timber.log.Timber;
/**
* Displays the lyrics of a song
*/
public class LyricsFragment extends Fragment {
private TextView artistView;
private TextView titleView;
private TextView textView;
private SwipeRefreshLayout swipe;
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.lyrics, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
cancellationToken = new CancellationToken();
Timber.d("Lyrics set title");
FragmentTitle.Companion.setTitle(this, R.string.download_menu_lyrics);
swipe = view.findViewById(R.id.lyrics_refresh);
swipe.setEnabled(false);
artistView = view.findViewById(R.id.lyrics_artist);
titleView = view.findViewById(R.id.lyrics_title);
textView = view.findViewById(R.id.lyrics_text);
load();
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void load()
{
BackgroundTask<Lyrics> task = new FragmentBackgroundTask<Lyrics>(getActivity(), true, swipe, cancellationToken)
{
@Override
protected Lyrics doInBackground() throws Throwable
{
Bundle arguments = getArguments();
if (arguments == null) return null;
String artist = arguments.getString(Constants.INTENT_EXTRA_NAME_ARTIST);
String title = arguments.getString(Constants.INTENT_EXTRA_NAME_TITLE);
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
return musicService.getLyrics(artist, title, getContext());
}
@Override
protected void done(Lyrics result)
{
if (result != null && result.getArtist() != null)
{
artistView.setText(result.getArtist());
titleView.setText(result.getTitle());
textView.setText(result.getText());
}
else
{
artistView.setText(R.string.lyrics_nomatch);
}
}
};
task.execute();
}
}

View File

@ -0,0 +1,271 @@
package org.moire.ultrasonic.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.data.ServerSetting;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.MergeAdapter;
import org.moire.ultrasonic.util.Util;
import java.util.Collections;
import kotlin.Lazy;
import static java.util.Arrays.asList;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Displays the Main screen of Ultrasonic, where the music library can be browsed
*/
public class MainFragment extends Fragment {
private static boolean shouldUseId3;
private static String lastActiveServerProperties;
private ListView list;
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.main, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
list = view.findViewById(R.id.main_list);
setupMenuList(list);
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onResume() {
super.onResume();
boolean shouldRestart = false;
boolean id3 = Util.getShouldUseId3Tags(MainFragment.this.getContext());
String currentActiveServerProperties = getActiveServerProperties();
if (id3 != shouldUseId3)
{
shouldUseId3 = id3;
shouldRestart = true;
}
if (!currentActiveServerProperties.equals(lastActiveServerProperties))
{
lastActiveServerProperties = currentActiveServerProperties;
shouldRestart = true;
}
if (shouldRestart) {
setupMenuList(list);
}
}
private void setupMenuList(ListView list)
{
final View buttons = getLayoutInflater().inflate(R.layout.main_buttons, list, false);
final View serverButton = buttons.findViewById(R.id.main_select_server);
final TextView serverTextView = serverButton.findViewById(R.id.main_select_server_2);
lastActiveServerProperties = getActiveServerProperties();
String name = activeServerProvider.getValue().getActiveServer().getName();
serverTextView.setText(name);
final View musicTitle = buttons.findViewById(R.id.main_music);
final View artistsButton = buttons.findViewById(R.id.main_artists_button);
final View albumsButton = buttons.findViewById(R.id.main_albums_button);
final View genresButton = buttons.findViewById(R.id.main_genres_button);
final View videosTitle = buttons.findViewById(R.id.main_videos_title);
final View songsTitle = buttons.findViewById(R.id.main_songs);
final View randomSongsButton = buttons.findViewById(R.id.main_songs_button);
final View songsStarredButton = buttons.findViewById(R.id.main_songs_starred);
final View albumsTitle = buttons.findViewById(R.id.main_albums);
final View albumsNewestButton = buttons.findViewById(R.id.main_albums_newest);
final View albumsRandomButton = buttons.findViewById(R.id.main_albums_random);
final View albumsHighestButton = buttons.findViewById(R.id.main_albums_highest);
final View albumsStarredButton = buttons.findViewById(R.id.main_albums_starred);
final View albumsRecentButton = buttons.findViewById(R.id.main_albums_recent);
final View albumsFrequentButton = buttons.findViewById(R.id.main_albums_frequent);
final View albumsAlphaByNameButton = buttons.findViewById(R.id.main_albums_alphaByName);
final View albumsAlphaByArtistButton = buttons.findViewById(R.id.main_albums_alphaByArtist);
final View videosButton = buttons.findViewById(R.id.main_videos);
final MergeAdapter adapter = new MergeAdapter();
adapter.addViews(Collections.singletonList(serverButton), true);
if (!ActiveServerProvider.Companion.isOffline(this.getContext()))
{
adapter.addView(musicTitle, false);
adapter.addViews(asList(artistsButton, albumsButton, genresButton), true);
adapter.addView(songsTitle, false);
adapter.addViews(asList(randomSongsButton, songsStarredButton), true);
adapter.addView(albumsTitle, false);
if (Util.getShouldUseId3Tags(MainFragment.this.getContext()))
{
shouldUseId3 = true;
adapter.addViews(asList(albumsNewestButton, albumsRecentButton, albumsFrequentButton, albumsRandomButton, albumsStarredButton, albumsAlphaByNameButton, albumsAlphaByArtistButton), true);
}
else
{
shouldUseId3 = false;
adapter.addViews(asList(albumsNewestButton, albumsRecentButton, albumsFrequentButton, albumsHighestButton, albumsRandomButton, albumsStarredButton, albumsAlphaByNameButton, albumsAlphaByArtistButton), true);
}
adapter.addView(videosTitle, false);
adapter.addViews(Collections.singletonList(videosButton), true);
}
list.setAdapter(adapter);
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (view == serverButton)
{
showServers();
}
else if (view == albumsNewestButton)
{
showAlbumList("newest", R.string.main_albums_newest);
}
else if (view == albumsRandomButton)
{
showAlbumList("random", R.string.main_albums_random);
}
else if (view == albumsHighestButton)
{
showAlbumList("highest", R.string.main_albums_highest);
}
else if (view == albumsRecentButton)
{
showAlbumList("recent", R.string.main_albums_recent);
}
else if (view == albumsFrequentButton)
{
showAlbumList("frequent", R.string.main_albums_frequent);
}
else if (view == albumsStarredButton)
{
showAlbumList(Constants.STARRED, R.string.main_albums_starred);
}
else if (view == albumsAlphaByNameButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName);
}
else if (view == albumsAlphaByArtistButton)
{
showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist);
}
else if (view == songsStarredButton)
{
showStarredSongs();
}
else if (view == artistsButton)
{
showArtists();
}
else if (view == albumsButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title);
}
else if (view == randomSongsButton)
{
showRandomSongs();
}
else if (view == genresButton)
{
showGenres();
}
else if (view == videosButton)
{
showVideos();
}
}
});
}
private String getActiveServerProperties()
{
ServerSetting currentSetting = activeServerProvider.getValue().getActiveServer();
return String.format("%s;%s;%s;%s;%s;%s", currentSetting.getUrl(), currentSetting.getUserName(),
currentSetting.getPassword(), currentSetting.getAllowSelfSignedCertificate(),
currentSetting.getLdapSupport(), currentSetting.getMinimumApiVersion());
}
private void showAlbumList(final String type, final int title)
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxAlbums(getContext()));
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
}
private void showStarredSongs()
{
Bundle bundle = new Bundle();
bundle.putInt(Constants.INTENT_EXTRA_NAME_STARRED, 1);
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
}
private void showRandomSongs()
{
Bundle bundle = new Bundle();
bundle.putInt(Constants.INTENT_EXTRA_NAME_RANDOM, 1);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs(getContext()));
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
}
private void showArtists()
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, getContext().getResources().getString(R.string.main_artists_title));
Navigation.findNavController(getView()).navigate(R.id.selectArtistFragment, bundle);
}
private void showGenres()
{
Navigation.findNavController(getView()).navigate(R.id.mainToSelectGenre);
}
private void showVideos()
{
Bundle bundle = new Bundle();
bundle.putInt(Constants.INTENT_EXTRA_NAME_VIDEOS, 1);
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
}
private void showServers()
{
Navigation.findNavController(getView()).navigate(R.id.mainToServerSelector);
}
}

View File

@ -0,0 +1,209 @@
package org.moire.ultrasonic.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.NowPlayingEventDistributor;
import org.moire.ultrasonic.util.NowPlayingEventListener;
import org.moire.ultrasonic.util.Util;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Contains the mini-now playing information box displayed at the bottom of the screen
*/
public class NowPlayingFragment extends Fragment {
private static final int MIN_DISTANCE = 30;
private float downX;
private float downY;
ImageView playButton;
ImageView nowPlayingAlbumArtImage;
TextView nowPlayingTrack;
TextView nowPlayingArtist;
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
private final Lazy<NowPlayingEventDistributor> nowPlayingEventDistributor = inject(NowPlayingEventDistributor.class);
private NowPlayingEventListener nowPlayingEventListener;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.now_playing, container, false);
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable Bundle savedInstanceState) {
playButton = (ImageView) view.findViewById(R.id.now_playing_control_play);
nowPlayingAlbumArtImage = (ImageView) view.findViewById(R.id.now_playing_image);
nowPlayingTrack = (TextView) view.findViewById(R.id.now_playing_trackname);
nowPlayingArtist = (TextView) view.findViewById(R.id.now_playing_artist);
nowPlayingEventListener = new NowPlayingEventListener() {
@Override
public void onDismissNowPlaying() { }
@Override
public void onHideNowPlaying() { }
@Override
public void onShowNowPlaying() { Update(); }
};
nowPlayingEventDistributor.getValue().subscribe(nowPlayingEventListener);
}
@Override
public void onResume() {
super.onResume();
Update();
}
@Override
public void onDestroy() {
super.onDestroy();
nowPlayingEventDistributor.getValue().unsubscribe(nowPlayingEventListener);
}
private void Update() {
try
{
PlayerState playerState = mediaPlayerControllerLazy.getValue().getPlayerState();
if (playerState == PlayerState.PAUSED) {
playButton.setImageDrawable(Util.getDrawableFromAttribute(getContext(), R.attr.media_play));
} else if (playerState == PlayerState.STARTED) {
playButton.setImageDrawable(Util.getDrawableFromAttribute(getContext(), R.attr.media_pause));
}
DownloadFile file = mediaPlayerControllerLazy.getValue().getCurrentPlaying();
if (file != null) {
final MusicDirectory.Entry song = file.getSong();
String title = song.getTitle();
String artist = song.getArtist();
imageLoader.getValue().getImageLoader().loadImage(nowPlayingAlbumArtImage, song, false, Util.getNotificationImageSize(getContext()), false, true);
nowPlayingTrack.setText(title);
nowPlayingArtist.setText(artist);
nowPlayingAlbumArtImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Bundle bundle = new Bundle();
if (Util.getShouldUseId3Tags(getContext())) {
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, true);
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, song.getAlbumId());
} else {
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, false);
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, song.getParent());
}
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.getAlbum());
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
}
});
}
getView().setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return handleOnTouch(event);
}
});
// This empty onClickListener is necessary for the onTouchListener to work
getView().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
playButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mediaPlayerControllerLazy.getValue().togglePlayPause();
}
});
}
catch (Exception x) {
Timber.w(x, "Failed to get notification cover art");
}
}
private boolean handleOnTouch(MotionEvent event) {
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
{
downX = event.getX();
downY = event.getY();
return false;
}
case MotionEvent.ACTION_UP:
{
float upX = event.getX();
float upY = event.getY();
float deltaX = downX - upX;
float deltaY = downY - upY;
if (Math.abs(deltaX) > MIN_DISTANCE)
{
// left or right
if (deltaX < 0)
{
mediaPlayerControllerLazy.getValue().previous();
return false;
}
if (deltaX > 0)
{
mediaPlayerControllerLazy.getValue().next();
return false;
}
}
else if (Math.abs(deltaY) > MIN_DISTANCE)
{
if (deltaY < 0)
{
nowPlayingEventDistributor.getValue().raiseNowPlayingDismissedEvent();
return false;
}
if (deltaY > 0)
{
return false;
}
}
Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(R.id.playerFragment);
return false;
}
}
return false;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,342 @@
package org.moire.ultrasonic.fragment;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Playlist;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.service.OfflineException;
import org.moire.ultrasonic.subsonic.DownloadHandler;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CacheCleaner;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.LoadingTask;
import org.moire.ultrasonic.util.FragmentBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.PlaylistAdapter;
import java.util.List;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Displays the playlists stored on the server
*/
public class PlaylistsFragment extends Fragment {
private SwipeRefreshLayout refreshPlaylistsListView;
private ListView playlistsListView;
private View emptyTextView;
private PlaylistAdapter playlistAdapter;
private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.select_playlist, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
cancellationToken = new CancellationToken();
refreshPlaylistsListView = view.findViewById(R.id.select_playlist_refresh);
playlistsListView = view.findViewById(R.id.select_playlist_list);
refreshPlaylistsListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh() {
load(true);
}
});
emptyTextView = view.findViewById(R.id.select_playlist_empty);
playlistsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Playlist playlist = (Playlist) parent.getItemAtPosition(position);
if (playlist == null)
{
return;
}
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
}
});
registerForContextMenu(playlistsListView);
FragmentTitle.Companion.setTitle(this, R.string.playlist_label);
load(false);
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void load(final boolean refresh)
{
BackgroundTask<List<Playlist>> task = new FragmentBackgroundTask<List<Playlist>>(getActivity(), true, refreshPlaylistsListView, cancellationToken)
{
@Override
protected List<Playlist> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
List<Playlist> playlists = musicService.getPlaylists(refresh, getContext());
if (!ActiveServerProvider.Companion.isOffline(getContext()))
new CacheCleaner(getContext()).cleanPlaylists(playlists);
return playlists;
}
@Override
protected void done(List<Playlist> result)
{
playlistsListView.setAdapter(playlistAdapter = new PlaylistAdapter(getContext(), result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
@Override
public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
MenuInflater inflater = getActivity().getMenuInflater();
if (ActiveServerProvider.Companion.isOffline(getContext())) inflater.inflate(R.menu.select_playlist_context_offline, menu);
else inflater.inflate(R.menu.select_playlist_context, menu);
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
if (downloadMenuItem != null)
{
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(getContext()));
}
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return false;
}
Playlist playlist = (Playlist) playlistsListView.getItemAtPosition(info.position);
if (playlist == null)
{
return false;
}
Bundle bundle;
int itemId = menuItem.getItemId();
if (itemId == R.id.playlist_menu_pin) {
downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), true, true, false, false, true, false, false);
} else if (itemId == R.id.playlist_menu_unpin) {
downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, true);
} else if (itemId == R.id.playlist_menu_download) {
downloadHandler.getValue().downloadPlaylist(this, playlist.getId(), playlist.getName(), false, false, false, false, true, false, false);
} else if (itemId == R.id.playlist_menu_play_now) {
bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
} else if (itemId == R.id.playlist_menu_play_shuffled) {
bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
} else if (itemId == R.id.playlist_menu_delete) {
deletePlaylist(playlist);
} else if (itemId == R.id.playlist_info) {
displayPlaylistInfo(playlist);
} else if (itemId == R.id.playlist_update_info) {
updatePlaylistInfo(playlist);
} else {
return super.onContextItemSelected(menuItem);
}
return true;
}
private void deletePlaylist(final Playlist playlist)
{
new AlertDialog.Builder(getContext()).setIcon(android.R.drawable.ic_dialog_alert).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, playlist.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(getActivity(), refreshPlaylistsListView, cancellationToken)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
musicService.deletePlaylist(playlist.getId(), getContext());
return null;
}
@Override
protected void done(Void result)
{
playlistAdapter.remove(playlist);
playlistAdapter.notifyDataSetChanged();
Util.toast(getContext(), getResources().getString(R.string.menu_deleted_playlist, playlist.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_playlist_error, playlist.getName()), getErrorMessage(error));
Util.toast(getContext(), msg, false);
}
}.execute();
}
}).setNegativeButton(R.string.common_cancel, null).show();
}
private void displayPlaylistInfo(final Playlist playlist)
{
final TextView textView = new TextView(getContext());
textView.setPadding(5, 5, 5, 5);
final Spannable message = new SpannableString("Owner: " + playlist.getOwner() + "\nComments: " +
((playlist.getComment() == null) ? "" : playlist.getComment()) +
"\nSong Count: " + playlist.getSongCount() +
((playlist.getPublic() == null) ? "" : ("\nPublic: " + playlist.getPublic()) + ((playlist.getCreated() == null) ? "" : ("\nCreation Date: " + playlist.getCreated().replace('T', ' ')))));
Linkify.addLinks(message, Linkify.WEB_URLS);
textView.setText(message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(getContext()).setTitle(playlist.getName()).setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setView(textView).show();
}
private void updatePlaylistInfo(final Playlist playlist)
{
View dialogView = getLayoutInflater().inflate(R.layout.update_playlist, null);
if (dialogView == null)
{
return;
}
final EditText nameBox = dialogView.findViewById(R.id.get_playlist_name);
final EditText commentBox = dialogView.findViewById(R.id.get_playlist_comment);
final CheckBox publicBox = dialogView.findViewById(R.id.get_playlist_public);
nameBox.setText(playlist.getName());
commentBox.setText(playlist.getComment());
Boolean pub = playlist.getPublic();
if (pub == null)
{
publicBox.setEnabled(false);
}
else
{
publicBox.setChecked(pub);
}
AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
alertDialog.setIcon(android.R.drawable.ic_dialog_alert);
alertDialog.setTitle(R.string.playlist_update_info);
alertDialog.setView(dialogView);
alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(getActivity(), refreshPlaylistsListView, cancellationToken)
{
@Override
protected Void doInBackground() throws Throwable
{
Editable nameBoxText = nameBox.getText();
Editable commentBoxText = commentBox.getText();
String name = nameBoxText != null ? nameBoxText.toString() : null;
String comment = commentBoxText != null ? commentBoxText.toString() : null;
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
musicService.updatePlaylist(playlist.getId(), name, comment, publicBox.isChecked(), getContext());
return null;
}
@Override
protected void done(Void result)
{
load(true);
Util.toast(getContext(), getResources().getString(R.string.playlist_updated_info, playlist.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, playlist.getName()), getErrorMessage(error));
Util.toast(getContext(), msg, false);
}
}.execute();
}
});
alertDialog.setNegativeButton(R.string.common_cancel, null);
alertDialog.show();
}
}

View File

@ -0,0 +1,106 @@
package org.moire.ultrasonic.fragment;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.PodcastsChannel;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FragmentBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.PodcastsChannelsAdapter;
import java.util.List;
/**
* Displays the podcasts available on the server
*/
public class PodcastFragment extends Fragment {
private View emptyTextView;
ListView channelItemsListView = null;
private CancellationToken cancellationToken;
private SwipeRefreshLayout swipeRefresh;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.podcasts, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
cancellationToken = new CancellationToken();
swipeRefresh = view.findViewById(R.id.podcasts_refresh);
swipeRefresh.setEnabled(false);
FragmentTitle.Companion.setTitle(this, R.string.podcasts_label);
emptyTextView = view.findViewById(R.id.select_podcasts_empty);
channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list);
channelItemsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
PodcastsChannel pc = (PodcastsChannel) parent.getItemAtPosition(position);
if (pc == null) {
return;
}
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID, pc.getId());
Navigation.findNavController(view).navigate(R.id.selectAlbumFragment, bundle);
}
});
load(view.getContext());
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void load(final Context context)
{
BackgroundTask<List<PodcastsChannel>> task = new FragmentBackgroundTask<List<PodcastsChannel>>(getActivity(), true, swipeRefresh, cancellationToken)
{
@Override
protected List<PodcastsChannel> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(context);
return musicService.getPodcastsChannels(false, context);
}
@Override
protected void done(List<PodcastsChannel> result)
{
channelItemsListView.setAdapter(new PodcastsChannelsAdapter(context, result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
}

View File

@ -0,0 +1,593 @@
package org.moire.ultrasonic.fragment;
import android.app.Activity;
import android.app.SearchManager;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Artist;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.SearchCriteria;
import org.moire.ultrasonic.domain.SearchResult;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.subsonic.DownloadHandler;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker;
import org.moire.ultrasonic.subsonic.ShareHandler;
import org.moire.ultrasonic.subsonic.VideoPlayer;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.MergeAdapter;
import org.moire.ultrasonic.util.FragmentBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ArtistAdapter;
import org.moire.ultrasonic.view.EntryAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Initiates a search on the media library and displays the results
*/
public class SearchFragment extends Fragment {
private static int DEFAULT_ARTISTS;
private static int DEFAULT_ALBUMS;
private static int DEFAULT_SONGS;
private ListView list;
private View artistsHeading;
private View albumsHeading;
private View songsHeading;
private TextView notFound;
private View moreArtistsButton;
private View moreAlbumsButton;
private View moreSongsButton;
private SearchResult searchResult;
private MergeAdapter mergeAdapter;
private ArtistAdapter artistAdapter;
private ListAdapter moreArtistsAdapter;
private EntryAdapter albumAdapter;
private ListAdapter moreAlbumsAdapter;
private ListAdapter moreSongsAdapter;
private EntryAdapter songAdapter;
private SwipeRefreshLayout searchRefresh;
private final Lazy<VideoPlayer> videoPlayer = inject(VideoPlayer.class);
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
private final Lazy<ShareHandler> shareHandler = inject(ShareHandler.class);
private final Lazy<NetworkAndStorageChecker> networkAndStorageChecker = inject(NetworkAndStorageChecker.class);
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.search, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
cancellationToken = new CancellationToken();
FragmentTitle.Companion.setTitle(this, R.string.search_title);
setHasOptionsMenu(true);
DEFAULT_ARTISTS = Util.getDefaultArtists(getContext());
DEFAULT_ALBUMS = Util.getDefaultAlbums(getContext());
DEFAULT_SONGS = Util.getDefaultSongs(getContext());
View buttons = LayoutInflater.from(getContext()).inflate(R.layout.search_buttons, list, false);
if (buttons != null)
{
artistsHeading = buttons.findViewById(R.id.search_artists);
albumsHeading = buttons.findViewById(R.id.search_albums);
songsHeading = buttons.findViewById(R.id.search_songs);
notFound = buttons.findViewById(R.id.search_not_found);
moreArtistsButton = buttons.findViewById(R.id.search_more_artists);
moreAlbumsButton = buttons.findViewById(R.id.search_more_albums);
moreSongsButton = buttons.findViewById(R.id.search_more_songs);
}
list = view.findViewById(R.id.search_list);
searchRefresh = view.findViewById(R.id.search_entries_refresh);
searchRefresh.setEnabled(false); // TODO: It should be enabled if it is a good feature to refresh search results
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (view == moreArtistsButton)
{
expandArtists();
}
else if (view == moreAlbumsButton)
{
expandAlbums();
}
else if (view == moreSongsButton)
{
expandSongs();
}
else
{
Object item = parent.getItemAtPosition(position);
if (item instanceof Artist)
{
onArtistSelected((Artist) item);
}
else if (item instanceof MusicDirectory.Entry)
{
MusicDirectory.Entry entry = (MusicDirectory.Entry) item;
if (entry.isDirectory())
{
onAlbumSelected(entry, false);
}
else if (entry.isVideo())
{
onVideoSelected(entry);
}
else
{
onSongSelected(entry, true);
}
}
}
}
});
registerForContextMenu(list);
// Fragment was started with a query (e.g. from voice search), try to execute search right away
Bundle arguments = getArguments();
if (arguments != null) {
String query = arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY);
boolean autoPlay = arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
if (query != null) {
mergeAdapter = new MergeAdapter();
list.setAdapter(mergeAdapter);
search(query, autoPlay);
return;
}
}
// Fragment was started from the Menu, create empty list
populateList();
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
Activity activity = getActivity();
if (activity == null) return;
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
inflater.inflate(R.menu.search, menu);
MenuItem searchItem = menu.findItem(R.id.search_item);
final SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName()));
Bundle arguments = getArguments();
final boolean autoPlay = arguments != null && arguments.getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false);
String query = arguments == null? null : arguments.getString(Constants.INTENT_EXTRA_NAME_QUERY);
// If started with a query, enter it to the searchView
if (query != null) {
searchView.setQuery(query, false);
searchView.clearFocus();
}
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
@Override
public boolean onSuggestionSelect(int position) { return true; }
@Override
public boolean onSuggestionClick(int position) {
Timber.d("onSuggestionClick: %d", position);
Cursor cursor= searchView.getSuggestionsAdapter().getCursor();
cursor.moveToPosition(position);
String suggestion = cursor.getString(2); // TODO: Try to do something with this magic const -- 2 is the index of col containing suggestion name.
searchView.setQuery(suggestion,true);
return true;
}
});
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
Timber.d("onQueryTextSubmit: %s", query);
mergeAdapter = new MergeAdapter();
list.setAdapter(mergeAdapter);
searchView.clearFocus();
search(query, autoPlay);
return true;
}
@Override
public boolean onQueryTextChange(String newText) { return true; }
});
searchView.setIconifiedByDefault(false);
searchItem.expandActionView();
}
@Override
public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
if (getActivity() == null) return;
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
Object selectedItem = list.getItemAtPosition(info.position);
boolean isArtist = selectedItem instanceof Artist;
boolean isAlbum = selectedItem instanceof MusicDirectory.Entry && ((MusicDirectory.Entry) selectedItem).isDirectory();
MenuInflater inflater = getActivity().getMenuInflater();
if (!isArtist && !isAlbum)
{
inflater.inflate(R.menu.select_song_context, menu);
}
else
{
inflater.inflate(R.menu.select_album_context, menu);
}
MenuItem shareButton = menu.findItem(R.id.menu_item_share);
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
if (downloadMenuItem != null)
{
downloadMenuItem.setVisible(!ActiveServerProvider.Companion.isOffline(getContext()));
}
if (ActiveServerProvider.Companion.isOffline(getContext()) || isArtist)
{
if (shareButton != null)
{
shareButton.setVisible(false);
}
}
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null)
{
return true;
}
Object selectedItem = list.getItemAtPosition(info.position);
Artist artist = selectedItem instanceof Artist ? (Artist) selectedItem : null;
MusicDirectory.Entry entry = selectedItem instanceof MusicDirectory.Entry ? (MusicDirectory.Entry) selectedItem : null;
String entryId = null;
if (entry != null)
{
entryId = entry.getId();
}
String id = artist != null ? artist.getId() : entryId;
if (id == null)
{
return true;
}
List<MusicDirectory.Entry> songs = new ArrayList<>(1);
int itemId = menuItem.getItemId();
if (itemId == R.id.album_menu_play_now) {
downloadHandler.getValue().downloadRecursively(this, id, false, false, true, false, false, false, false, false);
} else if (itemId == R.id.album_menu_play_next) {
downloadHandler.getValue().downloadRecursively(this, id, false, true, false, true, false, true, false, false);
} else if (itemId == R.id.album_menu_play_last) {
downloadHandler.getValue().downloadRecursively(this, id, false, true, false, false, false, false, false, false);
} else if (itemId == R.id.album_menu_pin) {
downloadHandler.getValue().downloadRecursively(this, id, true, true, false, false, false, false, false, false);
} else if (itemId == R.id.album_menu_unpin) {
downloadHandler.getValue().downloadRecursively(this, id, false, false, false, false, false, false, true, false);
} else if (itemId == R.id.album_menu_download) {
downloadHandler.getValue().downloadRecursively(this, id, false, false, false, false, true, false, false, false);
} else if (itemId == R.id.song_menu_play_now) {
if (entry != null) {
songs = new ArrayList<>(1);
songs.add(entry);
downloadHandler.getValue().download(this, false, false, true, false, false, songs);
}
} else if (itemId == R.id.song_menu_play_next) {
if (entry != null) {
songs = new ArrayList<>(1);
songs.add(entry);
downloadHandler.getValue().download(this, true, false, false, true, false, songs);
}
} else if (itemId == R.id.song_menu_play_last) {
if (entry != null) {
songs = new ArrayList<>(1);
songs.add(entry);
downloadHandler.getValue().download(this, true, false, false, false, false, songs);
}
} else if (itemId == R.id.song_menu_pin) {
if (entry != null) {
songs.add(entry);
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_pinned, songs.size(), songs.size()));
downloadBackground(true, songs);
}
} else if (itemId == R.id.song_menu_download) {
if (entry != null) {
songs.add(entry);
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
downloadBackground(false, songs);
}
} else if (itemId == R.id.song_menu_unpin) {
if (entry != null) {
songs.add(entry);
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_unpinned, songs.size(), songs.size()));
mediaPlayerControllerLazy.getValue().unpin(songs);
}
} else if (itemId == R.id.menu_item_share) {
if (entry != null) {
songs = new ArrayList<>(1);
songs.add(entry);
shareHandler.getValue().createShare(this, songs, searchRefresh, cancellationToken);
}
return super.onContextItemSelected(menuItem);
} else {
return super.onContextItemSelected(menuItem);
}
return true;
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{
Runnable onValid = new Runnable()
{
@Override
public void run()
{
networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable();
mediaPlayerControllerLazy.getValue().downloadBackground(songs, save);
}
};
onValid.run();
}
private void search(final String query, final boolean autoplay)
{
final int maxArtists = Util.getMaxArtists(getContext());
final int maxAlbums = Util.getMaxAlbums(getContext());
final int maxSongs = Util.getMaxSongs(getContext());
BackgroundTask<SearchResult> task = new FragmentBackgroundTask<SearchResult>(getActivity(), true, searchRefresh, cancellationToken)
{
@Override
protected SearchResult doInBackground() throws Throwable
{
SearchCriteria criteria = new SearchCriteria(query, maxArtists, maxAlbums, maxSongs);
MusicService service = MusicServiceFactory.getMusicService(getContext());
return service.search(criteria, getContext());
}
@Override
protected void done(SearchResult result)
{
searchResult = result;
populateList();
if (autoplay)
{
autoplay();
}
}
};
task.execute();
}
private void populateList()
{
mergeAdapter = new MergeAdapter();
if (searchResult != null)
{
List<Artist> artists = searchResult.getArtists();
if (!artists.isEmpty())
{
mergeAdapter.addView(artistsHeading);
List<Artist> displayedArtists = new ArrayList<>(artists.subList(0, Math.min(DEFAULT_ARTISTS, artists.size())));
artistAdapter = new ArtistAdapter(getContext(), displayedArtists);
mergeAdapter.addAdapter(artistAdapter);
if (artists.size() > DEFAULT_ARTISTS)
{
moreArtistsAdapter = mergeAdapter.addView(moreArtistsButton, true);
}
}
List<MusicDirectory.Entry> albums = searchResult.getAlbums();
if (!albums.isEmpty())
{
mergeAdapter.addView(albumsHeading);
List<MusicDirectory.Entry> displayedAlbums = new ArrayList<>(albums.subList(0, Math.min(DEFAULT_ALBUMS, albums.size())));
albumAdapter = new EntryAdapter(getContext(), imageLoaderProvider.getValue().getImageLoader(), displayedAlbums, false);
mergeAdapter.addAdapter(albumAdapter);
if (albums.size() > DEFAULT_ALBUMS)
{
moreAlbumsAdapter = mergeAdapter.addView(moreAlbumsButton, true);
}
}
List<MusicDirectory.Entry> songs = searchResult.getSongs();
if (!songs.isEmpty())
{
mergeAdapter.addView(songsHeading);
List<MusicDirectory.Entry> displayedSongs = new ArrayList<>(songs.subList(0, Math.min(DEFAULT_SONGS, songs.size())));
songAdapter = new EntryAdapter(getContext(), imageLoaderProvider.getValue().getImageLoader(), displayedSongs, false);
mergeAdapter.addAdapter(songAdapter);
if (songs.size() > DEFAULT_SONGS)
{
moreSongsAdapter = mergeAdapter.addView(moreSongsButton, true);
}
}
boolean empty = searchResult.getArtists().isEmpty() && searchResult.getAlbums().isEmpty() && searchResult.getSongs().isEmpty();
if (empty) mergeAdapter.addView(notFound, false);
}
list.setAdapter(mergeAdapter);
}
private void expandArtists()
{
artistAdapter.clear();
for (Artist artist : searchResult.getArtists())
{
artistAdapter.add(artist);
}
artistAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreArtistsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void expandAlbums()
{
albumAdapter.clear();
for (MusicDirectory.Entry album : searchResult.getAlbums())
{
albumAdapter.add(album);
}
albumAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreAlbumsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void expandSongs()
{
songAdapter.clear();
for (MusicDirectory.Entry song : searchResult.getSongs())
{
songAdapter.add(song);
}
songAdapter.notifyDataSetChanged();
mergeAdapter.removeAdapter(moreSongsAdapter);
mergeAdapter.notifyDataSetChanged();
}
private void onArtistSelected(Artist artist)
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.getId());
Navigation.findNavController(getView()).navigate(R.id.searchToSelectAlbum, bundle);
}
private void onAlbumSelected(MusicDirectory.Entry album, boolean autoplay)
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, album.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, album.getTitle());
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, album.isDirectory());
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoplay);
Navigation.findNavController(getView()).navigate(R.id.searchToSelectAlbum, bundle);
}
private void onSongSelected(MusicDirectory.Entry song, boolean append)
{
MediaPlayerController mediaPlayerController = mediaPlayerControllerLazy.getValue();
if (mediaPlayerController != null)
{
if (!append)
{
mediaPlayerController.clear();
}
mediaPlayerController.download(Collections.singletonList(song), false, false, false, false, false);
if (true)
{
mediaPlayerController.play(mediaPlayerController.getPlaylistSize() - 1);
}
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_added, 1, 1));
}
}
private void onVideoSelected(MusicDirectory.Entry entry)
{
videoPlayer.getValue().playVideo(entry);
}
private void autoplay()
{
if (!searchResult.getSongs().isEmpty())
{
onSongSelected(searchResult.getSongs().get(0), false);
}
else if (!searchResult.getAlbums().isEmpty())
{
onAlbumSelected(searchResult.getAlbums().get(0), true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,134 @@
package org.moire.ultrasonic.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Genre;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FragmentBackgroundTask;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.GenreAdapter;
import java.util.ArrayList;
import java.util.List;
import timber.log.Timber;
/**
* Displays the available genres in the media library
*/
public class SelectGenreFragment extends Fragment {
private SwipeRefreshLayout refreshGenreListView;
private ListView genreListView;
private View emptyView;
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.select_genre, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
cancellationToken = new CancellationToken();
refreshGenreListView = view.findViewById(R.id.select_genre_refresh);
genreListView = view.findViewById(R.id.select_genre_list);
refreshGenreListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
load(true);
}
});
genreListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Genre genre = (Genre) parent.getItemAtPosition(position);
if (genre != null)
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_GENRE_NAME, genre.getName());
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs(getContext()));
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
Navigation.findNavController(view).navigate(R.id.selectAlbumFragment, bundle);
}
}
});
emptyView = view.findViewById(R.id.select_genre_empty);
registerForContextMenu(genreListView);
FragmentTitle.Companion.setTitle(this, R.string.main_genres_title);
load(false);
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void load(final boolean refresh)
{
BackgroundTask<List<Genre>> task = new FragmentBackgroundTask<List<Genre>>(getActivity(), true, refreshGenreListView, cancellationToken)
{
@Override
protected List<Genre> doInBackground()
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
List<Genre> genres = new ArrayList<>();
try
{
genres = musicService.getGenres(refresh, getContext());
}
catch (Exception x)
{
Timber.e(x, "Failed to load genres");
}
return genres;
}
@Override
protected void done(List<Genre> result)
{
emptyView.setVisibility(result == null || result.isEmpty() ? View.VISIBLE : View.GONE);
if (result != null)
{
genreListView.setAdapter(new GenreAdapter(getContext(), result));
}
}
};
task.execute();
}
}

View File

@ -2,22 +2,28 @@ package org.moire.ultrasonic.fragment;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.*;
import android.provider.SearchRecentSuggestions;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.DialogFragment;
import androidx.navigation.Navigation;
import androidx.preference.CheckBoxPreference;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import timber.log.Timber;
import android.view.View;
import org.jetbrains.annotations.NotNull;
import org.koin.java.KoinJavaComponent;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.ServerSelectorActivity;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.featureflags.Feature;
import org.moire.ultrasonic.featureflags.FeatureStorage;
import org.moire.ultrasonic.filepicker.FilePickerDialog;
@ -26,6 +32,7 @@ import org.moire.ultrasonic.log.FileLoggerTree;
import org.moire.ultrasonic.provider.SearchSuggestionProvider;
import org.moire.ultrasonic.service.Consumer;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.util.*;
import java.io.File;
@ -33,14 +40,15 @@ import java.io.File;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
import static org.moire.ultrasonic.activity.ServerSelectorActivity.SERVER_SELECTOR_MANAGE_MODE;
import static org.moire.ultrasonic.fragment.ServerSelectorFragment.SERVER_SELECTOR_MANAGE_MODE;
/**
* Shows main app settings.
*/
public class SettingsFragment extends PreferenceFragment
public class SettingsFragment extends PreferenceFragmentCompat
implements SharedPreferences.OnSharedPreferenceChangeListener {
private Preference addServerPreference;
private ListPreference theme;
private ListPreference videoPlayer;
private ListPreference maxBitrateWifi;
@ -69,61 +77,68 @@ public class SettingsFragment extends PreferenceFragment
private EditTextPreference sharingDefaultDescription;
private EditTextPreference sharingDefaultGreeting;
private TimeSpanPreference sharingDefaultExpiration;
private PreferenceCategory serversCategory;
private Preference resumeOnBluetoothDevice;
private Preference pauseOnBluetoothDevice;
private CheckBoxPreference debugLogToFile;
private SharedPreferences settings;
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
private final Lazy<PermissionUtil> permissionUtil = inject(PermissionUtil.class);
private final Lazy<ThemeChangedEventDistributor> themeChangedEventDistributor = inject(ThemeChangedEventDistributor.class);
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.settings);
settings = PreferenceManager.getDefaultSharedPreferences(getActivity());
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.settings, rootKey);
}
theme = (ListPreference) findPreference(Constants.PREFERENCES_KEY_THEME);
videoPlayer = (ListPreference) findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER);
maxBitrateWifi = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
maxBitrateMobile = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
cacheSize = (ListPreference) findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
@Override
public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
FragmentTitle.Companion.setTitle(this, R.string.menu_settings);
addServerPreference = findPreference(Constants.PREFERENCES_KEY_SERVERS_EDIT);
theme = findPreference(Constants.PREFERENCES_KEY_THEME);
videoPlayer = findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER);
maxBitrateWifi = findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
maxBitrateMobile = findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
cacheSize = findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
cacheLocation = findPreference(Constants.PREFERENCES_KEY_CACHE_LOCATION);
preloadCount = (ListPreference) findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT);
bufferLength = (ListPreference) findPreference(Constants.PREFERENCES_KEY_BUFFER_LENGTH);
incrementTime = (ListPreference) findPreference(Constants.PREFERENCES_KEY_INCREMENT_TIME);
networkTimeout = (ListPreference) findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT);
maxAlbums = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_ALBUMS);
maxSongs = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_SONGS);
maxArtists = (ListPreference) findPreference(Constants.PREFERENCES_KEY_MAX_ARTISTS);
defaultArtists = (ListPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_ARTISTS);
defaultSongs = (ListPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SONGS);
defaultAlbums = (ListPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_ALBUMS);
chatRefreshInterval = (ListPreference) findPreference(Constants.PREFERENCES_KEY_CHAT_REFRESH_INTERVAL);
directoryCacheTime = (ListPreference) findPreference(Constants.PREFERENCES_KEY_DIRECTORY_CACHE_TIME);
mediaButtonsEnabled = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_MEDIA_BUTTONS);
lockScreenEnabled = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS);
sendBluetoothAlbumArt = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART);
sendBluetoothNotifications = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS);
viewRefresh = (ListPreference) findPreference(Constants.PREFERENCES_KEY_VIEW_REFRESH);
imageLoaderConcurrency = (ListPreference) findPreference(Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY);
sharingDefaultDescription = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION);
sharingDefaultGreeting = (EditTextPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING);
sharingDefaultExpiration = (TimeSpanPreference) findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION);
serversCategory = (PreferenceCategory) findPreference(Constants.PREFERENCES_KEY_SERVERS_KEY);
preloadCount = findPreference(Constants.PREFERENCES_KEY_PRELOAD_COUNT);
bufferLength = findPreference(Constants.PREFERENCES_KEY_BUFFER_LENGTH);
incrementTime = findPreference(Constants.PREFERENCES_KEY_INCREMENT_TIME);
networkTimeout = findPreference(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT);
maxAlbums = findPreference(Constants.PREFERENCES_KEY_MAX_ALBUMS);
maxSongs = findPreference(Constants.PREFERENCES_KEY_MAX_SONGS);
maxArtists = findPreference(Constants.PREFERENCES_KEY_MAX_ARTISTS);
defaultArtists = findPreference(Constants.PREFERENCES_KEY_DEFAULT_ARTISTS);
defaultSongs = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SONGS);
defaultAlbums = findPreference(Constants.PREFERENCES_KEY_DEFAULT_ALBUMS);
chatRefreshInterval = findPreference(Constants.PREFERENCES_KEY_CHAT_REFRESH_INTERVAL);
directoryCacheTime = findPreference(Constants.PREFERENCES_KEY_DIRECTORY_CACHE_TIME);
mediaButtonsEnabled = findPreference(Constants.PREFERENCES_KEY_MEDIA_BUTTONS);
lockScreenEnabled = findPreference(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS);
sendBluetoothAlbumArt = findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART);
sendBluetoothNotifications = findPreference(Constants.PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS);
viewRefresh = findPreference(Constants.PREFERENCES_KEY_VIEW_REFRESH);
imageLoaderConcurrency = findPreference(Constants.PREFERENCES_KEY_IMAGE_LOADER_CONCURRENCY);
sharingDefaultDescription = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION);
sharingDefaultGreeting = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_GREETING);
sharingDefaultExpiration = findPreference(Constants.PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION);
resumeOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE);
pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE);
debugLogToFile = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE);
showArtistPicture = (CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE);
debugLogToFile = findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE);
showArtistPicture = findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE);
setupServersCategory();
sharingDefaultGreeting.setText(Util.getShareGreeting(getActivity()));
setupClearSearchPreference();
setupGaplessControlSettingsV14();
@ -133,9 +148,11 @@ public class SettingsFragment extends PreferenceFragment
// After API26 foreground services must be used for music playback, and they must have a notification
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PreferenceCategory notificationsCategory = (PreferenceCategory) findPreference(Constants.PREFERENCES_KEY_CATEGORY_NOTIFICATIONS);
notificationsCategory.removePreference(findPreference(Constants.PREFERENCES_KEY_SHOW_NOTIFICATION));
notificationsCategory.removePreference(findPreference(Constants.PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION));
PreferenceCategory notificationsCategory = findPreference(Constants.PREFERENCES_KEY_CATEGORY_NOTIFICATIONS);
Preference preferenceToRemove = findPreference(Constants.PREFERENCES_KEY_SHOW_NOTIFICATION);
if (preferenceToRemove != null) notificationsCategory.removePreference(preferenceToRemove);
preferenceToRemove = findPreference(Constants.PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION);
if (preferenceToRemove != null) notificationsCategory.removePreference(preferenceToRemove);
}
}
@ -149,8 +166,6 @@ public class SettingsFragment extends PreferenceFragment
@Override
public void onResume() {
super.onResume();
setupServersCategory();
SharedPreferences preferences = Util.getPreferences(getActivity());
preferences.registerOnSharedPreferenceChangeListener(this);
}
@ -158,7 +173,6 @@ public class SettingsFragment extends PreferenceFragment
@Override
public void onPause() {
super.onPause();
SharedPreferences prefs = Util.getPreferences(getActivity());
prefs.unregisterOnSharedPreferenceChangeListener(this);
}
@ -179,8 +193,32 @@ public class SettingsFragment extends PreferenceFragment
} else if (Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE.equals(key)) {
setDebugLogToFile(sharedPreferences.getBoolean(key, false));
} else if (Constants.PREFERENCES_KEY_ID3_TAGS.equals(key)) {
if (sharedPreferences.getBoolean(key, false)) showArtistPicture.setEnabled(true);
else showArtistPicture.setEnabled(false);
showArtistPicture.setEnabled(sharedPreferences.getBoolean(key, false));
} else if (Constants.PREFERENCES_KEY_THEME.equals(key)) {
themeChangedEventDistributor.getValue().RaiseThemeChangedEvent();
}
}
@Override
public void onDisplayPreferenceDialog(Preference preference)
{
DialogFragment dialogFragment = null;
if (preference instanceof TimeSpanPreference)
{
dialogFragment = new TimeSpanPreferenceDialogFragmentCompat();
Bundle bundle = new Bundle(1);
bundle.putString("key", preference.getKey());
dialogFragment.setArguments(bundle);
}
if (dialogFragment != null)
{
dialogFragment.setTargetFragment(this, 0);
dialogFragment.show(this.getParentFragmentManager(), "android.support.v7.preference.PreferenceFragment.DIALOG");
}
else
{
super.onDisplayPreferenceDialog(preference);
}
}
@ -196,15 +234,15 @@ public class SettingsFragment extends PreferenceFragment
@Override
public void onPermissionRequestFinished(boolean hasPermission) {
if (hasPermission) {
FilePickerDialog filePickerDialog = FilePickerDialog.Companion.createFilePickerDialog(getActivity());
FilePickerDialog filePickerDialog = FilePickerDialog.Companion.createFilePickerDialog(getContext());
filePickerDialog.setDefaultDirectory(FileUtil.getDefaultMusicDirectory(getActivity()).getPath());
filePickerDialog.setInitialDirectory(cacheLocation.getSummary().toString());
filePickerDialog.setOnFileSelectedListener(new OnFileSelectedListener() {
@Override
public void onFileSelected(File file, String path) {
SharedPreferences.Editor editor = cacheLocation.getEditor();
SharedPreferences.Editor editor = cacheLocation.getSharedPreferences().edit();
editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, path);
editor.commit();
editor.apply();
setCacheLocation(path);
}
@ -234,9 +272,9 @@ public class SettingsFragment extends PreferenceFragment
new Consumer<Integer>() {
@Override
public void accept(Integer choice) {
SharedPreferences.Editor editor = resumeOnBluetoothDevice.getEditor();
SharedPreferences.Editor editor = resumeOnBluetoothDevice.getSharedPreferences().edit();
editor.putInt(Constants.PREFERENCES_KEY_RESUME_ON_BLUETOOTH_DEVICE, choice);
editor.commit();
editor.apply();
resumeOnBluetoothDevice.setSummary(bluetoothDevicePreferenceToString(choice));
}
});
@ -253,9 +291,9 @@ public class SettingsFragment extends PreferenceFragment
new Consumer<Integer>() {
@Override
public void accept(Integer choice) {
SharedPreferences.Editor editor = pauseOnBluetoothDevice.getEditor();
SharedPreferences.Editor editor = pauseOnBluetoothDevice.getSharedPreferences().edit();
editor.putInt(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE, choice);
editor.commit();
editor.apply();
pauseOnBluetoothDevice.setSummary(bluetoothDevicePreferenceToString(choice));
}
});
@ -330,7 +368,7 @@ public class SettingsFragment extends PreferenceFragment
@Override
public boolean onPreferenceChange(Preference preference, Object o) {
featureStorage.changeFeatureFlag(Feature.NEW_IMAGE_DOWNLOADER, (Boolean) o);
((SubsonicTabActivity) getActivity()).clearImageLoader();
imageLoader.getValue().clearImageLoader();
return true;
}
});
@ -355,9 +393,9 @@ public class SettingsFragment extends PreferenceFragment
private void setupGaplessControlSettingsV14() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
PreferenceCategory playbackControlSettings =
(PreferenceCategory) findPreference(Constants.PREFERENCES_KEY_PLAYBACK_CONTROL_SETTINGS);
findPreference(Constants.PREFERENCES_KEY_PLAYBACK_CONTROL_SETTINGS);
CheckBoxPreference gaplessPlaybackEnabled =
(CheckBoxPreference) findPreference(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK);
findPreference(Constants.PREFERENCES_KEY_GAPLESS_PLAYBACK);
if (gaplessPlaybackEnabled != null) {
gaplessPlaybackEnabled.setChecked(false);
@ -371,25 +409,19 @@ public class SettingsFragment extends PreferenceFragment
}
private void setupServersCategory() {
final Preference addServerPreference = new Preference(getActivity());
addServerPreference.setPersistent(false);
addServerPreference.setTitle(getResources().getString(R.string.settings_server_manage_servers));
addServerPreference.setEnabled(true);
// TODO new server management here
serversCategory.removeAll();
serversCategory.addPreference(addServerPreference);
addServerPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
final Intent intent = new Intent(getActivity(), ServerSelectorActivity.class);
intent.putExtra(SERVER_SELECTOR_MANAGE_MODE, true);
startActivityForResult(intent, 0);
Bundle bundle = new Bundle();
bundle.putBoolean(SERVER_SELECTOR_MANAGE_MODE, true);
Navigation.findNavController(getView()).navigate(R.id.settingsToServerSelector, bundle);
return true;
}
});
}
private void update() {
@ -433,20 +465,15 @@ public class SettingsFragment extends PreferenceFragment
debugLogToFile.setSummary("");
}
if (Util.getShouldUseId3Tags(getActivity())) showArtistPicture.setEnabled(true);
else showArtistPicture.setEnabled(false);
showArtistPicture.setEnabled(Util.getShouldUseId3Tags(getActivity()));
}
private static void setImageLoaderConcurrency(int concurrency) {
SubsonicTabActivity instance = SubsonicTabActivity.getInstance();
private void setImageLoaderConcurrency(int concurrency) {
ImageLoader imageLoaderInstance = imageLoader.getValue().getImageLoader();
if (instance != null) {
ImageLoader imageLoader = instance.getImageLoader();
if (imageLoader != null) {
imageLoader.stopImageLoader();
imageLoader.setConcurrency(concurrency);
}
if (imageLoaderInstance != null) {
imageLoaderInstance.stopImageLoader();
imageLoaderInstance.setConcurrency(concurrency);
}
}
@ -475,18 +502,14 @@ public class SettingsFragment extends PreferenceFragment
}
private void setBluetoothPreferences(boolean enabled) {
if (enabled) {
sendBluetoothAlbumArt.setEnabled(true);
} else {
sendBluetoothAlbumArt.setEnabled(false);
}
sendBluetoothAlbumArt.setEnabled(enabled);
}
private void setCacheLocation(String path) {
File dir = new File(path);
if (!FileUtil.ensureDirectoryExistsAndIsReadWritable(dir)) {
PermissionUtil.handlePermissionFailed(getActivity(), new PermissionUtil.PermissionRequestFinishedCallback() {
permissionUtil.getValue().handlePermissionFailed(new PermissionUtil.PermissionRequestFinishedCallback() {
@Override
public void onPermissionRequestFinished(boolean hasPermission) {
String currentPath = settings.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION,

View File

@ -0,0 +1,331 @@
package org.moire.ultrasonic.fragment;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.service.OfflineException;
import org.moire.ultrasonic.subsonic.DownloadHandler;
import org.moire.ultrasonic.util.BackgroundTask;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.LoadingTask;
import org.moire.ultrasonic.util.FragmentBackgroundTask;
import org.moire.ultrasonic.util.TimeSpan;
import org.moire.ultrasonic.util.TimeSpanPicker;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.ShareAdapter;
import java.util.List;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Displays the shares in the media library
*/
public class SharesFragment extends Fragment {
private SwipeRefreshLayout refreshSharesListView;
private ListView sharesListView;
private View emptyTextView;
private ShareAdapter shareAdapter;
private final Lazy<DownloadHandler> downloadHandler = inject(DownloadHandler.class);
private CancellationToken cancellationToken;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Util.applyTheme(this.getContext());
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.select_share, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
cancellationToken = new CancellationToken();
refreshSharesListView = view.findViewById(R.id.select_share_refresh);
sharesListView = view.findViewById(R.id.select_share_list);
refreshSharesListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener()
{
@Override
public void onRefresh()
{
load(true);
}
});
emptyTextView = view.findViewById(R.id.select_share_empty);
sharesListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Share share = (Share) parent.getItemAtPosition(position);
if (share == null)
{
return;
}
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_SHARE_ID, share.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_SHARE_NAME, share.getName());
Navigation.findNavController(view).navigate(R.id.selectAlbumFragment, bundle);
}
});
registerForContextMenu(sharesListView);
FragmentTitle.Companion.setTitle(this, R.string.button_bar_shares);
load(false);
}
@Override
public void onDestroyView() {
cancellationToken.cancel();
super.onDestroyView();
}
private void load(final boolean refresh)
{
BackgroundTask<List<Share>> task = new FragmentBackgroundTask<List<Share>>(getActivity(), true, refreshSharesListView, cancellationToken)
{
@Override
protected List<Share> doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
return musicService.getShares(refresh, getContext());
}
@Override
protected void done(List<Share> result)
{
sharesListView.setAdapter(shareAdapter = new ShareAdapter(getContext(), result));
emptyTextView.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
};
task.execute();
}
@Override
public void onCreateContextMenu(@NotNull ContextMenu menu, @NotNull View view, ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.select_share_context, menu);
}
@Override
public boolean onContextItemSelected(MenuItem menuItem)
{
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
if (info == null) return false;
Share share = (Share) sharesListView.getItemAtPosition(info.position);
if (share == null || share.getId() == null) return false;
int itemId = menuItem.getItemId();
if (itemId == R.id.share_menu_pin) {
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), true, true, false, false, true, false, false);
} else if (itemId == R.id.share_menu_unpin) {
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, false, false, true, false, true);
} else if (itemId == R.id.share_menu_download) {
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, false, false, true, false, false);
} else if (itemId == R.id.share_menu_play_now) {
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, true, false, false, false, false);
} else if (itemId == R.id.share_menu_play_shuffled) {
downloadHandler.getValue().downloadShare(this, share.getId(), share.getName(), false, false, true, true, false, false, false);
} else if (itemId == R.id.share_menu_delete) {
deleteShare(share);
} else if (itemId == R.id.share_info) {
displayShareInfo(share);
} else if (itemId == R.id.share_update_info) {
updateShareInfo(share);
} else {
return super.onContextItemSelected(menuItem);
}
return true;
}
private void deleteShare(final Share share)
{
new AlertDialog.Builder(getContext()).setIcon(android.R.drawable.ic_dialog_alert).setTitle(R.string.common_confirm).setMessage(getResources().getString(R.string.delete_playlist, share.getName())).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(getActivity(), refreshSharesListView, cancellationToken)
{
@Override
protected Void doInBackground() throws Throwable
{
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
musicService.deleteShare(share.getId(), getContext());
return null;
}
@Override
protected void done(Void result)
{
shareAdapter.remove(share);
shareAdapter.notifyDataSetChanged();
Util.toast(getContext(), getResources().getString(R.string.menu_deleted_share, share.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.menu_deleted_share_error, share.getName()), getErrorMessage(error));
Util.toast(getContext(), msg, false);
}
}.execute();
}
}).setNegativeButton(R.string.common_cancel, null).show();
}
private void displayShareInfo(final Share share)
{
final TextView textView = new TextView(getContext());
textView.setPadding(5, 5, 5, 5);
final Spannable message = new SpannableString("Owner: " + share.getUsername() +
"\nComments: " + ((share.getDescription() == null) ? "" : share.getDescription()) +
"\nURL: " + share.getUrl() +
"\nEntry Count: " + share.getEntries().size() +
"\nVisit Count: " + share.getVisitCount() +
((share.getCreated() == null) ? "" : ("\nCreation Date: " + share.getCreated().replace('T', ' '))) +
((share.getLastVisited() == null) ? "" : ("\nLast Visited Date: " + share.getLastVisited().replace('T', ' '))) +
((share.getExpires() == null) ? "" : ("\nExpiration Date: " + share.getExpires().replace('T', ' '))));
Linkify.addLinks(message, Linkify.WEB_URLS);
textView.setText(message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(getContext()).setTitle("Share Details").setCancelable(true).setIcon(android.R.drawable.ic_dialog_info).setView(textView).show();
}
private void updateShareInfo(final Share share)
{
View dialogView = getLayoutInflater().inflate(R.layout.share_details, null);
if (dialogView == null)
{
return;
}
final EditText shareDescription = dialogView.findViewById(R.id.share_description);
final TimeSpanPicker timeSpanPicker = dialogView.findViewById(R.id.date_picker);
shareDescription.setText(share.getDescription());
CheckBox hideDialogCheckBox = dialogView.findViewById(R.id.hide_dialog);
CheckBox saveAsDefaultsCheckBox = dialogView.findViewById(R.id.save_as_defaults);
CheckBox noExpirationCheckBox = dialogView.findViewById(R.id.timeSpanDisableCheckBox);
noExpirationCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener()
{
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b)
{
timeSpanPicker.setEnabled(!b);
}
});
noExpirationCheckBox.setChecked(true);
timeSpanPicker.setTimeSpanDisableText(getResources().getText(R.string.no_expiration));
hideDialogCheckBox.setVisibility(View.GONE);
saveAsDefaultsCheckBox.setVisibility(View.GONE);
AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
alertDialog.setIcon(android.R.drawable.ic_dialog_alert);
alertDialog.setTitle(R.string.playlist_update_info);
alertDialog.setView(dialogView);
alertDialog.setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
new LoadingTask<Void>(getActivity(), refreshSharesListView, cancellationToken)
{
@Override
protected Void doInBackground() throws Throwable
{
long millis = timeSpanPicker.getTimeSpan().getTotalMilliseconds();
if (millis > 0)
{
millis = TimeSpan.getCurrentTime().add(millis).getTotalMilliseconds();
}
Editable shareDescriptionText = shareDescription.getText();
String description = shareDescriptionText != null ? shareDescriptionText.toString() : null;
MusicService musicService = MusicServiceFactory.getMusicService(getContext());
musicService.updateShare(share.getId(), description, millis, getContext());
return null;
}
@Override
protected void done(Void result)
{
load(true);
Util.toast(getContext(), getResources().getString(R.string.playlist_updated_info, share.getName()));
}
@Override
protected void error(Throwable error)
{
String msg;
msg = error instanceof OfflineException || error instanceof ApiNotSupportedException ? getErrorMessage(error) : String.format("%s %s", getResources().getString(R.string.playlist_updated_info_error, share.getName()), getErrorMessage(error));
Util.toast(getContext(), msg, false);
}
}.execute();
}
});
alertDialog.setNegativeButton(R.string.common_cancel, null);
alertDialog.show();
}
}

View File

@ -14,14 +14,16 @@ import android.view.KeyEvent;
import android.widget.RemoteViews;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.MainActivity;
import org.moire.ultrasonic.activity.NavigationActivity;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
/**
* Widget Provider for the Ultrasonic Widgets
*/
public class UltrasonicAppWidgetProvider extends AppWidgetProvider
{
protected int layoutId;
@ -41,6 +43,8 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider
final Resources res = context.getResources();
final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId);
views.setTextViewText(R.id.title, null);
views.setTextViewText(R.id.album, null);
views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text));
linkButtons(context, views, false);
@ -72,7 +76,7 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider
{
if (hasInstances(context))
{
performUpdate(context, currentSong, null, playing, setAlbum);
performUpdate(context, currentSong, playing, setAlbum);
}
}
@ -95,7 +99,7 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider
/**
* Update all active widget instances by pushing changes
*/
private void performUpdate(Context context, MusicDirectory.Entry currentSong, int[] appWidgetIds, boolean playing, boolean setAlbum)
private void performUpdate(Context context, MusicDirectory.Entry currentSong, boolean playing, boolean setAlbum)
{
final Resources res = context.getResources();
final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId);
@ -176,20 +180,18 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider
// Link actions buttons to intents
linkButtons(context, views, currentSong != null);
pushUpdate(context, appWidgetIds, views);
pushUpdate(context, null, views);
}
/**
* Link up various button actions using {@link PendingIntent}.
*
* @param playerActive True if player is active in background, which means
* widget click will launch {@link DownloadActivity},
* otherwise we launch {@link MainActivity}.
*/
private static void linkButtons(Context context, RemoteViews views, boolean playerActive)
{
Intent intent = new Intent(context, NavigationActivity.class).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
if (playerActive)
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true);
Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class);
intent.setAction("android.intent.action.MAIN");
intent.addCategory("android.intent.category.LAUNCHER");
PendingIntent pendingIntent = PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT);
@ -198,21 +200,18 @@ public class UltrasonicAppWidgetProvider extends AppWidgetProvider
// Emulate media button clicks.
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
//intent.setPackage(context.getPackageName());
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0);
views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
//intent.setPackage(context.getPackageName());
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0);
views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
//intent.setPackage(context.getPackageName());
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0);

View File

@ -39,7 +39,6 @@ import org.moire.ultrasonic.domain.UserInfo;
import org.moire.ultrasonic.util.CancellableTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.LRUCache;
import org.moire.ultrasonic.util.ProgressListener;
import org.moire.ultrasonic.util.TimeLimitedCache;
import org.moire.ultrasonic.util.Util;
@ -59,7 +58,7 @@ import static org.koin.java.KoinJavaComponent.inject;
*/
public class CachedMusicService implements MusicService
{
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private static final int MUSIC_DIR_CACHE_SIZE = 100;
@ -68,49 +67,49 @@ public class CachedMusicService implements MusicService
private final LRUCache<String, TimeLimitedCache<MusicDirectory>> cachedArtist;
private final LRUCache<String, TimeLimitedCache<MusicDirectory>> cachedAlbum;
private final LRUCache<String, TimeLimitedCache<UserInfo>> cachedUserInfo;
private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS);
private final TimeLimitedCache<Indexes> cachedIndexes = new TimeLimitedCache<Indexes>(60 * 60, TimeUnit.SECONDS);
private final TimeLimitedCache<Indexes> cachedArtists = new TimeLimitedCache<Indexes>(60 * 60, TimeUnit.SECONDS);
private final TimeLimitedCache<List<Playlist>> cachedPlaylists = new TimeLimitedCache<List<Playlist>>(3600, TimeUnit.SECONDS);
private final TimeLimitedCache<List<PodcastsChannel>> cachedPodcastsChannels = new TimeLimitedCache<List<PodcastsChannel>>(3600, TimeUnit.SECONDS);
private final TimeLimitedCache<List<MusicFolder>> cachedMusicFolders = new TimeLimitedCache<List<MusicFolder>>(10 * 3600, TimeUnit.SECONDS);
private final TimeLimitedCache<List<Genre>> cachedGenres = new TimeLimitedCache<List<Genre>>(10 * 3600, TimeUnit.SECONDS);
private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<>(120, TimeUnit.SECONDS);
private final TimeLimitedCache<Indexes> cachedIndexes = new TimeLimitedCache<>(60 * 60, TimeUnit.SECONDS);
private final TimeLimitedCache<Indexes> cachedArtists = new TimeLimitedCache<>(60 * 60, TimeUnit.SECONDS);
private final TimeLimitedCache<List<Playlist>> cachedPlaylists = new TimeLimitedCache<>(3600, TimeUnit.SECONDS);
private final TimeLimitedCache<List<PodcastsChannel>> cachedPodcastsChannels = new TimeLimitedCache<>(3600, TimeUnit.SECONDS);
private final TimeLimitedCache<List<MusicFolder>> cachedMusicFolders = new TimeLimitedCache<>(10 * 3600, TimeUnit.SECONDS);
private final TimeLimitedCache<List<Genre>> cachedGenres = new TimeLimitedCache<>(10 * 3600, TimeUnit.SECONDS);
private String restUrl;
public CachedMusicService(MusicService musicService)
{
this.musicService = musicService;
cachedMusicDirectories = new LRUCache<String, TimeLimitedCache<MusicDirectory>>(MUSIC_DIR_CACHE_SIZE);
cachedArtist = new LRUCache<String, TimeLimitedCache<MusicDirectory>>(MUSIC_DIR_CACHE_SIZE);
cachedAlbum = new LRUCache<String, TimeLimitedCache<MusicDirectory>>(MUSIC_DIR_CACHE_SIZE);
cachedUserInfo = new LRUCache<String, TimeLimitedCache<UserInfo>>(MUSIC_DIR_CACHE_SIZE);
cachedMusicDirectories = new LRUCache<>(MUSIC_DIR_CACHE_SIZE);
cachedArtist = new LRUCache<>(MUSIC_DIR_CACHE_SIZE);
cachedAlbum = new LRUCache<>(MUSIC_DIR_CACHE_SIZE);
cachedUserInfo = new LRUCache<>(MUSIC_DIR_CACHE_SIZE);
}
@Override
public void ping(Context context, ProgressListener progressListener) throws Exception
public void ping(Context context) throws Exception
{
checkSettingsChanged(context);
musicService.ping(context, progressListener);
checkSettingsChanged();
musicService.ping(context);
}
@Override
public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception
public boolean isLicenseValid(Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
Boolean result = cachedLicenseValid.get();
if (result == null)
{
result = musicService.isLicenseValid(context, progressListener);
result = musicService.isLicenseValid(context);
cachedLicenseValid.set(result, result ? 30L * 60L : 2L * 60L, TimeUnit.SECONDS);
}
return result;
}
@Override
public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception
public List<MusicFolder> getMusicFolders(boolean refresh, Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
if (refresh)
{
cachedMusicFolders.clear();
@ -118,16 +117,16 @@ public class CachedMusicService implements MusicService
List<MusicFolder> result = cachedMusicFolders.get();
if (result == null)
{
result = musicService.getMusicFolders(refresh, context, progressListener);
result = musicService.getMusicFolders(refresh, context);
cachedMusicFolders.set(result);
}
return result;
}
@Override
public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception
public Indexes getIndexes(String musicFolderId, boolean refresh, Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
if (refresh)
{
cachedIndexes.clear();
@ -137,16 +136,16 @@ public class CachedMusicService implements MusicService
Indexes result = cachedIndexes.get();
if (result == null)
{
result = musicService.getIndexes(musicFolderId, refresh, context, progressListener);
result = musicService.getIndexes(musicFolderId, refresh, context);
cachedIndexes.set(result);
}
return result;
}
@Override
public Indexes getArtists(boolean refresh, Context context, ProgressListener progressListener) throws Exception
public Indexes getArtists(boolean refresh, Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
if (refresh)
{
cachedArtists.clear();
@ -154,24 +153,24 @@ public class CachedMusicService implements MusicService
Indexes result = cachedArtists.get();
if (result == null)
{
result = musicService.getArtists(refresh, context, progressListener);
result = musicService.getArtists(refresh, context);
cachedArtists.set(result);
}
return result;
}
@Override
public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedMusicDirectories.get(id);
MusicDirectory dir = cache == null ? null : cache.get();
if (dir == null)
{
dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener);
cache = new TimeLimitedCache<MusicDirectory>(Util.getDirectoryCacheTime(context), TimeUnit.SECONDS);
dir = musicService.getMusicDirectory(id, name, refresh, context);
cache = new TimeLimitedCache<>(Util.getDirectoryCacheTime(context), TimeUnit.SECONDS);
cache.set(dir);
cachedMusicDirectories.put(id, cache);
}
@ -179,15 +178,15 @@ public class CachedMusicService implements MusicService
}
@Override
public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getArtist(String id, String name, boolean refresh, Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedArtist.get(id);
MusicDirectory dir = cache == null ? null : cache.get();
if (dir == null)
{
dir = musicService.getArtist(id, name, refresh, context, progressListener);
cache = new TimeLimitedCache<MusicDirectory>(Util.getDirectoryCacheTime(context), TimeUnit.SECONDS);
dir = musicService.getArtist(id, name, refresh, context);
cache = new TimeLimitedCache<>(Util.getDirectoryCacheTime(context), TimeUnit.SECONDS);
cache.set(dir);
cachedArtist.put(id, cache);
}
@ -195,15 +194,15 @@ public class CachedMusicService implements MusicService
}
@Override
public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedAlbum.get(id);
MusicDirectory dir = cache == null ? null : cache.get();
if (dir == null)
{
dir = musicService.getAlbum(id, name, refresh, context, progressListener);
cache = new TimeLimitedCache<MusicDirectory>(Util.getDirectoryCacheTime(context), TimeUnit.SECONDS);
dir = musicService.getAlbum(id, name, refresh, context);
cache = new TimeLimitedCache<>(Util.getDirectoryCacheTime(context), TimeUnit.SECONDS);
cache.set(dir);
cachedAlbum.put(id, cache);
}
@ -211,113 +210,113 @@ public class CachedMusicService implements MusicService
}
@Override
public SearchResult search(SearchCriteria criteria, Context context, ProgressListener progressListener) throws Exception
public SearchResult search(SearchCriteria criteria, Context context) throws Exception
{
return musicService.search(criteria, context, progressListener);
return musicService.search(criteria, context);
}
@Override
public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getPlaylist(String id, String name, Context context) throws Exception
{
return musicService.getPlaylist(id, name, context, progressListener);
return musicService.getPlaylist(id, name, context);
}
@Override
public List<PodcastsChannel> getPodcastsChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkSettingsChanged(context);
public List<PodcastsChannel> getPodcastsChannels(boolean refresh, Context context) throws Exception {
checkSettingsChanged();
List<PodcastsChannel> result = refresh ? null : cachedPodcastsChannels.get();
if (result == null)
{
result = musicService.getPodcastsChannels(refresh, context, progressListener);
result = musicService.getPodcastsChannels(refresh, context);
cachedPodcastsChannels.set(result);
}
return result;
}
@Override
public MusicDirectory getPodcastEpisodes(String podcastChannelId, Context context, ProgressListener progressListener) throws Exception {
return musicService.getPodcastEpisodes(podcastChannelId,context,progressListener);
public MusicDirectory getPodcastEpisodes(String podcastChannelId, Context context) throws Exception {
return musicService.getPodcastEpisodes(podcastChannelId,context);
}
@Override
public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception
public List<Playlist> getPlaylists(boolean refresh, Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
List<Playlist> result = refresh ? null : cachedPlaylists.get();
if (result == null)
{
result = musicService.getPlaylists(refresh, context, progressListener);
result = musicService.getPlaylists(refresh, context);
cachedPlaylists.set(result);
}
return result;
}
@Override
public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception
public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context) throws Exception
{
cachedPlaylists.clear();
musicService.createPlaylist(id, name, entries, context, progressListener);
musicService.createPlaylist(id, name, entries, context);
}
@Override
public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception
public void deletePlaylist(String id, Context context) throws Exception
{
musicService.deletePlaylist(id, context, progressListener);
musicService.deletePlaylist(id, context);
}
@Override
public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception
public void updatePlaylist(String id, String name, String comment, boolean pub, Context context) throws Exception
{
musicService.updatePlaylist(id, name, comment, pub, context, progressListener);
musicService.updatePlaylist(id, name, comment, pub, context);
}
@Override
public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception
public Lyrics getLyrics(String artist, String title, Context context) throws Exception
{
return musicService.getLyrics(artist, title, context, progressListener);
return musicService.getLyrics(artist, title, context);
}
@Override
public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception
public void scrobble(String id, boolean submission, Context context) throws Exception
{
musicService.scrobble(id, submission, context, progressListener);
musicService.scrobble(id, submission, context);
}
@Override
public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getAlbumList(String type, int size, int offset, Context context) throws Exception
{
return musicService.getAlbumList(type, size, offset, context, progressListener);
return musicService.getAlbumList(type, size, offset, context);
}
@Override
public MusicDirectory getAlbumList2(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getAlbumList2(String type, int size, int offset, Context context) throws Exception
{
return musicService.getAlbumList2(type, size, offset, context, progressListener);
return musicService.getAlbumList2(type, size, offset, context);
}
@Override
public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getRandomSongs(int size, Context context) throws Exception
{
return musicService.getRandomSongs(size, context, progressListener);
return musicService.getRandomSongs(size, context);
}
@Override
public SearchResult getStarred(Context context, ProgressListener progressListener) throws Exception
public SearchResult getStarred(Context context) throws Exception
{
return musicService.getStarred(context, progressListener);
return musicService.getStarred(context);
}
@Override
public SearchResult getStarred2(Context context, ProgressListener progressListener) throws Exception
public SearchResult getStarred2(Context context) throws Exception
{
return musicService.getStarred2(context, progressListener);
return musicService.getStarred2(context);
}
@Override
public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception
public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality) throws Exception
{
return musicService.getCoverArt(context, entry, size, saveToFile, highQuality, progressListener);
return musicService.getCoverArt(context, entry, size, saveToFile, highQuality);
}
@Override
@ -333,42 +332,42 @@ public class CachedMusicService implements MusicService
}
@Override
public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context) throws Exception
{
return musicService.updateJukeboxPlaylist(ids, context, progressListener);
return musicService.updateJukeboxPlaylist(ids, context);
}
@Override
public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context) throws Exception
{
return musicService.skipJukebox(index, offsetSeconds, context, progressListener);
return musicService.skipJukebox(index, offsetSeconds, context);
}
@Override
public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus stopJukebox(Context context) throws Exception
{
return musicService.stopJukebox(context, progressListener);
return musicService.stopJukebox(context);
}
@Override
public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus startJukebox(Context context) throws Exception
{
return musicService.startJukebox(context, progressListener);
return musicService.startJukebox(context);
}
@Override
public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus getJukeboxStatus(Context context) throws Exception
{
return musicService.getJukeboxStatus(context, progressListener);
return musicService.getJukeboxStatus(context);
}
@Override
public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus setJukeboxGain(float gain, Context context) throws Exception
{
return musicService.setJukeboxGain(gain, context, progressListener);
return musicService.setJukeboxGain(gain, context);
}
private void checkSettingsChanged(Context context)
private void checkSettingsChanged()
{
String newUrl = activeServerProvider.getValue().getRestUrl(null);
if (!Util.equals(newUrl, restUrl))
@ -387,27 +386,27 @@ public class CachedMusicService implements MusicService
}
@Override
public void star(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception
public void star(String id, String albumId, String artistId, Context context) throws Exception
{
musicService.star(id, albumId, artistId, context, progressListener);
musicService.star(id, albumId, artistId, context);
}
@Override
public void unstar(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception
public void unstar(String id, String albumId, String artistId, Context context) throws Exception
{
musicService.unstar(id, albumId, artistId, context, progressListener);
musicService.unstar(id, albumId, artistId, context);
}
@Override
public void setRating(String id, int rating, Context context, ProgressListener progressListener) throws Exception
public void setRating(String id, int rating, Context context) throws Exception
{
musicService.setRating(id, rating, context, progressListener);
musicService.setRating(id, rating, context);
}
@Override
public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception
public List<Genre> getGenres(boolean refresh, Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
if (refresh)
{
cachedGenres.clear();
@ -416,7 +415,7 @@ public class CachedMusicService implements MusicService
if (result == null)
{
result = musicService.getGenres(refresh, context, progressListener);
result = musicService.getGenres(refresh, context);
cachedGenres.set(result);
}
@ -433,59 +432,59 @@ public class CachedMusicService implements MusicService
}
@Override
public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context) throws Exception
{
return musicService.getSongsByGenre(genre, count, offset, context, progressListener);
return musicService.getSongsByGenre(genre, count, offset, context);
}
@Override
public List<Share> getShares(boolean refresh, Context context, ProgressListener progressListener) throws Exception
public List<Share> getShares(boolean refresh, Context context) throws Exception
{
return musicService.getShares(refresh, context, progressListener);
return musicService.getShares(refresh, context);
}
@Override
public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception
public List<ChatMessage> getChatMessages(Long since, Context context) throws Exception
{
return musicService.getChatMessages(since, context, progressListener);
return musicService.getChatMessages(since, context);
}
@Override
public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception
public void addChatMessage(String message, Context context) throws Exception
{
musicService.addChatMessage(message, context, progressListener);
musicService.addChatMessage(message, context);
}
@Override
public List<Bookmark> getBookmarks(Context context, ProgressListener progressListener) throws Exception
public List<Bookmark> getBookmarks(Context context) throws Exception
{
return musicService.getBookmarks(context, progressListener);
return musicService.getBookmarks(context);
}
@Override
public void deleteBookmark(String id, Context context, ProgressListener progressListener) throws Exception
public void deleteBookmark(String id, Context context) throws Exception
{
musicService.deleteBookmark(id, context, progressListener);
musicService.deleteBookmark(id, context);
}
@Override
public void createBookmark(String id, int position, Context context, ProgressListener progressListener) throws Exception
public void createBookmark(String id, int position, Context context) throws Exception
{
musicService.createBookmark(id, position, context, progressListener);
musicService.createBookmark(id, position, context);
}
@Override
public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getVideos(boolean refresh, Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedMusicDirectories.get(Constants.INTENT_EXTRA_NAME_VIDEOS);
MusicDirectory dir = cache == null ? null : cache.get();
if (dir == null)
{
dir = musicService.getVideos(refresh, context, progressListener);
cache = new TimeLimitedCache<MusicDirectory>(Util.getDirectoryCacheTime(context), TimeUnit.SECONDS);
dir = musicService.getVideos(refresh, context);
cache = new TimeLimitedCache<>(Util.getDirectoryCacheTime(context), TimeUnit.SECONDS);
cache.set(dir);
cachedMusicDirectories.put(Constants.INTENT_EXTRA_NAME_VIDEOS, cache);
}
@ -494,9 +493,9 @@ public class CachedMusicService implements MusicService
}
@Override
public UserInfo getUser(String username, Context context, ProgressListener progressListener) throws Exception
public UserInfo getUser(String username, Context context) throws Exception
{
checkSettingsChanged(context);
checkSettingsChanged();
TimeLimitedCache<UserInfo> cache = cachedUserInfo.get(username);
@ -504,8 +503,8 @@ public class CachedMusicService implements MusicService
if (userInfo == null)
{
userInfo = musicService.getUser(username, context, progressListener);
cache = new TimeLimitedCache<UserInfo>(Util.getDirectoryCacheTime(context), TimeUnit.SECONDS);
userInfo = musicService.getUser(username, context);
cache = new TimeLimitedCache<>(Util.getDirectoryCacheTime(context), TimeUnit.SECONDS);
cache.set(userInfo);
cachedUserInfo.put(username, cache);
}
@ -514,27 +513,26 @@ public class CachedMusicService implements MusicService
}
@Override
public List<Share> createShare(List<String> ids, String description, Long expires, Context context, ProgressListener progressListener) throws Exception
public List<Share> createShare(List<String> ids, String description, Long expires, Context context) throws Exception
{
return musicService.createShare(ids, description, expires, context, progressListener);
return musicService.createShare(ids, description, expires, context);
}
@Override
public void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception
public void deleteShare(String id, Context context) throws Exception
{
musicService.deleteShare(id, context, progressListener);
musicService.deleteShare(id, context);
}
@Override
public void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception
public void updateShare(String id, String description, Long expires, Context context) throws Exception
{
musicService.updateShare(id, description, expires, context, progressListener);
musicService.updateShare(id, description, expires, context);
}
@Override
public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception
public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality) throws Exception
{
return musicService.getAvatar(context, username, size, saveToFile, highQuality, progressListener);
return musicService.getAvatar(context, username, size, saveToFile, highQuality);
}
}

View File

@ -67,7 +67,7 @@ public class DownloadFile
private volatile boolean saveWhenDone;
private volatile boolean completeWhenDone;
private Lazy<Downloader> downloader = inject(Downloader.class);
private final Lazy<Downloader> downloader = inject(Downloader.class);
public DownloadFile(Context context, MusicDirectory.Entry song, boolean save)
{
@ -196,7 +196,9 @@ public class DownloadFile
{
if (saveFile.exists())
{
saveFile.renameTo(completeFile);
if (!saveFile.renameTo(completeFile)){
Timber.w("Renaming file failed. Original file: %s; Rename to: %s", saveFile.getName(), completeFile.getName());
}
}
}
@ -456,13 +458,13 @@ public class DownloadFile
return String.format("DownloadTask (%s)", song);
}
private void downloadAndSaveCoverArt(MusicService musicService) throws Exception
private void downloadAndSaveCoverArt(MusicService musicService)
{
try
{
if (!TextUtils.isEmpty(song.getCoverArt())) {
int size = Util.getMinDisplayMetric(context);
musicService.getCoverArt(context, song, size, true, true, null);
musicService.getCoverArt(context, song, size, true, true);
}
}
catch (Exception x)

View File

@ -67,13 +67,13 @@ public class JukeboxMediaPlayer
private JukeboxStatus jukeboxStatus;
private float gain = 0.5f;
private VolumeToast volumeToast;
private AtomicBoolean running = new AtomicBoolean();
private final AtomicBoolean running = new AtomicBoolean();
private Thread serviceThread;
private boolean enabled = false;
private Context context;
private final Context context;
// TODO: These create circular references, try to refactor
private Lazy<MediaPlayerControllerImpl> mediaPlayerControllerLazy = inject(MediaPlayerControllerImpl.class);
private final Lazy<MediaPlayerControllerImpl> mediaPlayerControllerLazy = inject(MediaPlayerControllerImpl.class);
private final Downloader downloader;
// TODO: Report warning if queue fills up.
@ -397,7 +397,7 @@ public class JukeboxMediaPlayer
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().getJukeboxStatus(context, null);
return getMusicService().getJukeboxStatus(context);
}
}
@ -413,7 +413,7 @@ public class JukeboxMediaPlayer
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().updateJukeboxPlaylist(ids, context, null);
return getMusicService().updateJukeboxPlaylist(ids, context);
}
}
@ -431,7 +431,7 @@ public class JukeboxMediaPlayer
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().skipJukebox(index, offsetSeconds, context, null);
return getMusicService().skipJukebox(index, offsetSeconds, context);
}
}
@ -440,7 +440,7 @@ public class JukeboxMediaPlayer
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().stopJukebox(context, null);
return getMusicService().stopJukebox(context);
}
}
@ -449,7 +449,7 @@ public class JukeboxMediaPlayer
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().startJukebox(context, null);
return getMusicService().startJukebox(context);
}
}
@ -466,7 +466,7 @@ public class JukeboxMediaPlayer
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().setJukeboxGain(gain, context, null);
return getMusicService().setJukeboxGain(gain, context);
}
}

View File

@ -19,12 +19,12 @@ import timber.log.Timber;
import android.widget.SeekBar;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.audiofx.EqualizerController;
import org.moire.ultrasonic.audiofx.VisualizerController;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.fragment.PlayerFragment;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.util.CancellableTask;
import org.moire.ultrasonic.util.Constants;
@ -598,7 +598,7 @@ public class LocalMediaPlayer
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent)
{
SeekBar progressBar = DownloadActivity.getProgressBar();
SeekBar progressBar = PlayerFragment.getProgressBar();
MusicDirectory.Entry song = downloadFile.getSong();
if (percent == 100)
@ -627,12 +627,12 @@ public class LocalMediaPlayer
setPlayerState(PREPARED);
SeekBar progressBar = DownloadActivity.getProgressBar();
SeekBar progressBar = PlayerFragment.getProgressBar();
if (progressBar != null && downloadFile.isWorkDone())
{
// Populate seek bar secondary progress if we have a complete file for consistency
DownloadActivity.getProgressBar().setSecondaryProgress(100 * progressBar.getMax());
PlayerFragment.getProgressBar().setSecondaryProgress(100 * progressBar.getMax());
}
synchronized (LocalMediaPlayer.this)

View File

@ -23,8 +23,6 @@ import android.content.Intent;
import timber.log.Timber;
import org.koin.java.KoinJavaComponent;
import org.moire.ultrasonic.audiofx.EqualizerController;
import org.moire.ultrasonic.audiofx.VisualizerController;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
@ -60,9 +58,10 @@ public class MediaPlayerControllerImpl implements MediaPlayerController
private boolean showVisualization;
private boolean autoPlayStart;
private Context context;
private Lazy<JukeboxMediaPlayer> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private final Context context;
private final Lazy<JukeboxMediaPlayer> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private final DownloadQueueSerializer downloadQueueSerializer;
private final ExternalStorageMonitor externalStorageMonitor;
private final Downloader downloader;
@ -522,7 +521,7 @@ public class MediaPlayerControllerImpl implements MediaPlayerController
try
{
String username = activeServerProvider.getValue().getActiveServer().getUserName();
UserInfo user = MusicServiceFactory.getMusicService(context).getUser(username, context, null);
UserInfo user = MusicServiceFactory.getMusicService(context).getUser(username, context);
return user.getJukeboxRole();
}
catch (Exception e)
@ -595,7 +594,7 @@ public class MediaPlayerControllerImpl implements MediaPlayerController
{
try
{
MusicServiceFactory.getMusicService(context).setRating(song.getId(), rating, context, null);
MusicServiceFactory.getMusicService(context).setRating(song.getId(), rating, context);
}
catch (Exception e)
{

View File

@ -20,8 +20,7 @@ import androidx.core.app.NotificationManagerCompat;
import org.koin.java.KoinJavaComponent;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.activity.NavigationActivity;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.domain.RepeatMode;
@ -31,7 +30,9 @@ import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X1;
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X2;
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3;
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.NowPlayingEventDistributor;
import org.moire.ultrasonic.util.ShufflePlayBuffer;
import org.moire.ultrasonic.util.SimpleServiceBinder;
import org.moire.ultrasonic.util.Util;
@ -64,10 +65,11 @@ public class MediaPlayerService extends Service
private final Scrobbler scrobbler = new Scrobbler();
public Lazy<JukeboxMediaPlayer> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
private Lazy<DownloadQueueSerializer> downloadQueueSerializerLazy = inject(DownloadQueueSerializer.class);
private Lazy<ShufflePlayBuffer> shufflePlayBufferLazy = inject(ShufflePlayBuffer.class);
private Lazy<Downloader> downloaderLazy = inject(Downloader.class);
private Lazy<LocalMediaPlayer> localMediaPlayerLazy = inject(LocalMediaPlayer.class);
private final Lazy<DownloadQueueSerializer> downloadQueueSerializerLazy = inject(DownloadQueueSerializer.class);
private final Lazy<ShufflePlayBuffer> shufflePlayBufferLazy = inject(ShufflePlayBuffer.class);
private final Lazy<Downloader> downloaderLazy = inject(Downloader.class);
private final Lazy<LocalMediaPlayer> localMediaPlayerLazy = inject(LocalMediaPlayer.class);
private final Lazy<NowPlayingEventDistributor> nowPlayingEventDistributor = inject(NowPlayingEventDistributor.class);
private LocalMediaPlayer localMediaPlayer;
private Downloader downloader;
private ShufflePlayBuffer shufflePlayBuffer;
@ -280,21 +282,14 @@ public class MediaPlayerService extends Service
UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false);
UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false);
SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance();
if (currentPlaying != null)
{
updateNotification(localMediaPlayer.playerState, currentPlaying);
if (tabInstance != null) {
tabInstance.showNowPlaying();
}
nowPlayingEventDistributor.getValue().raiseShowNowPlayingEvent();
}
else
{
if (tabInstance != null)
{
tabInstance.hideNowPlaying();
}
nowPlayingEventDistributor.getValue().raiseHideNowPlayingEvent();
stopForeground(true);
localMediaPlayer.clearRemoteControl();
isInForeground = false;
@ -499,7 +494,6 @@ public class MediaPlayerService extends Service
UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, true);
UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false);
UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(MediaPlayerService.this, song, playerState == PlayerState.STARTED, false);
SubsonicTabActivity tabInstance = SubsonicTabActivity.getInstance();
if (show)
{
@ -507,18 +501,12 @@ public class MediaPlayerService extends Service
if (playerState == PlayerState.STARTED || playerState == PlayerState.PAUSED)
{
updateNotification(playerState, currentPlaying);
if (tabInstance != null)
{
tabInstance.showNowPlaying();
}
nowPlayingEventDistributor.getValue().raiseShowNowPlayingEvent();
}
}
else
{
if (tabInstance != null)
{
tabInstance.hideNowPlaying();
}
nowPlayingEventDistributor.getValue().raiseHideNowPlayingEvent();
stopForeground(true);
localMediaPlayer.clearRemoteControl();
isInForeground = false;
@ -553,7 +541,7 @@ public class MediaPlayerService extends Service
MusicService musicService = MusicServiceFactory.getMusicService(MediaPlayerService.this);
try
{
musicService.deleteBookmark(song.getId(), MediaPlayerService.this, null);
musicService.deleteBookmark(song.getId(), MediaPlayerService.this);
}
catch (Exception ignored)
{
@ -650,8 +638,10 @@ public class MediaPlayerService extends Service
notificationBuilder.setContent(contentView);
Intent notificationIntent = new Intent(this, DownloadActivity.class);
notificationBuilder.setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent, 0));
Intent notificationIntent = new Intent(this, NavigationActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
notificationIntent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true);
notificationBuilder.setContentIntent(PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT));
if (playerState == PlayerState.PAUSED || playerState == PlayerState.IDLE) {
contentView.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark);

View File

@ -36,7 +36,6 @@ import org.moire.ultrasonic.domain.SearchResult;
import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.domain.UserInfo;
import org.moire.ultrasonic.util.CancellableTask;
import org.moire.ultrasonic.util.ProgressListener;
import java.io.InputStream;
import java.util.List;
@ -49,61 +48,61 @@ import kotlin.Pair;
public interface MusicService
{
void ping(Context context, ProgressListener progressListener) throws Exception;
void ping(Context context) throws Exception;
boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception;
boolean isLicenseValid(Context context) throws Exception;
List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
List<Genre> getGenres(boolean refresh, Context context) throws Exception;
void star(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception;
void star(String id, String albumId, String artistId, Context context) throws Exception;
void unstar(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception;
void unstar(String id, String albumId, String artistId, Context context) throws Exception;
void setRating(String id, int rating, Context context, ProgressListener progressListener) throws Exception;
void setRating(String id, int rating, Context context) throws Exception;
List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
List<MusicFolder> getMusicFolders(boolean refresh, Context context) throws Exception;
Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
Indexes getIndexes(String musicFolderId, boolean refresh, Context context) throws Exception;
Indexes getArtists(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
Indexes getArtists(boolean refresh, Context context) throws Exception;
MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context) throws Exception;
MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getArtist(String id, String name, boolean refresh, Context context) throws Exception;
MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getAlbum(String id, String name, boolean refresh, Context context) throws Exception;
SearchResult search(SearchCriteria criteria, Context context, ProgressListener progressListener) throws Exception;
SearchResult search(SearchCriteria criteria, Context context) throws Exception;
MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getPlaylist(String id, String name, Context context) throws Exception;
List<PodcastsChannel> getPodcastsChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
List<PodcastsChannel> getPodcastsChannels(boolean refresh, Context context) throws Exception;
List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
List<Playlist> getPlaylists(boolean refresh, Context context) throws Exception;
void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception;
void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context) throws Exception;
void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception;
void deletePlaylist(String id, Context context) throws Exception;
void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception;
void updatePlaylist(String id, String name, String comment, boolean pub, Context context) throws Exception;
Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception;
Lyrics getLyrics(String artist, String title, Context context) throws Exception;
void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception;
void scrobble(String id, boolean submission, Context context) throws Exception;
MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getAlbumList(String type, int size, int offset, Context context) throws Exception;
MusicDirectory getAlbumList2(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getAlbumList2(String type, int size, int offset, Context context) throws Exception;
MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getRandomSongs(int size, Context context) throws Exception;
MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context) throws Exception;
SearchResult getStarred(Context context, ProgressListener progressListener) throws Exception;
SearchResult getStarred(Context context) throws Exception;
SearchResult getStarred2(Context context, ProgressListener progressListener) throws Exception;
SearchResult getStarred2(Context context) throws Exception;
Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception;
Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality) throws Exception;
/**
* Return response {@link InputStream} and a {@link Boolean} that indicates if this response is
@ -111,43 +110,44 @@ public interface MusicService
*/
Pair<InputStream, Boolean> getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception;
@Deprecated String getVideoUrl(Context context, String id, boolean useFlash) throws Exception;
// TODO: Refactor and remove this call (see RestMusicService implementation)
String getVideoUrl(Context context, String id, boolean useFlash) throws Exception;
JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception;
JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context) throws Exception;
JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception;
JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context) throws Exception;
JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception;
JukeboxStatus stopJukebox(Context context) throws Exception;
JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception;
JukeboxStatus startJukebox(Context context) throws Exception;
JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception;
JukeboxStatus getJukeboxStatus(Context context) throws Exception;
JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception;
JukeboxStatus setJukeboxGain(float gain, Context context) throws Exception;
List<Share> getShares(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
List<Share> getShares(boolean refresh, Context context) throws Exception;
List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception;
List<ChatMessage> getChatMessages(Long since, Context context) throws Exception;
void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception;
void addChatMessage(String message, Context context) throws Exception;
List<Bookmark> getBookmarks(Context context, ProgressListener progressListener) throws Exception;
List<Bookmark> getBookmarks(Context context) throws Exception;
void deleteBookmark(String id, Context context, ProgressListener progressListener) throws Exception;
void deleteBookmark(String id, Context context) throws Exception;
void createBookmark(String id, int position, Context context, ProgressListener progressListener) throws Exception;
void createBookmark(String id, int position, Context context) throws Exception;
MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getVideos(boolean refresh, Context context) throws Exception;
UserInfo getUser(String username, Context context, ProgressListener progressListener) throws Exception;
UserInfo getUser(String username, Context context) throws Exception;
List<Share> createShare(List<String> ids, String description, Long expires, Context context, ProgressListener progressListener) throws Exception;
List<Share> createShare(List<String> ids, String description, Long expires, Context context) throws Exception;
void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception;
void deleteShare(String id, Context context) throws Exception;
void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception;
void updateShare(String id, String description, Long expires, Context context) throws Exception;
Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception;
Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality) throws Exception;
MusicDirectory getPodcastEpisodes(String podcastChannelId, Context context, ProgressListener progressListener) throws Exception;
MusicDirectory getPodcastEpisodes(String podcastChannelId, Context context) throws Exception;
}

View File

@ -44,7 +44,6 @@ import org.moire.ultrasonic.domain.UserInfo;
import org.moire.ultrasonic.util.CancellableTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.ProgressListener;
import org.moire.ultrasonic.util.Util;
import java.io.BufferedReader;
@ -76,12 +75,12 @@ import static org.koin.java.KoinJavaComponent.inject;
public class OfflineMusicService implements MusicService
{
private static final Pattern COMPILE = Pattern.compile(" ");
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
@Override
public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception
public Indexes getIndexes(String musicFolderId, boolean refresh, Context context)
{
List<Artist> artists = new ArrayList<Artist>();
List<Artist> artists = new ArrayList<>();
File root = FileUtil.getMusicDirectory(context);
for (File file : FileUtil.listFiles(root))
{
@ -144,13 +143,13 @@ public class OfflineMusicService implements MusicService
}
@Override
public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener)
public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context)
{
File dir = new File(id);
MusicDirectory result = new MusicDirectory();
result.setName(dir.getName());
Collection<String> names = new HashSet<String>();
Collection<String> names = new HashSet<>();
for (File file : FileUtil.listMediaFiles(dir))
{
@ -335,7 +334,7 @@ public class OfflineMusicService implements MusicService
}
@Override
public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener)
public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality)
{
try
{
@ -349,7 +348,7 @@ public class OfflineMusicService implements MusicService
}
@Override
public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener)
public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality)
{
try
{
@ -363,11 +362,11 @@ public class OfflineMusicService implements MusicService
}
@Override
public SearchResult search(SearchCriteria criteria, Context context, ProgressListener progressListener)
public SearchResult search(SearchCriteria criteria, Context context)
{
List<Artist> artists = new ArrayList<Artist>();
List<MusicDirectory.Entry> albums = new ArrayList<MusicDirectory.Entry>();
List<MusicDirectory.Entry> songs = new ArrayList<MusicDirectory.Entry>();
List<Artist> artists = new ArrayList<>();
List<MusicDirectory.Entry> albums = new ArrayList<>();
List<MusicDirectory.Entry> songs = new ArrayList<>();
File root = FileUtil.getMusicDirectory(context);
int closeness;
@ -507,9 +506,9 @@ public class OfflineMusicService implements MusicService
}
@Override
public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener)
public List<Playlist> getPlaylists(boolean refresh, Context context)
{
List<Playlist> playlists = new ArrayList<Playlist>();
List<Playlist> playlists = new ArrayList<>();
File root = FileUtil.getPlaylistDirectory(context);
String lastServer = null;
boolean removeServer = true;
@ -544,11 +543,13 @@ public class OfflineMusicService implements MusicService
// Delete legacy playlist files
try
{
folder.delete();
if (!folder.delete()) {
Timber.w("Failed to delete old playlist file: %s", folder.getName());
}
}
catch (Exception e)
{
Timber.w("Failed to delete old playlist file: %s", folder.getName());
Timber.w(e, "Failed to delete old playlist file: %s", folder.getName());
}
}
}
@ -564,7 +565,7 @@ public class OfflineMusicService implements MusicService
}
@Override
public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getPlaylist(String id, String name, Context context) throws Exception
{
Reader reader = null;
BufferedReader buffer = null;
@ -606,7 +607,7 @@ public class OfflineMusicService implements MusicService
}
@Override
public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception
public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context) throws Exception
{
File playlistFile = FileUtil.getPlaylistFile(context, activeServerProvider.getValue().getActiveServer().getName(), name);
FileWriter fw = new FileWriter(playlistFile);
@ -639,10 +640,10 @@ public class OfflineMusicService implements MusicService
@Override
public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener)
public MusicDirectory getRandomSongs(int size, Context context)
{
File root = FileUtil.getMusicDirectory(context);
List<File> children = new LinkedList<File>();
List<File> children = new LinkedList<>();
listFilesRecursively(root, children);
MusicDirectory result = new MusicDirectory();
@ -677,138 +678,138 @@ public class OfflineMusicService implements MusicService
}
@Override
public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception
public void deletePlaylist(String id, Context context) throws Exception
{
throw new OfflineException("Playlists not available in offline mode");
}
@Override
public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception
public void updatePlaylist(String id, String name, String comment, boolean pub, Context context) throws Exception
{
throw new OfflineException("Updating playlist not available in offline mode");
}
@Override
public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception
public Lyrics getLyrics(String artist, String title, Context context) throws Exception
{
throw new OfflineException("Lyrics not available in offline mode");
}
@Override
public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception
public void scrobble(String id, boolean submission, Context context) throws Exception
{
throw new OfflineException("Scrobbling not available in offline mode");
}
@Override
public MusicDirectory getAlbumList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getAlbumList(String type, int size, int offset, Context context) throws Exception
{
throw new OfflineException("Album lists not available in offline mode");
}
@Override
public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context) throws Exception
{
throw new OfflineException("Jukebox not available in offline mode");
}
@Override
public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus skipJukebox(int index, int offsetSeconds, Context context) throws Exception
{
throw new OfflineException("Jukebox not available in offline mode");
}
@Override
public JukeboxStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus stopJukebox(Context context) throws Exception
{
throw new OfflineException("Jukebox not available in offline mode");
}
@Override
public JukeboxStatus startJukebox(Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus startJukebox(Context context) throws Exception
{
throw new OfflineException("Jukebox not available in offline mode");
}
@Override
public JukeboxStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus getJukeboxStatus(Context context) throws Exception
{
throw new OfflineException("Jukebox not available in offline mode");
}
@Override
public JukeboxStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception
public JukeboxStatus setJukeboxGain(float gain, Context context) throws Exception
{
throw new OfflineException("Jukebox not available in offline mode");
}
@Override
public SearchResult getStarred(Context context, ProgressListener progressListener) throws Exception
public SearchResult getStarred(Context context) throws Exception
{
throw new OfflineException("Starred not available in offline mode");
}
@Override
public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context) throws Exception
{
throw new OfflineException("Getting Songs By Genre not available in offline mode");
}
@Override
public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception
public List<Genre> getGenres(boolean refresh, Context context) throws Exception
{
throw new OfflineException("Getting Genres not available in offline mode");
}
@Override
public UserInfo getUser(String username, Context context, ProgressListener progressListener) throws Exception
public UserInfo getUser(String username, Context context) throws Exception
{
throw new OfflineException("Getting user info not available in offline mode");
}
@Override
public List<Share> createShare(List<String> ids, String description, Long expires, Context context, ProgressListener progressListener) throws Exception
public List<Share> createShare(List<String> ids, String description, Long expires, Context context) throws Exception
{
throw new OfflineException("Creating shares not available in offline mode");
}
@Override
public List<Share> getShares(boolean refresh, Context context, ProgressListener progressListener) throws Exception
public List<Share> getShares(boolean refresh, Context context) throws Exception
{
throw new OfflineException("Getting shares not available in offline mode");
}
@Override
public void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception
public void deleteShare(String id, Context context) throws Exception
{
throw new OfflineException("Deleting shares not available in offline mode");
}
@Override
public void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception
public void updateShare(String id, String description, Long expires, Context context) throws Exception
{
throw new OfflineException("Updating shares not available in offline mode");
}
@Override
public void star(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception
public void star(String id, String albumId, String artistId, Context context) throws Exception
{
throw new OfflineException("Star not available in offline mode");
}
@Override
public void unstar(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception
public void unstar(String id, String albumId, String artistId, Context context) throws Exception
{
throw new OfflineException("UnStar not available in offline mode");
}
@Override
public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception
public List<MusicFolder> getMusicFolders(boolean refresh, Context context) throws Exception
{
throw new OfflineException("Music folders not available in offline mode");
}
@Override
public MusicDirectory getAlbumList2(String type, int size, int offset, Context context, ProgressListener progressListener) {
public MusicDirectory getAlbumList2(String type, int size, int offset, Context context) {
Timber.w("OfflineMusicService.getAlbumList2 was called but it isn't available");
return null;
}
@ -820,73 +821,73 @@ public class OfflineMusicService implements MusicService
}
@Override
public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) {
public List<ChatMessage> getChatMessages(Long since, Context context) {
Timber.w("OfflineMusicService.getChatMessages was called but it isn't available");
return null;
}
@Override
public void addChatMessage(String message, Context context, ProgressListener progressListener) {
public void addChatMessage(String message, Context context) {
Timber.w("OfflineMusicService.addChatMessage was called but it isn't available");
}
@Override
public List<Bookmark> getBookmarks(Context context, ProgressListener progressListener) {
public List<Bookmark> getBookmarks(Context context) {
Timber.w("OfflineMusicService.getBookmarks was called but it isn't available");
return null;
}
@Override
public void deleteBookmark(String id, Context context, ProgressListener progressListener) {
public void deleteBookmark(String id, Context context) {
Timber.w("OfflineMusicService.deleteBookmark was called but it isn't available");
}
@Override
public void createBookmark(String id, int position, Context context, ProgressListener progressListener) {
public void createBookmark(String id, int position, Context context) {
Timber.w("OfflineMusicService.createBookmark was called but it isn't available");
}
@Override
public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) {
public MusicDirectory getVideos(boolean refresh, Context context) {
Timber.w("OfflineMusicService.getVideos was called but it isn't available");
return null;
}
@Override
public SearchResult getStarred2(Context context, ProgressListener progressListener) {
public SearchResult getStarred2(Context context) {
Timber.w("OfflineMusicService.getStarred2 was called but it isn't available");
return null;
}
@Override
public void ping(Context context, ProgressListener progressListener) {
public void ping(Context context) {
}
@Override
public boolean isLicenseValid(Context context, ProgressListener progressListener) {
public boolean isLicenseValid(Context context) {
return true;
}
@Override
public Indexes getArtists(boolean refresh, Context context, ProgressListener progressListener) {
public Indexes getArtists(boolean refresh, Context context) {
Timber.w("OfflineMusicService.getArtists was called but it isn't available");
return null;
}
@Override
public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) {
public MusicDirectory getArtist(String id, String name, boolean refresh, Context context) {
Timber.w("OfflineMusicService.getArtist was called but it isn't available");
return null;
}
@Override
public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) {
public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context) {
Timber.w("OfflineMusicService.getAlbum was called but it isn't available");
return null;
}
@Override
public MusicDirectory getPodcastEpisodes(String podcastChannelId, Context context, ProgressListener progressListener) {
public MusicDirectory getPodcastEpisodes(String podcastChannelId, Context context) {
Timber.w("OfflineMusicService.getPodcastEpisodes was called but it isn't available");
return null;
}
@ -898,12 +899,12 @@ public class OfflineMusicService implements MusicService
}
@Override
public void setRating(String id, int rating, Context context, ProgressListener progressListener) {
public void setRating(String id, int rating, Context context) {
Timber.w("OfflineMusicService.setRating was called but it isn't available");
}
@Override
public List<PodcastsChannel> getPodcastsChannels(boolean refresh, Context context, ProgressListener progressListener) {
public List<PodcastsChannel> getPodcastsChannels(boolean refresh, Context context) {
Timber.w("OfflineMusicService.getPodcastsChannels was called but it isn't available");
return null;
}

View File

@ -4,7 +4,6 @@ import android.content.Context;
import timber.log.Timber;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.util.Util;
/**
* Scrobbles played songs to Last.fm.
@ -19,32 +18,18 @@ public class Scrobbler
public void scrobble(final Context context, final DownloadFile song, final boolean submission)
{
if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled(context))
{
return;
}
if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled(context)) return;
final String id = song.getSong().getId();
if (id == null) return;
// Avoid duplicate registrations.
if (submission && id.equals(lastSubmission))
{
return;
}
if (submission && id.equals(lastSubmission)) return;
if (!submission && id.equals(lastNowPlaying))
{
return;
}
if (!submission && id.equals(lastNowPlaying)) return;
if (submission)
{
lastSubmission = id;
}
else
{
lastNowPlaying = id;
}
if (submission) lastSubmission = id;
else lastNowPlaying = id;
new Thread(String.format("Scrobble %s", song))
{
@ -54,7 +39,7 @@ public class Scrobbler
MusicService service = MusicServiceFactory.getMusicService(context);
try
{
service.scrobble(id, submission, context, null);
service.scrobble(id, submission, context);
Timber.i("Scrobbled '%s' for %s", submission ? "submission" : "now playing", song);
}
catch (Exception x)

View File

@ -52,13 +52,12 @@ public final class Constants
public static final String INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET = "subsonic.albumlistoffset";
public static final String INTENT_EXTRA_NAME_SHUFFLE = "subsonic.shuffle";
public static final String INTENT_EXTRA_NAME_REFRESH = "subsonic.refresh";
public static final String INTENT_EXTRA_REQUEST_SEARCH = "subsonic.requestsearch";
public static final String INTENT_EXTRA_NAME_EXIT = "subsonic.exit";
public static final String INTENT_EXTRA_NAME_STARRED = "subsonic.starred";
public static final String INTENT_EXTRA_NAME_RANDOM = "subsonic.random";
public static final String INTENT_EXTRA_NAME_GENRE_NAME = "subsonic.genre";
public static final String INTENT_EXTRA_NAME_IS_ALBUM = "subsonic.isalbum";
public static final String INTENT_EXTRA_NAME_VIDEOS = "subsonic.videos";
public static final String INTENT_EXTRA_NAME_SHOW_PLAYER = "subsonic.showplayer";
// Names for Intent Actions
public static final String CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE";
@ -70,13 +69,9 @@ public final class Constants
public static final String CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS";
public static final String CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT";
// Notification IDs.
public static final int NOTIFICATION_ID_PLAYING = 100;
// Preferences keys.
public static final String PREFERENCES_KEY_SERVER_INSTANCE = "serverInstanceId";
public static final String PREFERENCES_KEY_SERVERS_KEY = "serversKey";
public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime";
public static final String PREFERENCES_KEY_SERVERS_EDIT = "editServers";
public static final String PREFERENCES_KEY_THEME = "theme";
public static final String PREFERENCES_KEY_THEME_LIGHT = "light";
public static final String PREFERENCES_KEY_THEME_DARK = "dark";
@ -145,12 +140,6 @@ public final class Constants
public static final int PREFERENCE_VALUE_A2DP = 1;
public static final int PREFERENCE_VALUE_DISABLED = 2;
// Number of free trial days for non-licensed servers.
public static final int FREE_TRIAL_DAYS = 30;
// URL for project donations.
public static final String DONATION_URL = "http://www.subsonic.org/pages/premium.jsp";
public static final String FILENAME_DOWNLOADS_SER = "downloadstate.ser";
public static final String ALBUM_ART_FILE = "folder.jpeg";

View File

@ -24,10 +24,12 @@ import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import kotlin.Lazy;
import timber.log.Timber;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import java.io.File;
import java.io.FileInputStream;
@ -43,6 +45,8 @@ import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Pattern;
import static org.koin.java.KoinJavaComponent.inject;
/**
* @author Sindre Mehus
*/
@ -55,6 +59,9 @@ public class FileUtil
private static final List<String> PLAYLIST_FILE_EXTENSIONS = Collections.singletonList("m3u");
private static final Pattern TITLE_WITH_TRACK = Pattern.compile("^\\d\\d-.*");
private static final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
private static final Lazy<PermissionUtil> permissionUtil = inject(PermissionUtil.class);
public static File getSongFile(Context context, MusicDirectory.Entry song)
{
File dir = getAlbumDirectory(context, song);
@ -148,18 +155,12 @@ public class FileUtil
File avatarFile = getAvatarFile(context, username);
SubsonicTabActivity subsonicTabActivity = SubsonicTabActivity.getInstance();
Bitmap bitmap = null;
ImageLoader imageLoader = null;
ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader();
if (subsonicTabActivity != null)
if (imageLoader != null)
{
imageLoader = subsonicTabActivity.getImageLoader();
if (imageLoader != null)
{
bitmap = imageLoader.getImageBitmap(username, size);
}
bitmap = imageLoader.getImageBitmap(username, size);
}
if (bitmap != null)
@ -218,18 +219,12 @@ public class FileUtil
File albumArtFile = getAlbumArtFile(context, entry);
SubsonicTabActivity subsonicTabActivity = SubsonicTabActivity.getInstance();
Bitmap bitmap = null;
ImageLoader imageLoader = null;
ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader();
if (subsonicTabActivity != null)
if (imageLoader != null)
{
imageLoader = subsonicTabActivity.getImageLoader();
if (imageLoader != null)
{
bitmap = imageLoader.getImageBitmap(entry, true, size);
}
bitmap = imageLoader.getImageBitmap(entry, true, size);
}
if (bitmap != null)
@ -389,7 +384,7 @@ public class FileUtil
File dir = new File(path);
boolean hasAccess = ensureDirectoryExistsAndIsReadWritable(dir);
if (hasAccess == false) PermissionUtil.handlePermissionFailed(context, null);
if (!hasAccess) permissionUtil.getValue().handlePermissionFailed(null);
return hasAccess ? dir : defaultMusicDirectory;
}
@ -492,10 +487,10 @@ public class FileUtil
if (files == null)
{
Timber.w("Failed to list children for %s", dir.getPath());
return new TreeSet<File>();
return new TreeSet<>();
}
return new TreeSet<File>(Arrays.asList(files));
return new TreeSet<>(Arrays.asList(files));
}
public static SortedSet<File> listMediaFiles(File dir)

View File

@ -1,22 +1,26 @@
package org.moire.ultrasonic.util;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import android.app.Activity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
/**
* @author Sindre Mehus
* @version $Id$
*/
public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
public abstract class FragmentBackgroundTask<T> extends BackgroundTask<T>
{
private final SubsonicTabActivity tabActivity;
private final boolean changeProgress;
private final SwipeRefreshLayout swipe;
private final CancellationToken cancel;
public TabActivityBackgroundTask(SubsonicTabActivity activity, boolean changeProgress)
public FragmentBackgroundTask(Activity activity, boolean changeProgress,
SwipeRefreshLayout swipe, CancellationToken cancel)
{
super(activity);
tabActivity = activity;
this.changeProgress = changeProgress;
this.swipe = swipe;
this.cancel = cancel;
}
@Override
@ -24,7 +28,7 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
{
if (changeProgress)
{
tabActivity.setProgressVisible(true);
if (swipe != null) swipe.setRefreshing(true);
}
new Thread()
@ -35,7 +39,7 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
try
{
final T result = doInBackground();
if (isCancelled())
if (cancel.isCancellationRequested())
{
return;
}
@ -47,7 +51,7 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
{
if (changeProgress)
{
tabActivity.setProgressVisible(false);
if (swipe != null) swipe.setRefreshing(false);
}
done(result);
@ -56,7 +60,7 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
}
catch (final Throwable t)
{
if (isCancelled())
if (cancel.isCancellationRequested())
{
return;
}
@ -67,7 +71,7 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
{
if (changeProgress)
{
tabActivity.setProgressVisible(false);
if (swipe != null) swipe.setRefreshing(false);
}
error(t);
@ -78,21 +82,8 @@ public abstract class TabActivityBackgroundTask<T> extends BackgroundTask<T>
}.start();
}
private boolean isCancelled()
{
return tabActivity.getIsDestroyed();
}
@Override
public void updateProgress(final String message)
{
getHandler().post(new Runnable()
{
@Override
public void run()
{
tabActivity.updateProgress(message);
}
});
}
}

View File

@ -19,7 +19,6 @@
package org.moire.ultrasonic.util;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
@ -30,6 +29,9 @@ import timber.log.Timber;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.content.res.ResourcesCompat;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.MusicService;
@ -38,8 +40,7 @@ import org.moire.ultrasonic.service.MusicServiceFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.SortedSet;
import java.util.Locale;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
@ -58,9 +59,9 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
private final int imageSizeLarge;
private Bitmap largeUnknownImage;
private Bitmap unknownAvatarImage;
private Context context;
private final Context context;
private Collection<Thread> threads;
private AtomicBoolean running = new AtomicBoolean();
private final AtomicBoolean running = new AtomicBoolean();
private int concurrency;
public LegacyImageLoader(
@ -71,8 +72,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
this.concurrency = concurrency;
queue = new LinkedBlockingQueue<>(1000);
Resources resources = context.getResources();
Drawable drawable = resources.getDrawable(R.drawable.unknown_album);
Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.unknown_album, null);
// Determine the density-dependent image sizes.
if (drawable != null) {
@ -101,7 +101,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
threads = Collections.synchronizedCollection(new ArrayList<Thread>(this.concurrency));
for (int i = 0; i < this.concurrency; i++) {
Thread thread = new Thread(this, String.format("ImageLoader_%d", i));
Thread thread = new Thread(this, String.format(Locale.US, "ImageLoader_%d", i));
threads.add(thread);
thread.start();
}
@ -120,7 +120,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
}
private void createLargeUnknownImage(Context context) {
Drawable drawable = context.getResources().getDrawable(R.drawable.unknown_album);
Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.unknown_album, null);
Timber.i("createLargeUnknownImage");
if (drawable != null) {
@ -129,8 +129,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
}
private void createUnknownAvatarImage(Context context) {
Resources res = context.getResources();
Drawable contact = res.getDrawable(R.drawable.ic_contact_picture);
Drawable contact = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_contact_picture, null);
unknownAvatarImage = Util.createBitmapFromDrawable(contact);
}
@ -215,7 +214,7 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
}
private static String getKey(String coverArtId, int size) {
return String.format("%s:%d", coverArtId, size);
return String.format(Locale.US, "%s:%d", coverArtId, size);
}
@Override
@ -340,10 +339,6 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
setAvatarImageBitmap(view, null, unknownAvatarImage, false);
}
private void setUnknownImage(View view, boolean large) {
setUnknownImage(view, large, -1);
}
private void setUnknownImage(View view, boolean large, int resId) {
if (resId == -1) resId = R.drawable.unknown_album;
if (large) {
@ -424,8 +419,8 @@ public class LegacyImageLoader implements Runnable, ImageLoader {
MusicService musicService = MusicServiceFactory.getMusicService(view.getContext());
final boolean isAvatar = this.username != null && this.entry == null;
final Bitmap bitmap = this.entry != null ?
musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) :
musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null);
musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality) :
musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality);
if (bitmap == null) {
Timber.d("Found empty album art.");

View File

@ -1,11 +1,7 @@
package org.moire.ultrasonic.util;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.Build;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import android.app.Activity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
/**
* @author Sindre Mehus
@ -13,29 +9,20 @@ import org.moire.ultrasonic.activity.SubsonicTabActivity;
*/
public abstract class LoadingTask<T> extends BackgroundTask<T>
{
private final SubsonicTabActivity tabActivity;
private final boolean cancellable;
private boolean cancelled;
private final SwipeRefreshLayout swipe;
private final CancellationToken cancel;
public LoadingTask(SubsonicTabActivity activity, final boolean cancellable)
public LoadingTask(Activity activity, SwipeRefreshLayout swipe, CancellationToken cancel)
{
super(activity);
tabActivity = activity;
this.cancellable = cancellable;
this.swipe = swipe;
this.cancel = cancel;
}
@Override
public void execute()
{
final ProgressDialog loading = ProgressDialog.show(tabActivity, "", "Loading. Please Wait...", true, cancellable, new DialogInterface.OnCancelListener()
{
@Override
public void onCancel(DialogInterface dialog)
{
cancelled = true;
}
});
swipe.setRefreshing(true);
new Thread()
{
@ -45,7 +32,7 @@ public abstract class LoadingTask<T> extends BackgroundTask<T>
try
{
final T result = doInBackground();
if (isCancelled())
if (cancel.isCancellationRequested())
{
return;
}
@ -55,14 +42,14 @@ public abstract class LoadingTask<T> extends BackgroundTask<T>
@Override
public void run()
{
loading.cancel();
swipe.setRefreshing(false);
done(result);
}
});
}
catch (final Throwable t)
{
if (isCancelled())
if (cancel.isCancellationRequested())
{
return;
}
@ -72,7 +59,7 @@ public abstract class LoadingTask<T> extends BackgroundTask<T>
@Override
public void run()
{
loading.cancel();
swipe.setRefreshing(false);
error(t);
}
});
@ -81,22 +68,8 @@ public abstract class LoadingTask<T> extends BackgroundTask<T>
}.start();
}
@SuppressLint("NewApi")
private boolean isCancelled()
{
return Build.VERSION.SDK_INT >= 17 ? tabActivity.isDestroyed() || cancelled : cancelled;
}
@Override
public void updateProgress(final String message)
{
getHandler().post(new Runnable()
{
@Override
public void run()
{
}
});
}
}

View File

@ -21,7 +21,6 @@ import com.karumi.dexter.listener.PermissionRequestErrorListener;
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.MainActivity;
import java.util.List;
@ -32,43 +31,59 @@ import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
* Contains static functions for Permission handling
*/
public class PermissionUtil {
private Context activityContext;
private final Context applicationContext;
public PermissionUtil(Context context) {
applicationContext = context;
}
public interface PermissionRequestFinishedCallback {
void onPermissionRequestFinished(boolean hasPermission);
}
public void ForegroundApplicationStarted(Context context) {
this.activityContext = context;
}
public void ForegroundApplicationStopped() {
activityContext = null;
}
/**
* This function can be used to handle file access permission failures.
*
* It will check if the failure is because the necessary permissions aren't available,
* and it will request them, if necessary.
*
* @param context context for the operation
* @param callback callback function to execute after the permission request is finished
*/
public static void handlePermissionFailed(final Context context, final PermissionRequestFinishedCallback callback) {
String currentCachePath = Util.getPreferences(context).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory(context).getPath());
String defaultCachePath = FileUtil.getDefaultMusicDirectory(context).getPath();
public void handlePermissionFailed(final PermissionRequestFinishedCallback callback) {
String currentCachePath = Util.getPreferences(applicationContext).getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory(applicationContext).getPath());
String defaultCachePath = FileUtil.getDefaultMusicDirectory(applicationContext).getPath();
// Ultrasonic can do nothing about this error when the Music Directory is already set to the default.
if (currentCachePath.compareTo(defaultCachePath) == 0) return;
// We must get the context of the Main Activity for the dialogs, as this function may be called from a background thread where displaying dialogs is not available
final Context mainContext = MainActivity.getInstance();
if ((PermissionChecker.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) ||
(PermissionChecker.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_DENIED)) {
if ((PermissionChecker.checkSelfPermission(applicationContext, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) ||
(PermissionChecker.checkSelfPermission(applicationContext, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_DENIED)) {
// While we request permission, the Music Directory is temporarily reset to its default location
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
requestFailedPermission(mainContext, currentCachePath, callback);
setCacheLocation(applicationContext, FileUtil.getDefaultMusicDirectory(applicationContext).getPath());
// If the application is not running, we can't notify the user
if (activityContext == null) return;
requestFailedPermission(activityContext, currentCachePath, callback);
} else {
setCacheLocation(context, FileUtil.getDefaultMusicDirectory(context).getPath());
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
showWarning(mainContext, context.getString(R.string.permissions_message_box_title), context.getString(R.string.permissions_access_error), null);
}
});
setCacheLocation(applicationContext, FileUtil.getDefaultMusicDirectory(applicationContext).getPath());
// If the application is not running, we can't notify the user
if (activityContext != null) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
showWarning(activityContext, activityContext.getString(R.string.permissions_message_box_title), activityContext.getString(R.string.permissions_access_error), null);
}
});
}
callback.onPermissionRequestFinished(false);
}
}

View File

@ -41,7 +41,7 @@ public class ShufflePlayBuffer
private static final int CAPACITY = 50;
private static final int REFILL_THRESHOLD = 40;
private final List<MusicDirectory.Entry> buffer = new ArrayList<MusicDirectory.Entry>();
private final List<MusicDirectory.Entry> buffer = new ArrayList<>();
private final Context context;
private ScheduledExecutorService executorService;
private int currentServer;
@ -78,7 +78,7 @@ public class ShufflePlayBuffer
{
clearBufferIfNecessary();
List<MusicDirectory.Entry> result = new ArrayList<MusicDirectory.Entry>(size);
List<MusicDirectory.Entry> result = new ArrayList<>(size);
synchronized (buffer)
{
while (!buffer.isEmpty() && result.size() < size)
@ -106,7 +106,7 @@ public class ShufflePlayBuffer
{
MusicService service = MusicServiceFactory.getMusicService(context);
int n = CAPACITY - buffer.size();
MusicDirectory songs = service.getRandomSongs(n, context, null);
MusicDirectory songs = service.getRandomSongs(n, context);
synchronized (buffer)
{

View File

@ -1,22 +1,16 @@
package org.moire.ultrasonic.util;
import android.content.Context;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.view.View;
import androidx.preference.DialogPreference;
import org.moire.ultrasonic.R;
import java.util.regex.Pattern;
/**
* Created by Joshua Bahnsen on 12/22/13.
*/
public class TimeSpanPreference extends DialogPreference
{
private static final Pattern COMPILE = Pattern.compile(":");
Context context;
TimeSpanPicker picker;
public TimeSpanPreference(Context context, AttributeSet attrs)
{
@ -27,6 +21,7 @@ public class TimeSpanPreference extends DialogPreference
setNegativeButtonText(android.R.string.cancel);
setDialogIcon(null);
}
public String getText()
@ -40,59 +35,4 @@ public class TimeSpanPreference extends DialogPreference
return this.context.getResources().getString(R.string.time_span_disabled);
}
@Override
public View onCreateDialogView()
{
picker = new TimeSpanPicker(this.context);
picker.setTimeSpanDisableText(this.context.getResources().getString(R.string.no_expiration));
String persisted = getPersistedString("");
if (!"".equals(persisted))
{
String[] split = COMPILE.split(persisted);
if (split.length == 2)
{
String amount = split[0];
if ("0".equals(amount) || "".equals(amount))
{
picker.setTimeSpanDisableCheckboxChecked(true);
}
picker.setTimeSpanAmount(amount);
picker.setTimeSpanType(split[1]);
}
}
else
{
picker.setTimeSpanDisableCheckboxChecked(true);
}
return picker;
}
@Override
protected void onDialogClosed(boolean positiveResult)
{
super.onDialogClosed(positiveResult);
String persisted = "";
if (picker.getTimeSpanEnabled())
{
int tsAmount = picker.getTimeSpanAmount();
if (tsAmount > 0)
{
String tsType = picker.getTimeSpanType();
persisted = String.format("%d:%s", tsAmount, tsType);
}
}
persistString(persisted);
}
}

View File

@ -0,0 +1,86 @@
package org.moire.ultrasonic.util;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.DialogPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceDialogFragmentCompat;
import org.moire.ultrasonic.R;
import java.util.Locale;
import java.util.regex.Pattern;
/**
* Created by Joshua Bahnsen on 12/22/13.
*/
public class TimeSpanPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat implements DialogPreference.TargetFragment
{
private static final Pattern COMPILE = Pattern.compile(":");
Context context;
TimeSpanPicker picker;
@Override
protected View onCreateDialogView(Context context) {
picker = new TimeSpanPicker(context);
this.context = context;
picker.setTimeSpanDisableText(this.context.getResources().getString(R.string.no_expiration));
Preference preference = getPreference();
String persisted = preference.getSharedPreferences().getString(preference.getKey(), "");
if (!"".equals(persisted))
{
String[] split = COMPILE.split(persisted);
if (split.length == 2)
{
String amount = split[0];
if ("0".equals(amount) || "".equals(amount))
{
picker.setTimeSpanDisableCheckboxChecked(true);
}
picker.setTimeSpanAmount(amount);
picker.setTimeSpanType(split[1]);
}
}
else
{
picker.setTimeSpanDisableCheckboxChecked(true);
}
return picker;
}
@Override
public void onDialogClosed(boolean positiveResult)
{
String persisted = "";
if (picker.getTimeSpanEnabled())
{
int tsAmount = picker.getTimeSpanAmount();
if (tsAmount > 0)
{
String tsType = picker.getTimeSpanType();
persisted = String.format(Locale.US, "%d:%s", tsAmount, tsType);
}
}
Preference preference = getPreference();
preference.getSharedPreferences().edit().putString(preference.getKey(), persisted).apply();
}
@Nullable
@Override
public Preference findPreference(@NonNull CharSequence key) {
return getPreference();
}
}

View File

@ -18,6 +18,7 @@
*/
package org.moire.ultrasonic.util;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
@ -39,21 +40,21 @@ import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Environment;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.util.DisplayMetrics;
import timber.log.Timber;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.RemoteViews;
import android.widget.Toast;
import androidx.annotation.ColorInt;
import androidx.preference.PreferenceManager;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.DownloadActivity;
import org.moire.ultrasonic.activity.MainActivity;
import org.moire.ultrasonic.activity.SettingsActivity;
import org.moire.ultrasonic.activity.NavigationActivity;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.*;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
@ -63,10 +64,8 @@ import org.moire.ultrasonic.service.DownloadFile;
import java.io.*;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@ -95,8 +94,6 @@ public class Util
private static boolean mediaButtonsRegisteredForUI;
private static boolean mediaButtonsRegisteredForService;
private static final Map<Integer, Version> SERVER_REST_VERSIONS = new ConcurrentHashMap<Integer, Version>();
// Used by hexEncode()
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private static Toast toast;
@ -124,7 +121,7 @@ public class Util
SharedPreferences preferences = getPreferences(context);
SharedPreferences.Editor editor = preferences.edit();
editor.putString(Constants.PREFERENCES_KEY_REPEAT_MODE, repeatMode.name());
editor.commit();
editor.apply();
}
public static boolean isNotificationEnabled(Context context)
@ -143,6 +140,7 @@ public class Util
return preferences.getBoolean(Constants.PREFERENCES_KEY_ALWAYS_SHOW_NOTIFICATION, false);
}
@SuppressWarnings({"BooleanMethodIsAlwaysInverted"}) // It is inverted for readability
public static boolean isLockScreenEnabled(Context context)
{
SharedPreferences preferences = getPreferences(context);
@ -207,25 +205,6 @@ public class Util
return PreferenceManager.getDefaultSharedPreferences(context);
}
public static int getRemainingTrialDays(Context context)
{
SharedPreferences preferences = getPreferences(context);
long installTime = preferences.getLong(Constants.PREFERENCES_KEY_INSTALL_TIME, 0L);
if (installTime == 0L)
{
installTime = System.currentTimeMillis();
SharedPreferences.Editor editor = preferences.edit();
editor.putLong(Constants.PREFERENCES_KEY_INSTALL_TIME, installTime);
editor.commit();
}
long now = System.currentTimeMillis();
long millisPerDay = 24L * 60L * 60L * 1000L;
int daysSinceInstall = (int) ((now - installTime) / millisPerDay);
return Math.max(0, Constants.FREE_TRIAL_DAYS - daysSinceInstall);
}
/**
* Get the contents of an <code>InputStream</code> as a <code>byte[]</code>.
* <p/>
@ -348,6 +327,7 @@ public class Util
toast(context, message, true);
}
@SuppressLint("ShowToast") // Invalid warning
public static void toast(Context context, CharSequence message, boolean shortDuration)
{
if (toast == null)
@ -383,22 +363,19 @@ public class Util
// More than 1 GB?
if (byteCount >= 1024 * 1024 * 1024)
{
NumberFormat gigaByteFormat = GIGA_BYTE_FORMAT;
return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024));
return GIGA_BYTE_FORMAT.format((double) byteCount / (1024 * 1024 * 1024));
}
// More than 1 MB?
if (byteCount >= 1024 * 1024)
{
NumberFormat megaByteFormat = MEGA_BYTE_FORMAT;
return megaByteFormat.format((double) byteCount / (1024 * 1024));
return MEGA_BYTE_FORMAT.format((double) byteCount / (1024 * 1024));
}
// More than 1 KB?
if (byteCount >= 1024)
{
NumberFormat kiloByteFormat = KILO_BYTE_FORMAT;
return kiloByteFormat.format((double) byteCount / 1024);
return KILO_BYTE_FORMAT.format((double) byteCount / 1024);
}
return byteCount + " B";
@ -604,19 +581,6 @@ public class Util
showDialog(context, android.R.drawable.ic_dialog_info, titleId, messageId);
}
public static void showWelcomeDialog(final Context context, final MainActivity activity, int titleId, int messageId)
{
new AlertDialog.Builder(context).setIcon(android.R.drawable.ic_dialog_info).setTitle(titleId).setMessage(messageId).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int i)
{
dialog.dismiss();
activity.startActivityForResultWithoutTransition(activity, SettingsActivity.class);
}
}).show();
}
private static void showDialog(Context context, int icon, int titleId, int messageId)
{
new AlertDialog.Builder(context).setIcon(icon).setTitle(titleId).setMessage(messageId).setPositiveButton(R.string.common_ok, new DialogInterface.OnClickListener()
@ -642,11 +606,6 @@ public class Util
}
}
public static void disablePendingTransition(Activity activity)
{
activity.overridePendingTransition(0, 0);
}
public static Drawable getDrawableFromAttribute(Context context, int attr)
{
int[] attrs = new int[]{attr};
@ -682,7 +641,7 @@ public class Util
public static WifiManager.WifiLock createWifiLock(Context context, String tag)
{
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
return wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, tag);
}
@ -917,11 +876,7 @@ public class Util
avrcpIntent.putExtra("playing", true);
break;
case STOPPED:
avrcpIntent.putExtra("playing", false);
break;
case PAUSED:
avrcpIntent.putExtra("playing", false);
break;
case COMPLETED:
avrcpIntent.putExtra("playing", false);
break;
@ -1026,7 +981,7 @@ public class Util
// guarantee
// a final image with both dimensions larger than or equal to the
// requested height and width.
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
inSampleSize = Math.min(heightRatio, widthRatio);
}
return inSampleSize;
@ -1034,8 +989,10 @@ public class Util
public static void linkButtons(Context context, RemoteViews views, boolean playerActive)
{
Intent intent = new Intent(context, NavigationActivity.class).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
if (playerActive)
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true);
Intent intent = new Intent(context, playerActive ? DownloadActivity.class : MainActivity.class);
intent.setAction("android.intent.action.MAIN");
intent.addCategory("android.intent.category.LAUNCHER");
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
@ -1098,6 +1055,7 @@ public class Util
views.setOnClickPendingIntent(R.id.notification_five_star_5, pendingIntent);
}
// TODO: Shouldn't this be used when making requests?
public static int getNetworkTimeout(Context context)
{
SharedPreferences preferences = getPreferences(context);
@ -1203,6 +1161,7 @@ public class Util
return Integer.parseInt(preferences.getString(Constants.PREFERENCES_KEY_DIRECTORY_CACHE_TIME, "300"));
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted") // Inverted for readability
public static boolean isNullOrWhiteSpace(String string)
{
return string == null || string.isEmpty() || string.trim().isEmpty();
@ -1252,18 +1211,18 @@ public class Util
if (hours >= 10)
{
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
return String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds);
}
else if (hours > 0)
{
return String.format("%d:%02d:%02d", hours, minutes, seconds);
return String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds);
}
else if (minutes >= 10)
{
return String.format("%02d:%02d", minutes, seconds);
return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds);
}
else return minutes > 0 ? String.format("%d:%02d", minutes, seconds) : String.format("0:%02d", seconds);
else return minutes > 0 ? String.format(Locale.getDefault(), "%d:%02d", minutes, seconds) : String.format(Locale.getDefault(), "0:%02d", seconds);
}
public static VideoPlayerType getVideoPlayerType(Context context)
@ -1342,6 +1301,7 @@ public class Util
return versionCode;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted") // Inverted for readability
public static boolean getShouldSendBluetoothNotifications(Context context)
{
SharedPreferences preferences = getPreferences(context);
@ -1409,7 +1369,7 @@ public class Util
SharedPreferences preferences = getPreferences(context);
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean(Constants.PREFERENCES_KEY_ASK_FOR_SHARE_DETAILS, shouldAskForShareDetails);
editor.commit();
editor.apply();
}
public static void setDefaultShareExpiration(Context context, String defaultShareExpiration)
@ -1417,7 +1377,7 @@ public class Util
SharedPreferences preferences = getPreferences(context);
SharedPreferences.Editor editor = preferences.edit();
editor.putString(Constants.PREFERENCES_KEY_DEFAULT_SHARE_EXPIRATION, defaultShareExpiration);
editor.commit();
editor.apply();
}
public static void setDefaultShareDescription(Context context, String defaultShareDescription)
@ -1425,7 +1385,7 @@ public class Util
SharedPreferences preferences = getPreferences(context);
SharedPreferences.Editor editor = preferences.edit();
editor.putString(Constants.PREFERENCES_KEY_DEFAULT_SHARE_DESCRIPTION, defaultShareDescription);
editor.commit();
editor.apply();
}
public static boolean getShouldShowAllSongsByArtist(Context context)
@ -1498,4 +1458,12 @@ public class Util
return preferences.getBoolean(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE, false);
}
public static void hideKeyboard(Activity activity) {
InputMethodManager inputManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
View currentFocusedView = activity.getCurrentFocus();
if (currentFocusedView != null) {
inputManager.hideSoftInputFromWindow(currentFocusedView.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
}

View File

@ -18,8 +18,8 @@
*/
package org.moire.ultrasonic.util;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
@ -38,27 +38,27 @@ public enum VideoPlayerType
MX("mx")
{
@Override
public void playVideo(final Activity activity, MusicDirectory.Entry entry) throws Exception
public void playVideo(final Context context, MusicDirectory.Entry entry) throws Exception
{
// Check if MX Player is installed.
boolean installedAd = Util.isPackageInstalled(activity, PACKAGE_NAME_MX_AD);
boolean installedPro = Util.isPackageInstalled(activity, PACKAGE_NAME_MX_PRO);
boolean installedAd = Util.isPackageInstalled(context, PACKAGE_NAME_MX_AD);
boolean installedPro = Util.isPackageInstalled(context, PACKAGE_NAME_MX_PRO);
if (!installedAd && !installedPro)
{
new AlertDialog.Builder(activity).setMessage(R.string.video_get_mx_player_text).setPositiveButton(R.string.video_get_mx_player_button, new DialogInterface.OnClickListener()
new AlertDialog.Builder(context).setMessage(R.string.video_get_mx_player_text).setPositiveButton(R.string.video_get_mx_player_button, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int i)
{
try
{
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("market://details?id=%s", PACKAGE_NAME_MX_AD))));
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("market://details?id=%s", PACKAGE_NAME_MX_AD))));
}
catch (android.content.ActivityNotFoundException x)
{
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("http://play.google.com/store/apps/details?id=%s", PACKAGE_NAME_MX_AD))));
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("http://play.google.com/store/apps/details?id=%s", PACKAGE_NAME_MX_AD))));
}
dialog.dismiss();
@ -79,8 +79,8 @@ public enum VideoPlayerType
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setPackage(installedPro ? PACKAGE_NAME_MX_PRO : PACKAGE_NAME_MX_AD);
intent.putExtra("title", entry.getTitle());
intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(activity).getVideoUrl(activity, entry.getId(), false)), "video/*");
activity.startActivity(intent);
intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(context, entry.getId(), false)), "video/*");
context.startActivity(intent);
}
}
},
@ -88,22 +88,22 @@ public enum VideoPlayerType
FLASH("flash")
{
@Override
public void playVideo(Activity activity, MusicDirectory.Entry entry) throws Exception
public void playVideo(Context context, MusicDirectory.Entry entry) throws Exception
{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(MusicServiceFactory.getMusicService(activity).getVideoUrl(activity, entry.getId(), true)));
activity.startActivity(intent);
intent.setData(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(context, entry.getId(), true)));
context.startActivity(intent);
}
},
DEFAULT("default")
{
@Override
public void playVideo(Activity activity, MusicDirectory.Entry entry) throws Exception
public void playVideo(Context context, MusicDirectory.Entry entry) throws Exception
{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(activity).getVideoUrl(activity, entry.getId(), false)), "video/*");
activity.startActivity(intent);
intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService(context).getVideoUrl(context, entry.getId(), false)), "video/*");
context.startActivity(intent);
}
};
@ -131,7 +131,7 @@ public enum VideoPlayerType
return null;
}
public abstract void playVideo(Activity activity, MusicDirectory.Entry entry) throws Exception;
public abstract void playVideo(Context context, MusicDirectory.Entry entry) throws Exception;
private static final String PACKAGE_NAME_MX_AD = "com.mxtech.videoplayer.ad";
private static final String PACKAGE_NAME_MX_PRO = "com.mxtech.videoplayer.pro";

View File

@ -23,8 +23,6 @@ import android.graphics.drawable.Drawable;
import timber.log.Timber;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
@ -44,10 +42,10 @@ public class AlbumView extends UpdateView
private static Drawable starHollowDrawable;
private static String theme;
private Context context;
private final Context context;
private MusicDirectory.Entry entry;
private EntryAdapter.AlbumViewHolder viewHolder;
private ImageLoader imageLoader;
private final ImageLoader imageLoader;
private boolean maximized = false;
public AlbumView(Context context, ImageLoader imageLoader)
@ -75,10 +73,10 @@ public class AlbumView extends UpdateView
{
LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true);
viewHolder = new EntryAdapter.AlbumViewHolder();
viewHolder.title = (TextView) findViewById(R.id.album_title);
viewHolder.artist = (TextView) findViewById(R.id.album_artist);
viewHolder.cover_art = (ImageView) findViewById(R.id.album_coverart);
viewHolder.star = (ImageView) findViewById(R.id.album_star);
viewHolder.title = findViewById(R.id.album_title);
viewHolder.artist = findViewById(R.id.album_artist);
viewHolder.cover_art = findViewById(R.id.album_coverart);
viewHolder.star = findViewById(R.id.album_star);
setTag(viewHolder);
}
@ -99,11 +97,7 @@ public class AlbumView extends UpdateView
}
public void maximizeOrMinimize() {
if (maximized) {
maximized = false;
} else {
maximized = true;
}
maximized = !maximized;
if (this.viewHolder.title != null) {
this.viewHolder.title.setSingleLine(!maximized);
}
@ -164,11 +158,11 @@ public class AlbumView extends UpdateView
{
if (!isStarred)
{
musicService.star(!useId3 ? id : null, useId3 ? id : null, null, getContext(), null);
musicService.star(!useId3 ? id : null, useId3 ? id : null, null, getContext());
}
else
{
musicService.unstar(!useId3 ? id : null, useId3 ? id : null, null, getContext(), null);
musicService.unstar(!useId3 ? id : null, useId3 ? id : null, null, getContext());
}
}
catch (Exception e)

View File

@ -1,5 +1,6 @@
package org.moire.ultrasonic.view;
import android.content.Context;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.LayoutInflater;
@ -9,11 +10,10 @@ import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.ChatMessage;
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
import org.moire.ultrasonic.util.ImageLoader;
import org.moire.ultrasonic.util.Util;
import java.text.DateFormat;
import java.util.Date;
@ -26,18 +26,19 @@ import static org.koin.java.KoinJavaComponent.inject;
public class ChatAdapter extends ArrayAdapter<ChatMessage>
{
private final SubsonicTabActivity activity;
private List<ChatMessage> messages;
private final Context context;
private final List<ChatMessage> messages;
private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})";
private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
private final Lazy<ImageLoaderProvider> imageLoader = inject(ImageLoaderProvider.class);
public ChatAdapter(SubsonicTabActivity activity, List<ChatMessage> messages)
public ChatAdapter(Context context, List<ChatMessage> messages)
{
super(activity, R.layout.chat_item, messages);
this.activity = activity;
super(context, R.layout.chat_item, messages);
this.context = context;
this.messages = messages;
}
@ -91,14 +92,14 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
holder.chatMessage = message;
DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(activity);
DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(context);
String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
ImageLoader imageLoader = activity.getImageLoader();
ImageLoader imageLoaderInstance = imageLoader.getValue().getImageLoader();
if (imageLoader != null)
if (imageLoaderInstance != null)
{
imageLoader.loadAvatarImage(holder.avatar, messageUser, false, holder.avatar.getWidth(), false, true);
imageLoaderInstance.loadAvatarImage(holder.avatar, messageUser, false, holder.avatar.getWidth(), false, true);
}
holder.username.setText(messageUser);
@ -110,7 +111,7 @@ public class ChatAdapter extends ArrayAdapter<ChatMessage>
private View inflateView(int layout, ViewGroup parent)
{
return LayoutInflater.from(activity).inflate(layout, parent, false);
return LayoutInflater.from(context).inflate(layout, parent, false);
}
private static ViewHolder createViewHolder(int layout, View convertView)

View File

@ -18,6 +18,7 @@
*/
package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
@ -25,7 +26,7 @@ import android.widget.CheckedTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.util.ImageLoader;
@ -36,15 +37,15 @@ import java.util.List;
*/
public class EntryAdapter extends ArrayAdapter<Entry>
{
private final SubsonicTabActivity activity;
private final Context context;
private final ImageLoader imageLoader;
private final boolean checkable;
public EntryAdapter(SubsonicTabActivity activity, ImageLoader imageLoader, List<Entry> entries, boolean checkable)
public EntryAdapter(Context context, ImageLoader imageLoader, List<Entry> entries, boolean checkable)
{
super(activity, android.R.layout.simple_list_item_1, entries);
super(context, android.R.layout.simple_list_item_1, entries);
this.activity = activity;
this.context = context;
this.imageLoader = imageLoader;
this.checkable = checkable;
}
@ -58,7 +59,7 @@ public class EntryAdapter extends ArrayAdapter<Entry>
{
AlbumView view;
if (convertView != null && convertView instanceof AlbumView)
if (convertView instanceof AlbumView)
{
AlbumView currentView = (AlbumView) convertView;
@ -75,7 +76,7 @@ public class EntryAdapter extends ArrayAdapter<Entry>
}
else
{
view = new AlbumView(activity, imageLoader);
view = new AlbumView(context, imageLoader);
view.setLayout();
}
@ -86,7 +87,7 @@ public class EntryAdapter extends ArrayAdapter<Entry>
{
SongView view;
if (convertView != null && convertView instanceof SongView)
if (convertView instanceof SongView)
{
SongView currentView = (SongView) convertView;
@ -104,7 +105,7 @@ public class EntryAdapter extends ArrayAdapter<Entry>
}
else
{
view = new SongView(activity);
view = new SongView(context);
view.setLayout(entry);
}

View File

@ -1,17 +1,14 @@
package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.Playlist;
import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
@ -20,12 +17,12 @@ import java.util.List;
public class PlaylistAdapter extends ArrayAdapter<Playlist>
{
private final SubsonicTabActivity activity;
private final Context context;
public PlaylistAdapter(SubsonicTabActivity activity, List<Playlist> Playlists)
public PlaylistAdapter(Context context, List<Playlist> Playlists)
{
super(activity, R.layout.playlist_list_item, Playlists);
this.activity = activity;
super(context, R.layout.playlist_list_item, Playlists);
this.context = context;
}
@Override
@ -34,7 +31,7 @@ public class PlaylistAdapter extends ArrayAdapter<Playlist>
Playlist entry = getItem(position);
PlaylistView view;
if (convertView != null && convertView instanceof PlaylistView)
if (convertView instanceof PlaylistView)
{
PlaylistView currentView = (PlaylistView) convertView;
@ -44,7 +41,7 @@ public class PlaylistAdapter extends ArrayAdapter<Playlist>
}
else
{
view = new PlaylistView(activity);
view = new PlaylistView(context);
view.setLayout();
}
@ -52,23 +49,6 @@ public class PlaylistAdapter extends ArrayAdapter<Playlist>
return view;
}
public static class PlaylistComparator implements Comparator<Playlist>, Serializable
{
private static final long serialVersionUID = -6201663557439120008L;
@Override
public int compare(Playlist playlist1, Playlist playlist2)
{
return playlist1.getName().compareToIgnoreCase(playlist2.getName());
}
public static List<Playlist> sort(List<Playlist> playlists)
{
Collections.sort(playlists, new PlaylistComparator());
return playlists;
}
}
static class ViewHolder
{
TextView name;

View File

@ -1,17 +1,14 @@
package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.SubsonicTabActivity;
import org.moire.ultrasonic.domain.Share;
import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
@ -20,12 +17,12 @@ import java.util.List;
public class ShareAdapter extends ArrayAdapter<Share>
{
private final SubsonicTabActivity activity;
private final Context context;
public ShareAdapter(SubsonicTabActivity activity, List<Share> Shares)
public ShareAdapter(Context context, List<Share> Shares)
{
super(activity, R.layout.share_list_item, Shares);
this.activity = activity;
super(context, R.layout.share_list_item, Shares);
this.context = context;
}
@Override
@ -34,7 +31,7 @@ public class ShareAdapter extends ArrayAdapter<Share>
Share entry = getItem(position);
ShareView view;
if (convertView != null && convertView instanceof ShareView)
if (convertView instanceof ShareView)
{
ShareView currentView = (ShareView) convertView;
@ -44,7 +41,7 @@ public class ShareAdapter extends ArrayAdapter<Share>
}
else
{
view = new ShareView(activity);
view = new ShareView(context);
view.setLayout();
}
@ -52,23 +49,6 @@ public class ShareAdapter extends ArrayAdapter<Share>
return view;
}
public static class ShareComparator implements Comparator<Share>, Serializable
{
private static final long serialVersionUID = -7169409928471418921L;
@Override
public int compare(Share share1, Share share2)
{
return share1.getId().compareToIgnoreCase(share2.getId());
}
public static List<Share> sort(List<Share> shares)
{
Collections.sort(shares, new ShareComparator());
return shares;
}
}
static class ViewHolder
{
TextView url;

View File

@ -0,0 +1,379 @@
package org.moire.ultrasonic.activity
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.provider.MediaStore
import android.provider.SearchRecentSuggestions
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentContainerView
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.onNavDestinationSelected
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
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.domain.PlayerState
import org.moire.ultrasonic.fragment.OnBackPressedHandler
import org.moire.ultrasonic.fragment.ServerSettingsModel
import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.DownloadFile
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.NowPlayingEventDistributor
import org.moire.ultrasonic.util.NowPlayingEventListener
import org.moire.ultrasonic.util.PermissionUtil
import org.moire.ultrasonic.util.SubsonicUncaughtExceptionHandler
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
import org.moire.ultrasonic.util.ThemeChangedEventListener
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* The main Activity of Ultrasonic which loads all other screens as Fragments
*/
class NavigationActivity : AppCompatActivity() {
private var chatMenuItem: MenuItem? = null
private var bookmarksMenuItem: MenuItem? = null
private var sharesMenuItem: MenuItem? = null
private var podcastsMenuItem: MenuItem? = null
private var nowPlayingView: FragmentContainerView? = null
private var nowPlayingHidden = false
private var navigationView: NavigationView? = null
private var drawerLayout: DrawerLayout? = null
private var host: NavHostFragment? = null
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var nowPlayingEventListener: NowPlayingEventListener
private lateinit var themeChangedEventListener: ThemeChangedEventListener
private val serverSettingsModel: ServerSettingsModel by viewModel()
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
private val mediaPlayerController: MediaPlayerController by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
private val nowPlayingEventDistributor: NowPlayingEventDistributor by inject()
private val themeChangedEventDistributor: ThemeChangedEventDistributor by inject()
private val permissionUtil: PermissionUtil by inject()
private var infoDialogDisplayed = false
private var currentFragmentId: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
setUncaughtExceptionHandler()
permissionUtil.ForegroundApplicationStarted(this)
Util.applyTheme(this)
super.onCreate(savedInstanceState)
volumeControlStream = AudioManager.STREAM_MUSIC
setContentView(R.layout.navigation_activity)
nowPlayingView = findViewById(R.id.now_playing_fragment)
navigationView = findViewById(R.id.nav_view)
drawerLayout = findViewById(R.id.drawer_layout)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
host = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ?: return
val navController = host!!.navController
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.mainFragment,
R.id.selectArtistFragment,
R.id.searchFragment,
R.id.playlistsFragment,
R.id.sharesFragment,
R.id.bookmarksFragment,
R.id.chatFragment,
R.id.podcastFragment,
R.id.settingsFragment,
R.id.aboutFragment,
R.id.playerFragment
),
drawerLayout
)
setupActionBar(navController, appBarConfiguration)
setupNavigationMenu(navController)
navController.addOnDestinationChangedListener { _, destination, _ ->
val dest: String = try {
resources.getResourceName(destination.id)
} catch (e: Resources.NotFoundException) {
destination.id.toString()
}
Timber.d("Navigated to $dest")
currentFragmentId = destination.id
// Handle the hiding of the NowPlaying fragment when the Player is active
if (currentFragmentId == R.id.playerFragment) {
hideNowPlaying()
} else {
if (!nowPlayingHidden) showNowPlaying()
}
// Hides menu items for Offline mode
setMenuForServerSetting()
}
// Determine first run and migrate server settings to DB as early as possible
var showWelcomeScreen = Util.isFirstRun(this)
val areServersMigrated: Boolean = serverSettingsModel.migrateFromPreferences()
// If there are any servers in the DB, do not show the welcome screen
showWelcomeScreen = showWelcomeScreen and !areServersMigrated
loadSettings()
showInfoDialog(showWelcomeScreen)
nowPlayingEventListener = object : NowPlayingEventListener {
override fun onDismissNowPlaying() {
nowPlayingHidden = true
hideNowPlaying()
}
override fun onHideNowPlaying() {
hideNowPlaying()
}
override fun onShowNowPlaying() {
showNowPlaying()
}
}
themeChangedEventListener = object : ThemeChangedEventListener {
override fun onThemeChanged() { recreate() }
}
nowPlayingEventDistributor.subscribe(nowPlayingEventListener)
themeChangedEventDistributor.subscribe(themeChangedEventListener)
}
override fun onResume() {
super.onResume()
setMenuForServerSetting()
Util.registerMediaButtonEventReceiver(this, false)
// Lifecycle support's constructor registers some event receivers so it should be created early
lifecycleSupport.onCreate()
if (!nowPlayingHidden) showNowPlaying()
else hideNowPlaying()
}
override fun onDestroy() {
super.onDestroy()
Util.unregisterMediaButtonEventReceiver(this, false)
nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener)
themeChangedEventDistributor.unsubscribe(themeChangedEventListener)
imageLoaderProvider.clearImageLoader()
permissionUtil.ForegroundApplicationStopped()
}
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) {
navigationView?.setupWithNavController(navController)
// The exit menu is handled here manually
val exitItem: MenuItem? = navigationView?.menu?.findItem(R.id.menu_exit)
exitItem?.setOnMenuItemClickListener { item ->
if (item.itemId == R.id.menu_exit) {
setResult(Constants.RESULT_CLOSE_ALL)
mediaPlayerController.stopJukeboxService()
imageLoaderProvider.getImageLoader().stopImageLoader()
finish()
exit()
}
true
}
chatMenuItem = navigationView?.menu?.findItem(R.id.chatFragment)
bookmarksMenuItem = navigationView?.menu?.findItem(R.id.bookmarksFragment)
sharesMenuItem = navigationView?.menu?.findItem(R.id.sharesFragment)
podcastsMenuItem = navigationView?.menu?.findItem(R.id.podcastFragment)
}
private fun setupActionBar(navController: NavController, appBarConfig: AppBarConfiguration) {
setupActionBarWithNavController(navController, appBarConfig)
}
override fun onBackPressed() {
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) {
this.drawerLayout?.closeDrawer(GravityCompat.START)
} else {
val currentFragment = host!!.childFragmentManager.fragments.last()
if (currentFragment is OnBackPressedHandler) currentFragment.onBackPressed()
else super.onBackPressed()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val retValue = super.onCreateOptionsMenu(menu)
if (navigationView == null) {
menuInflater.inflate(R.menu.navigation, menu)
return true
}
return retValue
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return item.onNavDestinationSelected(findNavController(R.id.nav_host_fragment)) ||
super.onOptionsItemSelected(item)
}
override fun onSupportNavigateUp(): Boolean {
val currentFragment = host!!.childFragmentManager.fragments.last()
return if (currentFragment is OnBackPressedHandler) {
currentFragment.onBackPressed()
true
} else {
findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
}
}
// TODO Test if this works with external Intents
// android.intent.action.SEARCH and android.media.action.MEDIA_PLAY_FROM_SEARCH calls here
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent == null) return
if (intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, false)) {
findNavController(R.id.nav_host_fragment).navigate(R.id.playerFragment)
return
}
val query = intent.getStringExtra(SearchManager.QUERY)
if (query != null) {
val autoPlay = intent.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
val suggestions = SearchRecentSuggestions(
this,
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
)
suggestions.saveRecentQuery(query, null)
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_QUERY, query)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, autoPlay)
findNavController(R.id.nav_host_fragment).navigate(R.id.searchFragment, bundle)
}
}
private fun loadSettings() {
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
val preferences = Util.getPreferences(this)
if (!preferences.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION)) {
val editor = preferences.edit()
editor.putString(
Constants.PREFERENCES_KEY_CACHE_LOCATION,
FileUtil.getDefaultMusicDirectory(this).path
)
editor.apply()
}
}
private fun exit() {
lifecycleSupport.onDestroy()
Util.unregisterMediaButtonEventReceiver(this, false)
finish()
}
private fun showInfoDialog(show: Boolean) {
if (!infoDialogDisplayed) {
infoDialogDisplayed = true
if (show) {
AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_info)
.setTitle(R.string.main_welcome_title)
.setMessage(R.string.main_welcome_text)
.setPositiveButton(R.string.common_ok) { dialog, _ ->
dialog.dismiss()
findNavController(R.id.nav_host_fragment).navigate(R.id.settingsFragment)
}.show()
}
}
}
private fun setUncaughtExceptionHandler() {
val handler = Thread.getDefaultUncaughtExceptionHandler()
if (handler !is SubsonicUncaughtExceptionHandler) {
Thread.setDefaultUncaughtExceptionHandler(SubsonicUncaughtExceptionHandler(this))
}
}
private fun showNowPlaying() {
if (!Util.getShowNowPlayingPreference(this)) {
hideNowPlaying()
return
}
// The logic for nowPlayingHidden is that the user can dismiss NowPlaying with a gesture,
// and when the MediaPlayerService requests that it should be shown, it returns
nowPlayingHidden = false
// Do not show for Player fragment
if (currentFragmentId == R.id.playerFragment) {
hideNowPlaying()
return
}
if (nowPlayingView != null) {
val playerState: PlayerState = mediaPlayerController.playerState
if (playerState == PlayerState.PAUSED || playerState == PlayerState.STARTED) {
val file: DownloadFile? = mediaPlayerController.currentPlaying
if (file != null) {
nowPlayingView?.visibility = View.VISIBLE
}
} else {
hideNowPlaying()
}
}
}
private fun hideNowPlaying() {
nowPlayingView?.visibility = View.GONE
}
private fun setMenuForServerSetting() {
val visibility = !isOffline(this)
chatMenuItem?.isVisible = visibility
bookmarksMenuItem?.isVisible = visibility
sharesMenuItem?.isVisible = visibility
podcastsMenuItem?.isVisible = visibility
}
}

View File

@ -1,215 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2020 (C) Jozsef Varga
*/
package org.moire.ultrasonic.activity
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.PopupMenu
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Util
/**
* Displays the available Artists in a list
*/
class SelectArtistActivity : SubsonicTabActivity() {
private val activeServerProvider: ActiveServerProvider by inject()
private val serverSettingsModel: ServerSettingsModel by viewModel()
private val artistListModel: ArtistListModel by viewModel()
private var refreshArtistListView: SwipeRefreshLayout? = null
private var artistListView: RecyclerView? = null
private var musicFolders: List<MusicFolder>? = null
private lateinit var viewManager: RecyclerView.LayoutManager
private lateinit var viewAdapter: ArtistRowAdapter
/**
* Called when the activity is first created.
*/
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.select_artist)
refreshArtistListView = findViewById(R.id.select_artist_refresh)
refreshArtistListView!!.setOnRefreshListener {
artistListModel.refresh(refreshArtistListView!!)
}
val shouldShowHeader = (!isOffline(this) && !Util.getShouldUseId3Tags(this))
val title = intent.getStringExtra(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE)
if (title == null) {
setActionBarSubtitle(
if (isOffline(this)) R.string.music_library_label_offline
else R.string.music_library_label
)
} else {
actionBarSubtitle = title
}
val browseMenuItem = findViewById<View>(R.id.menu_browse)
menuDrawer.setActiveView(browseMenuItem)
musicFolders = null
val refresh = intent.getBooleanExtra(Constants.INTENT_EXTRA_NAME_REFRESH, false)
artistListModel.getMusicFolders()
.observe(
this,
Observer { changedFolders ->
if (changedFolders != null) {
musicFolders = changedFolders
viewAdapter.setFolderName(getMusicFolderName(changedFolders))
}
}
)
val artists = artistListModel.getArtists(refresh, refreshArtistListView!!)
artists.observe(
this, Observer { changedArtists -> viewAdapter.setData(changedArtists) }
)
viewManager = LinearLayoutManager(this)
viewAdapter = ArtistRowAdapter(
artists.value ?: listOf(),
getText(R.string.select_artist_all_folders).toString(),
shouldShowHeader,
{ artist -> onItemClick(artist) },
{ menuItem, artist -> onArtistMenuItemSelected(menuItem, artist) },
{ view -> onFolderClick(view) },
imageLoader
)
artistListView = findViewById<RecyclerView>(R.id.select_artist_list).apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
menuDrawer.toggleMenu()
return true
}
R.id.main_shuffle -> {
val intent = Intent(this, DownloadActivity::class.java)
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHUFFLE, true)
startActivityForResultWithoutTransition(this, intent)
return true
}
}
return false
}
private fun getMusicFolderName(musicFolders: List<MusicFolder>): String {
val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
if (musicFolderId != null && musicFolderId != "") {
for ((id, name) in musicFolders) {
if (id == musicFolderId) {
return name
}
}
}
return getText(R.string.select_artist_all_folders).toString()
}
private fun onItemClick(artist: Artist) {
val intent = Intent(this, SelectAlbumActivity::class.java)
intent.putExtra(Constants.INTENT_EXTRA_NAME_ID, artist.id)
intent.putExtra(Constants.INTENT_EXTRA_NAME_NAME, artist.name)
intent.putExtra(Constants.INTENT_EXTRA_NAME_PARENT_ID, artist.id)
intent.putExtra(Constants.INTENT_EXTRA_NAME_ARTIST, true)
startActivityForResultWithoutTransition(this, intent)
}
private fun onFolderClick(view: View) {
val popup = PopupMenu(this, view)
val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
var menuItem = popup.menu.add(
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
)
if (musicFolderId == null || musicFolderId.isEmpty()) {
menuItem.isChecked = true
}
if (musicFolders != null) {
for (i in musicFolders!!.indices) {
val (id, name) = musicFolders!![i]
menuItem = popup.menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, name)
if (id == musicFolderId) {
menuItem.isChecked = true
}
}
}
popup.menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true)
popup.setOnMenuItemClickListener { item -> onFolderMenuItemSelected(item) }
popup.show()
}
private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean {
when (menuItem.itemId) {
R.id.artist_menu_play_now ->
downloadRecursively(artist.id, false, false, true, false, false, false, false, true)
R.id.artist_menu_play_next ->
downloadRecursively(artist.id, false, false, true, true, false, true, false, true)
R.id.artist_menu_play_last ->
downloadRecursively(artist.id, false, true, false, false, false, false, false, true)
R.id.artist_menu_pin ->
downloadRecursively(artist.id, true, true, false, false, false, false, false, true)
R.id.artist_menu_unpin ->
downloadRecursively(artist.id, false, false, false, false, false, false, true, true)
R.id.artist_menu_download ->
downloadRecursively(artist.id, false, false, false, false, true, false, false, true)
}
return true
}
private fun onFolderMenuItemSelected(menuItem: MenuItem): Boolean {
val selectedFolder = if (menuItem.itemId == -1) null else musicFolders!![menuItem.itemId]
val musicFolderId = selectedFolder?.id
val musicFolderName = selectedFolder?.name
?: getString(R.string.select_artist_all_folders)
if (!isOffline(this)) {
val currentSetting = activeServerProvider.getActiveServer()
currentSetting.musicFolderId = musicFolderId
serverSettingsModel.updateItem(currentSetting)
}
viewAdapter.setFolderName(musicFolderName)
artistListModel.refresh(refreshArtistListView!!)
return true
}
companion object {
private const val MENU_GROUP_MUSIC_FOLDER = 10
}
}

View File

@ -6,17 +6,22 @@ import org.koin.core.context.startKoin
import org.koin.core.logger.Level
import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.di.appPermanentStorage
import org.moire.ultrasonic.di.applicationModule
import org.moire.ultrasonic.di.baseNetworkModule
import org.moire.ultrasonic.di.directoriesModule
import org.moire.ultrasonic.di.featureFlagsModule
import org.moire.ultrasonic.di.mediaPlayerModule
import org.moire.ultrasonic.di.musicServiceModule
import org.moire.ultrasonic.log.FileLoggerTree
import org.moire.ultrasonic.log.timberLogger
import org.moire.ultrasonic.log.TimberKoinLogger
import org.moire.ultrasonic.util.Util
import timber.log.Timber
import timber.log.Timber.DebugTree
/**
* The Main class of the Application
*/
@Suppress("unused")
class UApp : MultiDexApplication() {
override fun onCreate() {
super.onCreate()
@ -29,12 +34,12 @@ class UApp : MultiDexApplication() {
}
startKoin {
// TODO Current version of Koin has a bug, which forces the usage of Level.ERROR
timberLogger(Level.ERROR)
logger(TimberKoinLogger(Level.INFO))
// declare Android context
androidContext(this@UApp)
// declare modules to use
modules(
applicationModule,
directoriesModule,
appPermanentStorage,
baseNetworkModule,

View File

@ -5,14 +5,16 @@ import org.koin.android.ext.koin.androidContext
import org.koin.android.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.moire.ultrasonic.activity.ServerSettingsModel
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.AppDatabase
import org.moire.ultrasonic.data.MIGRATION_1_2
import org.moire.ultrasonic.fragment.ServerSettingsModel
import org.moire.ultrasonic.util.Util
const val SP_NAME = "Default_SP"
/**
* This Koin module contains registration of classes related to permanent storage
*/
val appPermanentStorage = module {
single(named(SP_NAME)) { Util.getPreferences(androidContext()) }
@ -30,6 +32,4 @@ val appPermanentStorage = module {
single { get<AppDatabase>().serverSettingDao() }
viewModel { ServerSettingsModel(get(), get(), androidContext()) }
single { ActiveServerProvider(get(), androidContext()) }
}

View File

@ -0,0 +1,20 @@
package org.moire.ultrasonic.di
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.NowPlayingEventDistributor
import org.moire.ultrasonic.util.PermissionUtil
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
/**
* This Koin module contains the registration of general classes needed for Ultrasonic
*/
val applicationModule = module {
single { ActiveServerProvider(get(), androidContext()) }
single { ImageLoaderProvider(androidContext()) }
single { PermissionUtil(androidContext()) }
single { NowPlayingEventDistributor() }
single { ThemeChangedEventDistributor() }
}

View File

@ -4,7 +4,7 @@ import okhttp3.OkHttpClient
import org.koin.dsl.module
/**
* Provides base network dependencies.
* This Koin module provides base network dependencies.
*/
val baseNetworkModule = module {
single { OkHttpClient.Builder().build() }

View File

@ -5,6 +5,9 @@ import org.koin.dsl.module
import org.moire.ultrasonic.cache.AndroidDirectories
import org.moire.ultrasonic.cache.Directories
/**
* This Koin module contains the registration for Directories
*/
val directoriesModule = module {
single { AndroidDirectories(get()) } bind Directories::class
}

View File

@ -4,6 +4,9 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.moire.ultrasonic.featureflags.FeatureStorage
/**
* This Koin module contains the registration for the Feature Flags
*/
val featureFlagsModule = module {
factory { FeatureStorage(androidContext()) }
}

Some files were not shown because too many files have changed in this diff Show More