├── .gitignore ├── .idea ├── caches │ └── build_file_checksums.ser ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE.txt ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── max │ │ └── music_cyclon │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ ├── com │ │ ├── google │ │ │ └── samples │ │ │ │ └── apps │ │ │ │ └── iosched │ │ │ │ └── ui │ │ │ │ └── widget │ │ │ │ ├── SlidingTabLayout.java │ │ │ │ └── SlidingTabStrip.java │ │ └── maxmpz │ │ │ └── poweramp │ │ │ └── player │ │ │ └── PowerampAPI.java │ └── max │ │ └── music_cyclon │ │ ├── PagerAdapter.java │ │ ├── RenameDialogFragment.java │ │ ├── SynchronizeActivity.java │ │ ├── SynchronizeConfig.java │ │ ├── SynchronizeConfigFragment.java │ │ ├── preference │ │ └── MainPreferenceActivity.java │ │ ├── service │ │ ├── BeetsFetcher.java │ │ ├── DownloadTask.java │ │ ├── Item.java │ │ ├── LibraryService.java │ │ ├── PowerConnectionReceiver.java │ │ └── ProgressUpdater.java │ │ └── tracker │ │ ├── FileTracker.java │ │ ├── ForceClearReceiver.java │ │ └── LibraryDBOpenHelper.java │ └── res │ ├── drawable-hdpi │ ├── ic_add_white_48dp.png │ ├── ic_music_note_white_24dp.png │ └── ic_sync_white_24dp.png │ ├── drawable-mdpi │ ├── ic_add_white_48dp.png │ ├── ic_music_note_white_24dp.png │ └── ic_sync_white_24dp.png │ ├── drawable-xhdpi │ ├── ic_add_white_48dp.png │ ├── ic_music_note_white_24dp.png │ └── ic_sync_white_24dp.png │ ├── drawable-xxhdpi │ ├── ic_add_white_48dp.png │ ├── ic_music_note_white_24dp.png │ └── ic_sync_white_24dp.png │ ├── drawable-xxxhdpi │ ├── ic_add_white_48dp.png │ ├── ic_music_note_white_24dp.png │ └── ic_sync_white_24dp.png │ ├── layout │ ├── activity_main_preference.xml │ └── activity_synchronize.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values │ ├── colors.xml │ ├── default_config.xml │ ├── strings.xml │ └── style.xml │ └── xml │ ├── preferences.xml │ └── sync_config.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img └── screenshot │ ├── Screenshot_20160614-205250.png │ ├── Screenshot_20160614-205254.png │ └── Screenshot_20160614-205302.png ├── music-cyclon.iml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # music-cyclon 2 | 3 | Android app to synchronize music or files over network by using the [beets](https://github.com/beetbox/beets) web server. 4 | 5 | ## Preview 6 | 7 | 8 | 9 | ## Compilation 10 | 11 | The gradle build should work flawlessly 12 | 13 | ## License 14 | 15 | ![](http://www.wtfpl.net/wp-content/uploads/2012/12/wtfpl-badge-1.png) 16 | 17 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion '27.0.3' 6 | 7 | defaultConfig { 8 | applicationId "max.music_cyclon" 9 | minSdkVersion 14 10 | targetSdkVersion 23 11 | versionCode 4 12 | versionName "0.3.1" 13 | } 14 | compileOptions { 15 | sourceCompatibility JavaVersion.VERSION_1_7 16 | targetCompatibility JavaVersion.VERSION_1_7 17 | } 18 | } 19 | 20 | dependencies { 21 | compile 'com.squareup.okhttp3:okhttp:3.3.1' 22 | compile 'commons-io:commons-io:2.4' 23 | 24 | // compile 'com.android.support:appcompat-v7:23.4.0' 25 | // and 26 | // compile 'com.android.support:preference-v14:23.4.0' 27 | // are provided by: 28 | compile 'com.takisoft.fix:preference-v7:23.4.0.4' 29 | 30 | compile 'com.android.support:appcompat-v7:23.4.0' 31 | compile 'com.android.support:design:23.4.0' 32 | } 33 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/max/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/max/music_cyclon/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabLayout.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.iosched.ui.widget; 18 | 19 | import android.content.Context; 20 | import android.graphics.Typeface; 21 | import android.support.v4.view.PagerAdapter; 22 | import android.support.v4.view.ViewPager; 23 | import android.util.AttributeSet; 24 | import android.util.SparseArray; 25 | import android.util.TypedValue; 26 | import android.view.Gravity; 27 | import android.view.LayoutInflater; 28 | import android.view.View; 29 | import android.view.ViewGroup; 30 | import android.widget.HorizontalScrollView; 31 | import android.widget.LinearLayout; 32 | import android.widget.TextView; 33 | 34 | /** 35 | * To be used with ViewPager to provide a tab indicator component which give constant feedback as to 36 | * the user's scroll progress. 37 | *

38 | * To use the component, simply add it to your view hierarchy. Then in your 39 | * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call 40 | * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for. 41 | *

42 | * The colors can be customized in two ways. The first and simplest is to provide an array of colors 43 | * via {@link #setSelectedIndicatorColors(int...)}. The 44 | * alternative is via the {@link TabColorizer} interface which provides you complete control over 45 | * which color is used for any individual position. 46 | *

47 | * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, 48 | * providing the layout ID of your custom layout. 49 | */ 50 | public class SlidingTabLayout extends HorizontalScrollView { 51 | /** 52 | * Allows complete control over the colors drawn in the tab layout. Set with 53 | * {@link #setCustomTabColorizer(TabColorizer)}. 54 | */ 55 | public interface TabColorizer { 56 | 57 | /** 58 | * @return return the color of the indicator used when {@code position} is selected. 59 | */ 60 | int getIndicatorColor(int position); 61 | 62 | } 63 | 64 | private static final int TITLE_OFFSET_DIPS = 24; 65 | private static final int TAB_VIEW_PADDING_DIPS = 16; 66 | private static final int TAB_VIEW_TEXT_SIZE_SP = 12; 67 | 68 | private int mTitleOffset; 69 | 70 | private int mTabViewLayoutId; 71 | private int mTabViewTextViewId; 72 | private boolean mDistributeEvenly; 73 | 74 | private ViewPager mViewPager; 75 | private SparseArray mContentDescriptions = new SparseArray(); 76 | private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; 77 | 78 | private final SlidingTabStrip mTabStrip; 79 | 80 | private OnLongClickListener tabLongClickListener = null; 81 | 82 | public SlidingTabLayout(Context context) { 83 | this(context, null); 84 | } 85 | 86 | public SlidingTabLayout(Context context, AttributeSet attrs) { 87 | this(context, attrs, 0); 88 | } 89 | 90 | public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) { 91 | super(context, attrs, defStyle); 92 | 93 | // Disable the Scroll Bar 94 | setHorizontalScrollBarEnabled(false); 95 | // Make sure that the Tab Strips fills this View 96 | setFillViewport(true); 97 | 98 | mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); 99 | 100 | mTabStrip = new SlidingTabStrip(context); 101 | addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 102 | } 103 | 104 | /** 105 | * Set the custom {@link TabColorizer} to be used. 106 | * 107 | * If you only require simple custmisation then you can use 108 | * {@link #setSelectedIndicatorColors(int...)} to achieve 109 | * similar effects. 110 | */ 111 | public void setCustomTabColorizer(TabColorizer tabColorizer) { 112 | mTabStrip.setCustomTabColorizer(tabColorizer); 113 | } 114 | 115 | public void setDistributeEvenly(boolean distributeEvenly) { 116 | mDistributeEvenly = distributeEvenly; 117 | } 118 | 119 | /** 120 | * Sets the listener for long clicks on tabs 121 | * Should be set before calling {@link #setViewPager(ViewPager)} 122 | * 123 | * @param longClickListener The listener 124 | */ 125 | public void setTabLongClickListener(OnLongClickListener longClickListener) { 126 | this.tabLongClickListener = longClickListener; 127 | } 128 | 129 | /** 130 | * Sets the colors to be used for indicating the selected tab. These colors are treated as a 131 | * circular array. Providing one color will mean that all tabs are indicated with the same color. 132 | */ 133 | public void setSelectedIndicatorColors(int... colors) { 134 | mTabStrip.setSelectedIndicatorColors(colors); 135 | } 136 | 137 | /** 138 | * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are 139 | * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so 140 | * that the layout can update it's scroll position correctly. 141 | * 142 | * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) 143 | */ 144 | public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { 145 | mViewPagerPageChangeListener = listener; 146 | } 147 | 148 | /** 149 | * Set the custom layout to be inflated for the tab views. 150 | * 151 | * @param layoutResId Layout id to be inflated 152 | * @param textViewId id of the {@link TextView} in the inflated view 153 | */ 154 | public void setCustomTabView(int layoutResId, int textViewId) { 155 | mTabViewLayoutId = layoutResId; 156 | mTabViewTextViewId = textViewId; 157 | } 158 | 159 | /** 160 | * Sets the associated view pager. Note that the assumption here is that the pager content 161 | * (number of tabs and tab titles) does not change after this call has been made. 162 | */ 163 | public void setViewPager(ViewPager viewPager) { 164 | mTabStrip.removeAllViews(); 165 | 166 | mViewPager = viewPager; 167 | if (viewPager != null) { 168 | viewPager.setOnPageChangeListener(new InternalViewPagerListener()); 169 | populateTabStrip(); 170 | } 171 | } 172 | 173 | /** 174 | * Create a default view to be used for tabs. This is called if a custom tab view is not set via 175 | * {@link #setCustomTabView(int, int)}. 176 | */ 177 | protected TextView createDefaultTabView(Context context) { 178 | TextView textView = new TextView(context); 179 | textView.setGravity(Gravity.CENTER); 180 | textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); 181 | textView.setTypeface(Typeface.DEFAULT_BOLD); 182 | textView.setLayoutParams(new LinearLayout.LayoutParams( 183 | ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 184 | 185 | TypedValue outValue = new TypedValue(); 186 | getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, 187 | outValue, true); 188 | textView.setBackgroundResource(outValue.resourceId); 189 | textView.setAllCaps(true); 190 | 191 | int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density); 192 | textView.setPadding(padding, padding, padding, padding); 193 | 194 | return textView; 195 | } 196 | 197 | private void populateTabStrip() { 198 | final PagerAdapter adapter = mViewPager.getAdapter(); 199 | final View.OnClickListener tabClickListener = new TabClickListener(); 200 | 201 | for (int i = 0; i < adapter.getCount(); i++) { 202 | View tabView = null; 203 | TextView tabTitleView = null; 204 | 205 | if (mTabViewLayoutId != 0) { 206 | // If there is a custom tab view layout id set, try and inflate it 207 | tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, 208 | false); 209 | tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId); 210 | } 211 | 212 | if (tabView == null) { 213 | tabView = createDefaultTabView(getContext()); 214 | } 215 | 216 | if (tabTitleView == null && TextView.class.isInstance(tabView)) { 217 | tabTitleView = (TextView) tabView; 218 | } 219 | 220 | if (mDistributeEvenly) { 221 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabView.getLayoutParams(); 222 | lp.width = 0; 223 | lp.weight = 1; 224 | } 225 | 226 | tabTitleView.setText(adapter.getPageTitle(i)); 227 | tabView.setOnClickListener(tabClickListener); 228 | tabView.setOnLongClickListener(tabLongClickListener); 229 | String desc = mContentDescriptions.get(i, null); 230 | if (desc != null) { 231 | tabView.setContentDescription(desc); 232 | } 233 | 234 | mTabStrip.addView(tabView); 235 | if (i == mViewPager.getCurrentItem()) { 236 | tabView.setSelected(true); 237 | } 238 | } 239 | } 240 | 241 | public void setContentDescription(int i, String desc) { 242 | mContentDescriptions.put(i, desc); 243 | } 244 | 245 | @Override 246 | protected void onAttachedToWindow() { 247 | super.onAttachedToWindow(); 248 | 249 | if (mViewPager != null) { 250 | scrollToTab(mViewPager.getCurrentItem(), 0); 251 | } 252 | } 253 | 254 | private void scrollToTab(int tabIndex, int positionOffset) { 255 | final int tabStripChildCount = mTabStrip.getChildCount(); 256 | if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { 257 | return; 258 | } 259 | 260 | View selectedChild = mTabStrip.getChildAt(tabIndex); 261 | if (selectedChild != null) { 262 | int targetScrollX = selectedChild.getLeft() + positionOffset; 263 | 264 | if (tabIndex > 0 || positionOffset > 0) { 265 | // If we're not at the first child and are mid-scroll, make sure we obey the offset 266 | targetScrollX -= mTitleOffset; 267 | } 268 | 269 | scrollTo(targetScrollX, 0); 270 | } 271 | } 272 | 273 | private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { 274 | private int mScrollState; 275 | 276 | @Override 277 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 278 | int tabStripChildCount = mTabStrip.getChildCount(); 279 | if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { 280 | return; 281 | } 282 | 283 | mTabStrip.onViewPagerPageChanged(position, positionOffset); 284 | 285 | View selectedTitle = mTabStrip.getChildAt(position); 286 | int extraOffset = (selectedTitle != null) 287 | ? (int) (positionOffset * selectedTitle.getWidth()) 288 | : 0; 289 | scrollToTab(position, extraOffset); 290 | 291 | if (mViewPagerPageChangeListener != null) { 292 | mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, 293 | positionOffsetPixels); 294 | } 295 | } 296 | 297 | @Override 298 | public void onPageScrollStateChanged(int state) { 299 | mScrollState = state; 300 | 301 | if (mViewPagerPageChangeListener != null) { 302 | mViewPagerPageChangeListener.onPageScrollStateChanged(state); 303 | } 304 | } 305 | 306 | @Override 307 | public void onPageSelected(int position) { 308 | if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { 309 | mTabStrip.onViewPagerPageChanged(position, 0f); 310 | scrollToTab(position, 0); 311 | } 312 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 313 | mTabStrip.getChildAt(i).setSelected(position == i); 314 | } 315 | if (mViewPagerPageChangeListener != null) { 316 | mViewPagerPageChangeListener.onPageSelected(position); 317 | } 318 | } 319 | 320 | } 321 | 322 | private class TabClickListener implements View.OnClickListener { 323 | @Override 324 | public void onClick(View v) { 325 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 326 | if (v == mTabStrip.getChildAt(i)) { 327 | mViewPager.setCurrentItem(i); 328 | return; 329 | } 330 | } 331 | } 332 | } 333 | 334 | } -------------------------------------------------------------------------------- /app/src/main/java/com/google/samples/apps/iosched/ui/widget/SlidingTabStrip.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.samples.apps.iosched.ui.widget; 18 | 19 | import android.content.Context; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.Paint; 23 | import android.util.AttributeSet; 24 | import android.util.TypedValue; 25 | import android.view.View; 26 | import android.widget.LinearLayout; 27 | 28 | class SlidingTabStrip extends LinearLayout { 29 | 30 | private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 0; 31 | private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; 32 | private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 3; 33 | private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; 34 | 35 | private final int mBottomBorderThickness; 36 | private final Paint mBottomBorderPaint; 37 | 38 | private final int mSelectedIndicatorThickness; 39 | private final Paint mSelectedIndicatorPaint; 40 | 41 | private int mSelectedPosition; 42 | private float mSelectionOffset; 43 | 44 | private SlidingTabLayout.TabColorizer mCustomTabColorizer; 45 | private final SimpleTabColorizer mDefaultTabColorizer; 46 | 47 | SlidingTabStrip(Context context) { 48 | this(context, null); 49 | } 50 | 51 | SlidingTabStrip(Context context, AttributeSet attrs) { 52 | super(context, attrs); 53 | setWillNotDraw(false); 54 | 55 | final float density = getResources().getDisplayMetrics().density; 56 | 57 | TypedValue outValue = new TypedValue(); 58 | context.getTheme().resolveAttribute(android.R.attr.colorForeground, outValue, true); 59 | final int themeForegroundColor = outValue.data; 60 | 61 | int defaultBottomBorderColor = setColorAlpha(themeForegroundColor, 62 | DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); 63 | 64 | mDefaultTabColorizer = new SimpleTabColorizer(); 65 | mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); 66 | 67 | mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); 68 | mBottomBorderPaint = new Paint(); 69 | mBottomBorderPaint.setColor(defaultBottomBorderColor); 70 | 71 | mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); 72 | mSelectedIndicatorPaint = new Paint(); 73 | } 74 | 75 | void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) { 76 | mCustomTabColorizer = customTabColorizer; 77 | invalidate(); 78 | } 79 | 80 | void setSelectedIndicatorColors(int... colors) { 81 | // Make sure that the custom colorizer is removed 82 | mCustomTabColorizer = null; 83 | mDefaultTabColorizer.setIndicatorColors(colors); 84 | invalidate(); 85 | } 86 | 87 | void onViewPagerPageChanged(int position, float positionOffset) { 88 | mSelectedPosition = position; 89 | mSelectionOffset = positionOffset; 90 | invalidate(); 91 | } 92 | 93 | @Override 94 | protected void onDraw(Canvas canvas) { 95 | final int height = getHeight(); 96 | final int childCount = getChildCount(); 97 | final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null 98 | ? mCustomTabColorizer 99 | : mDefaultTabColorizer; 100 | 101 | // Thick colored underline below the current selection 102 | if (childCount > 0) { 103 | View selectedTitle = getChildAt(mSelectedPosition); 104 | int left = selectedTitle.getLeft(); 105 | int right = selectedTitle.getRight(); 106 | int color = tabColorizer.getIndicatorColor(mSelectedPosition); 107 | 108 | if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { 109 | int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); 110 | if (color != nextColor) { 111 | color = blendColors(nextColor, color, mSelectionOffset); 112 | } 113 | 114 | // Draw the selection partway between the tabs 115 | View nextTitle = getChildAt(mSelectedPosition + 1); 116 | left = (int) (mSelectionOffset * nextTitle.getLeft() + 117 | (1.0f - mSelectionOffset) * left); 118 | right = (int) (mSelectionOffset * nextTitle.getRight() + 119 | (1.0f - mSelectionOffset) * right); 120 | } 121 | 122 | mSelectedIndicatorPaint.setColor(color); 123 | 124 | canvas.drawRect(left, height - mSelectedIndicatorThickness, right, 125 | height, mSelectedIndicatorPaint); 126 | } 127 | 128 | // Thin underline along the entire bottom edge 129 | canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); 130 | } 131 | 132 | /** 133 | * Set the alpha value of the {@code color} to be the given {@code alpha} value. 134 | */ 135 | private static int setColorAlpha(int color, byte alpha) { 136 | return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); 137 | } 138 | 139 | /** 140 | * Blend {@code color1} and {@code color2} using the given ratio. 141 | * 142 | * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, 143 | * 0.0 will return {@code color2}. 144 | */ 145 | private static int blendColors(int color1, int color2, float ratio) { 146 | final float inverseRation = 1f - ratio; 147 | float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); 148 | float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); 149 | float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); 150 | return Color.rgb((int) r, (int) g, (int) b); 151 | } 152 | 153 | private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer { 154 | private int[] mIndicatorColors; 155 | 156 | @Override 157 | public final int getIndicatorColor(int position) { 158 | return mIndicatorColors[position % mIndicatorColors.length]; 159 | } 160 | 161 | void setIndicatorColors(int... colors) { 162 | mIndicatorColors = colors; 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /app/src/main/java/com/maxmpz/poweramp/player/PowerampAPI.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2011-2013 Maksim Petrov 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted for widgets, plugins, applications and other software 6 | which communicate with Poweramp application on Android platform. 7 | 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 9 | ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 10 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 11 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR 12 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 13 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 14 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 15 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 16 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 17 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 18 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | */ 20 | 21 | package com.maxmpz.poweramp.player; 22 | 23 | import android.content.ComponentName; 24 | import android.content.Intent; 25 | import android.net.Uri; 26 | 27 | 28 | /** 29 | * Poweramp intent based API. 30 | */ 31 | public final class PowerampAPI { 32 | /** 33 | * Defines PowerampAPI version, which could be also 200 and 210 for older Poweramps. 34 | */ 35 | public static final int VERSION = 533; 36 | 37 | /** 38 | * No id flag. 39 | */ 40 | public static final int NO_ID = 0; 41 | 42 | public static final String AUTHORITY = "com.maxmpz.audioplayer.data"; 43 | 44 | public static final Uri ROOT_URI = new Uri.Builder().scheme("content").authority(AUTHORITY).build(); 45 | 46 | /** 47 | * Uri query parameter - filter. 48 | */ 49 | public static final String PARAM_FILTER = "flt"; 50 | /** 51 | * Uri query parameter - shuffle mode. 52 | */ 53 | public static final String PARAM_SHUFFLE = "shf"; 54 | 55 | 56 | /** 57 | * Poweramp Control action. 58 | * Should be sent with sendBroadcast(). 59 | * Extras: 60 | * - cmd - int - command to execute. 61 | */ 62 | public static final String ACTION_API_COMMAND = "com.maxmpz.audioplayer.API_COMMAND"; 63 | 64 | public static Intent newAPIIntent() { 65 | return new Intent(ACTION_API_COMMAND).setComponent(PLAYER_SERVICE_COMPONENT_NAME); 66 | } 67 | 68 | /** 69 | * ACTION_API_COMMAND extra. 70 | * Int. 71 | */ 72 | public static final String COMMAND = "cmd"; 73 | 74 | 75 | /** 76 | * 77 | * Commonm extras: 78 | * - beep - boolean - (optional) if true, Poweramp will beep on playback command 79 | */ 80 | public static final class Commands { 81 | /** 82 | * Extras: 83 | * - keepService - boolean - (optional) if true, Poweramp won't unload player service. Notification will be appropriately updated. 84 | */ 85 | public static final int TOGGLE_PLAY_PAUSE = 1; 86 | /** 87 | * Extras: 88 | * - keepService - boolean - (optional) if true, Poweramp won't unload player service. Notification will be appropriately updated. 89 | */ 90 | public static final int PAUSE = 2; 91 | public static final int RESUME = 3; 92 | /** 93 | * NOTE: subject to 200ms throttling. 94 | */ 95 | public static final int NEXT = 4; 96 | /** 97 | * NOTE: subject to 200ms throttling. 98 | */ 99 | public static final int PREVIOUS = 5; 100 | /** 101 | * NOTE: subject to 200ms throttling. 102 | */ 103 | public static final int NEXT_IN_CAT = 6; 104 | /** 105 | * NOTE: subject to 200ms throttling. 106 | */ 107 | public static final int PREVIOUS_IN_CAT = 7; 108 | /** 109 | * Extras: 110 | * - showToast - boolean - (optional) if false, no toast will be shown. Applied for cycle only. 111 | * - repeat - int - (optional) if exists, appropriate mode will be directly selected, otherwise modes will be cycled, see Repeat class. 112 | */ 113 | public static final int REPEAT = 8; 114 | /** 115 | * Extras: 116 | * - showToast - boolean - (optional) if false, no toast will be shown. Applied for cycle only. 117 | * - shuffle - int - (optional) if exists, appropriate mode will be directly selected, otherwise modes will be cycled, see Shuffle class. 118 | */ 119 | public static final int SHUFFLE = 9; 120 | public static final int BEGIN_FAST_FORWARD = 10; 121 | public static final int END_FAST_FORWARD = 11; 122 | public static final int BEGIN_REWIND = 12; 123 | public static final int END_REWIND = 13; 124 | public static final int STOP = 14; 125 | /** 126 | * Extras: 127 | * - pos - int - seek position in seconds. 128 | */ 129 | public static final int SEEK = 15; 130 | public static final int POS_SYNC = 16; 131 | 132 | /** 133 | * Data: 134 | * - uri, following URIs are recognized: 135 | * - file://path 136 | * - content://com.maxmpz.audioplayer.data/... (see below) 137 | * 138 | * # means some numeric id (track id for queries ending with /files, otherwise - appropriate category id). 139 | * If song id (in place of #) is not specified, Poweramp plays whole list starting from the specified song, 140 | * or from first one, or from random one in shuffle mode. 141 | * 142 | * All queries support following params (added as URL encoded params, e.g. content://com.maxmpz.audioplayer.data/files?lim=10&flt=foo): 143 | * lim - integer - SQL LIMIT, which limits number of rows returned 144 | * flt - string - filter substring. Poweramp will return only matching rows (the same way as returned in Poweramp lists UI when filter is used). 145 | * hier - long - hierarchy folder id. Used only to play in shuffle lists/shuffle songs mode while in hierarchy folders view. This is the target folder id 146 | * which will be shuffled with the all subfolders in it as one list. 147 | * shf - integer - shuffle mode (see ShuffleMode class) 148 | * ssid - long - shuffle session id (for internal use) 149 | * 150 | * Each /files/meta subquery returns special crafted query with some metainformation provided (it differs in each category, you can explore it by analizing the cols returned). 151 | 152 | - All Songs: 153 | content://com.maxmpz.audioplayer.data/files 154 | content://com.maxmpz.audioplayer.data/files/meta 155 | content://com.maxmpz.audioplayer.data/files/# 156 | 157 | - Most Played 158 | content://com.maxmpz.audioplayer.data/most_played 159 | content://com.maxmpz.audioplayer.data/most_played/files 160 | content://com.maxmpz.audioplayer.data/most_played/files/meta 161 | content://com.maxmpz.audioplayer.data/most_played/files/# 162 | 163 | - Top Rated 164 | content://com.maxmpz.audioplayer.data/top_rated 165 | content://com.maxmpz.audioplayer.data/top_rated/files 166 | content://com.maxmpz.audioplayer.data/top_rated/files/meta 167 | content://com.maxmpz.audioplayer.data/top_rated/files/# 168 | 169 | - Recently Added 170 | content://com.maxmpz.audioplayer.data/recently_added 171 | content://com.maxmpz.audioplayer.data/recently_added/files 172 | content://com.maxmpz.audioplayer.data/recently_added/files/meta 173 | content://com.maxmpz.audioplayer.data/recently_added/files/# 174 | 175 | - Recently Played 176 | content://com.maxmpz.audioplayer.data/recently_played 177 | content://com.maxmpz.audioplayer.data/recently_played/files 178 | content://com.maxmpz.audioplayer.data/recently_played/files/meta 179 | content://com.maxmpz.audioplayer.data/recently_played/files/# 180 | 181 | - Plain folders view (just files in plain folders list) 182 | content://com.maxmpz.audioplayer.data/folders 183 | content://com.maxmpz.audioplayer.data/folders/# 184 | content://com.maxmpz.audioplayer.data/folders/#/files 185 | content://com.maxmpz.audioplayer.data/folders/#/files/meta 186 | content://com.maxmpz.audioplayer.data/folders/#/files/# 187 | 188 | - Hierarchy folders view (files and folders intermixed in one cursor) 189 | content://com.maxmpz.audioplayer.data/folders/#/folders_and_files 190 | content://com.maxmpz.audioplayer.data/folders/#/folders_and_files/meta 191 | content://com.maxmpz.audioplayer.data/folders/#/folders_and_files/# 192 | content://com.maxmpz.audioplayer.data/folders/files // All folder files, sorted as folders_files sort (for mass ops). 193 | 194 | - Genres 195 | content://com.maxmpz.audioplayer.data/genres 196 | content://com.maxmpz.audioplayer.data/genres/#/files 197 | content://com.maxmpz.audioplayer.data/genres/#/files/meta 198 | content://com.maxmpz.audioplayer.data/genres/#/files/# 199 | content://com.maxmpz.audioplayer.data/genres/files 200 | 201 | - Artists 202 | content://com.maxmpz.audioplayer.data/artists 203 | content://com.maxmpz.audioplayer.data/artists/# 204 | content://com.maxmpz.audioplayer.data/artists/#/files 205 | content://com.maxmpz.audioplayer.data/artists/#/files/meta 206 | content://com.maxmpz.audioplayer.data/artists/#/files/# 207 | content://com.maxmpz.audioplayer.data/artists/files 208 | 209 | - Composers 210 | content://com.maxmpz.audioplayer.data/composers 211 | content://com.maxmpz.audioplayer.data/composers/# 212 | content://com.maxmpz.audioplayer.data/composers/#/files 213 | content://com.maxmpz.audioplayer.data/composers/#/files/# 214 | content://com.maxmpz.audioplayer.data/composers/#/files/meta 215 | content://com.maxmpz.audioplayer.data/composers/files 216 | 217 | - Albums 218 | content://com.maxmpz.audioplayer.data/albums 219 | content://com.maxmpz.audioplayer.data/albums/#/files 220 | content://com.maxmpz.audioplayer.data/albums/#/files/# 221 | content://com.maxmpz.audioplayer.data/albums/#/files/meta 222 | content://com.maxmpz.audioplayer.data/albums/files 223 | 224 | - Albums by Genres 225 | content://com.maxmpz.audioplayer.data/genres/#/albums 226 | content://com.maxmpz.audioplayer.data/genres/#/albums/meta 227 | content://com.maxmpz.audioplayer.data/genres/#/albums/#/files 228 | content://com.maxmpz.audioplayer.data/genres/#/albums/#/files/# 229 | content://com.maxmpz.audioplayer.data/genres/#/albums/#/files/meta 230 | content://com.maxmpz.audioplayer.data/genres/#/albums/files 231 | content://com.maxmpz.audioplayer.data/genres/albums 232 | 233 | - Albums by Artists 234 | content://com.maxmpz.audioplayer.data/artists/#/albums 235 | content://com.maxmpz.audioplayer.data/artists/#/albums/meta 236 | content://com.maxmpz.audioplayer.data/artists/#/albums/#/files 237 | content://com.maxmpz.audioplayer.data/artists/#/albums/#/files/# 238 | content://com.maxmpz.audioplayer.data/artists/#/albums/#/files/meta 239 | content://com.maxmpz.audioplayer.data/artists/#/albums/files 240 | content://com.maxmpz.audioplayer.data/artists/albums 241 | 242 | - Albums by Composers 243 | content://com.maxmpz.audioplayer.data/composers/#/albums 244 | content://com.maxmpz.audioplayer.data/composers/#/albums/meta 245 | content://com.maxmpz.audioplayer.data/composers/#/albums/#/files 246 | content://com.maxmpz.audioplayer.data/composers/#/albums/#/files/# 247 | content://com.maxmpz.audioplayer.data/composers/#/albums/#/files/meta 248 | content://com.maxmpz.audioplayer.data/composers/#/albums/files 249 | content://com.maxmpz.audioplayer.data/composers/albums 250 | 251 | - Artists Albums 252 | content://com.maxmpz.audioplayer.data/artists_albums 253 | content://com.maxmpz.audioplayer.data/artists_albums/meta 254 | content://com.maxmpz.audioplayer.data/artists_albums/#/files 255 | content://com.maxmpz.audioplayer.data/artists_albums/#/files/# 256 | content://com.maxmpz.audioplayer.data/artists_albums/#/files/meta 257 | content://com.maxmpz.audioplayer.data/artists_albums/files 258 | 259 | - Playlists 260 | content://com.maxmpz.audioplayer.data/playlists 261 | content://com.maxmpz.audioplayer.data/playlists/# 262 | content://com.maxmpz.audioplayer.data/playlists/#/files 263 | content://com.maxmpz.audioplayer.data/playlists/#/files/# 264 | content://com.maxmpz.audioplayer.data/playlists/#/files/meta 265 | content://com.maxmpz.audioplayer.data/playlists/files 266 | 267 | - Library Search 268 | content://com.maxmpz.audioplayer.data/search 269 | 270 | - Equalizer Presets 271 | content://com.maxmpz.audioplayer.data/eq_presets 272 | content://com.maxmpz.audioplayer.data/eq_presets/# 273 | content://com.maxmpz.audioplayer.data/eq_presets_songs 274 | content://com.maxmpz.audioplayer.data/queue 275 | content://com.maxmpz.audioplayer.data/queue/# 276 | 277 | * 278 | * Extras: 279 | * - paused - boolean - (optional) default false. OPEN_TO_PLAY command starts playing the file immediately, unless "paused" extra is true. 280 | * (see PowerampAPI.PAUSED) 281 | * 282 | * - pos - int - (optional) seek to this position in song before playing (see PowerampAPI.Track.POSITION) 283 | */ 284 | public static final int OPEN_TO_PLAY = 20; 285 | 286 | /** 287 | * Extras: 288 | * - id - long - preset ID 289 | */ 290 | public static final int SET_EQU_PRESET = 50; 291 | 292 | /** 293 | * Extras: 294 | * - value - string - equalizer values, see ACTION_EQU_CHANGED description. 295 | */ 296 | public static final int SET_EQU_STRING = 51; 297 | 298 | /** 299 | * Extras: 300 | * - name - string - equalizer band (bass/treble/preamp/31/62../8K/16K) name 301 | * - value - float - equalizer band value (bass/treble/, 31/62../8K/16K => -1.0...1.0, preamp => 0..2.0) 302 | */ 303 | public static final int SET_EQU_BAND = 52; 304 | 305 | /** 306 | * Extras: 307 | * - equ - boolean - if exists and true, equalizer is enabled 308 | * - tone - boolean - if exists and true, tone is enabled 309 | */ 310 | public static final int SET_EQU_ENABLED = 53; 311 | 312 | /** 313 | * Used by Notification controls to stop pending/paused service/playback and unload/remove notification. 314 | * Since 2.0.6 315 | */ 316 | public static final int STOP_SERVICE = 100; 317 | } 318 | 319 | /** 320 | * Extra. 321 | * Mixed. 322 | */ 323 | public static final String API_VERSION = "api"; 324 | 325 | /** 326 | * Extra. 327 | * Mixed. 328 | */ 329 | public static final String CONTENT = "content"; 330 | 331 | /** 332 | * Extra. 333 | * String. 334 | */ 335 | public static final String PACKAGE = "pak"; 336 | 337 | /** 338 | * Extra. 339 | * String. 340 | */ 341 | public static final String LABEL = "label"; 342 | 343 | /** 344 | * Extra. 345 | * Boolean. 346 | */ 347 | public static final String AUTO_HIDE = "autoHide"; 348 | 349 | /** 350 | * Extra. 351 | * Bitmap. 352 | */ 353 | public static final String ICON = "icon"; 354 | 355 | /** 356 | * Extra. 357 | * Boolean. 358 | */ 359 | public static final String MATCH_FILE = "matchFile"; 360 | 361 | /** 362 | * Extra. 363 | * Boolean 364 | */ 365 | public static final String SHOW_TOAST = "showToast"; 366 | 367 | /** 368 | * Extra. 369 | * String. 370 | */ 371 | public static final String NAME = "name"; 372 | 373 | /** 374 | * Extra. 375 | * Mixed. 376 | */ 377 | public static final String VALUE = "value"; 378 | 379 | /** 380 | * Extra. 381 | * Boolean. 382 | */ 383 | public static final String EQU = "equ"; 384 | 385 | /** 386 | * Extra. 387 | * Boolean. 388 | */ 389 | public static final String TONE = "tone"; 390 | 391 | /** 392 | * Extra. 393 | * Boolean. 394 | * Since 2.0.6 395 | */ 396 | public static final String KEEP_SERVICE = "keepService"; 397 | 398 | /** 399 | * Extra. 400 | * Boolean 401 | * Since build 533 402 | */ 403 | public static final String BEEP = "beep"; 404 | 405 | 406 | /** 407 | * Poweramp track changed. 408 | * Sticky intent. 409 | * Extras: 410 | * - track - bundle - Track bundle, see Track class. 411 | * - ts - long - timestamp of the event (System.currentTimeMillis()). 412 | * Note, that by default Poweramp won't search/download album art when screen is OFF, but will do that on next screen ON event. 413 | */ 414 | public static final String ACTION_TRACK_CHANGED = "com.maxmpz.audioplayer.TRACK_CHANGED"; 415 | 416 | /** 417 | * Album art was changed. Album art can be the same for whole album/folder, thus usually it will be updated less frequently comparing to TRACK_CHANGE. 418 | * If both aaPath and aaBitmap extras are missing that means no album art exists for the current track(s). 419 | * Note that there is no direct Album Art to track relation, i.e. both track and album art can change independently from each other - 420 | * for example - when new album art asynchronously downloaded from internet or selected by user. 421 | * Sticky intent. 422 | * Extras: 423 | * - aaPath - String - (optional) if exists, direct path to the cached album art is available. 424 | * - aaBitmap - Bitmap - (optional) if exists, some rescaled up to 500x500 px album art bitmap is available. 425 | * There will be aaBitmap if aaPath is available, but image is bigger than 600x600 px. 426 | * - delayed - boolean - (optional) if true, this album art was downloaded or selected later by user. 427 | 428 | * - ts - long - timestamp of the event (System.currentTimeMillis()). 429 | */ 430 | public static final String ACTION_AA_CHANGED = "com.maxmpz.audioplayer.AA_CHANGED"; 431 | 432 | /** 433 | * Poweramp playing status changed (track started/paused/resumed/ended, playing ended). 434 | * Sticky intent. 435 | * Extras: 436 | * - status - string - one of the STATUS_* values 437 | * - pos - int - (optional) current in-track position in seconds. 438 | * - ts - long - timestamp of the event (System.currentTimeMillis()). 439 | * - additional extras - depending on STATUS_ value (see STATUS_* description below). 440 | */ 441 | public static final String ACTION_STATUS_CHANGED = "com.maxmpz.audioplayer.STATUS_CHANGED"; 442 | 443 | /** 444 | * NON sticky intent. 445 | * - pos - int - current in-track position in seconds. 446 | */ 447 | public static final String ACTION_TRACK_POS_SYNC = "com.maxmpz.audioplayer.TPOS_SYNC"; 448 | 449 | /** 450 | * Poweramp repeat or shuffle mode changed. 451 | * Sticky intent. 452 | * Extras: 453 | * - repeat - int - new repeat mode. See RepeatMode class. 454 | * - shuffle - int - new shuffle mode. See ShuffleMode class. 455 | * - ts - long - timestamp of the event (System.currentTimeMillis()). * 456 | */ 457 | public static final String ACTION_PLAYING_MODE_CHANGED = "com.maxmpz.audioplayer.PLAYING_MODE_CHANGED"; 458 | 459 | /** 460 | * Poweramp equalizer settings changed. 461 | * Sticky intent. 462 | * Extras: 463 | * - name - string - preset name. If no name extra exists, it's not a preset. 464 | * - id - long - preset id. If no id extra exists, it's not a preset. 465 | * - value - string - equalizer and tone values in format: 466 | * bass=pos_float|treble=pos_float|31=float|62=float|....|16K=float|preamp=0.0 ... 2.0 467 | * where float = -1.0 ... 1.0, pos_float = 0.0 ... 1.0 468 | * - equ - boolean - true if equalizer bands are enabled 469 | * - tone - boolean - truel if tone bands are enabled 470 | * - ts - long - timestamp of the event (System.currentTimeMillis()). 471 | */ 472 | public static final String ACTION_EQU_CHANGED = "com.maxmpz.audioplayer.EQU_CHANGED"; 473 | 474 | /** 475 | * Special actions for com.maxmpz.audioplayer.PlayerUIActivity only. 476 | */ 477 | public static final String ACTION_SHOW_CURRENT = "com.maxmpz.audioplayer.ACTION_SHOW_CURRENT"; 478 | public static final String ACTION_SHOW_LIST = "com.maxmpz.audioplayer.ACTION_SHOW_LIST"; 479 | 480 | 481 | public static final String PACKAGE_NAME = "com.maxmpz.audioplayer"; 482 | public static final String PLAYER_SERVICE_NAME = "com.maxmpz.audioplayer.player.PlayerService"; 483 | 484 | public static final ComponentName PLAYER_SERVICE_COMPONENT_NAME = new ComponentName(PACKAGE_NAME, PLAYER_SERVICE_NAME); 485 | 486 | public static final String ACTIVITY_PLAYER_UI = "com.maxmpz.audioplayer.PlayerUIActivity"; 487 | public static final String ACTIVITY_EQ = "com.maxmpz.audioplayer.EqActivity"; 488 | 489 | /** 490 | * If com.maxmpz.audioplayer.ACTION_SHOW_LIST action is sent to this activity, it will react to some extras. 491 | * Extras: 492 | * Data: 493 | * - uri - uri of the list to display. 494 | */ 495 | public static final String ACTIVITY_PLAYLIST = "com.maxmpz.audioplayer.PlayListActivity"; 496 | public static final String ACTIVITY_SETTINGS = "com.maxmpz.audioplayer.preference.SettingsActivity"; 497 | 498 | /** 499 | * Extra. 500 | * String. 501 | */ 502 | public static final String ALBUM_ART_PATH = "aaPath"; 503 | 504 | /** 505 | * Extra. 506 | * Bitmap. 507 | */ 508 | public static final String ALBUM_ART_BITMAP = "aaBitmap"; 509 | 510 | /** 511 | * Extra. 512 | * boolean. 513 | */ 514 | public static final String DELAYED = "delayed"; 515 | 516 | 517 | /** 518 | * Extra. 519 | * long. 520 | */ 521 | public static final String TIMESTAMP = "ts"; 522 | 523 | /** 524 | * STATUS_CHANGED extra. See Status class for values. 525 | * Int. 526 | */ 527 | public static final String STATUS = "status"; 528 | 529 | /** 530 | * STATUS extra values. 531 | */ 532 | public static final class Status { 533 | /** 534 | * STATUS_CHANGED status value - track has been started to play or has been paused. 535 | * Note that Poweramp will start track immediately into this state when it's just loaded to avoid STARTED => PAUSED transition. 536 | * Additional extras: 537 | * track - bundle - track info 538 | * paused - boolean - true if track paused, false if track resumed 539 | */ 540 | public static final int TRACK_PLAYING = 1; 541 | 542 | /** 543 | * STATUS_CHANGED status value - track has been ended. Note, this intent will NOT be sent for just finished song IF Poweramp advances to the next song. 544 | * Additional extras: 545 | * track - bundle - track info 546 | * failed - boolean - true if track failed to play 547 | */ 548 | public static final int TRACK_ENDED = 2; 549 | 550 | /** 551 | * STATUS_CHANGED status value - Poweramp finished playing some list and stopped. 552 | */ 553 | public static final int PLAYING_ENDED = 3; 554 | } 555 | 556 | 557 | /** 558 | * STATUS_CHANGED trackEnded extra. 559 | * Boolean. True if track failed to play. 560 | */ 561 | public static final String FAILED = "failed"; 562 | 563 | /** 564 | * STATUS_CHANGED trackStarted/trackPausedResumed extra. 565 | * Boolean. True if track is paused. 566 | */ 567 | public static final String PAUSED = "paused"; 568 | 569 | /** 570 | * PLAYING_MODE_CHANGED extra. See ShuffleMode class. 571 | * Integer. 572 | */ 573 | public static final String SHUFFLE = "shuffle"; 574 | 575 | /** 576 | * PLAYING_MODE_CHANGED extra. See RepeatMode class. 577 | * Integer. 578 | */ 579 | public static final String REPEAT = "repeat"; 580 | 581 | 582 | /** 583 | * Extra. 584 | * Long. 585 | */ 586 | public static final String ID = "id"; 587 | 588 | /** 589 | * STATUS_CHANGED track extra. 590 | * Bundle. 591 | */ 592 | public static final String TRACK = "track"; 593 | 594 | 595 | /** 596 | * shuffle extras values. 597 | */ 598 | public static final class ShuffleMode { 599 | public static final int SHUFFLE_NONE = 0; 600 | public static final int SHUFFLE_ALL = 1; 601 | public static final int SHUFFLE_SONGS = 2; 602 | public static final int SHUFFLE_CATS = 3; // Songs in order. 603 | public static final int SHUFFLE_SONGS_AND_CATS = 4; // Songs shuffled. 604 | } 605 | 606 | /** 607 | * repeat extras values. 608 | */ 609 | public static final class RepeatMode { 610 | public static final int REPEAT_NONE = 0; 611 | public static final int REPEAT_ON = 1; 612 | public static final int REPEAT_ADVANCE = 2; 613 | public static final int REPEAT_SONG = 3; 614 | } 615 | 616 | 617 | /** 618 | * STATUS_CHANGED track extra fields. 619 | */ 620 | public static final class Track { 621 | /** 622 | * Id of the current track. 623 | * Can be a playlist entry id. 624 | * Long. 625 | */ 626 | public static final String ID = "id"; 627 | 628 | /** 629 | * "Real" id. In case of playlist entry, this is always resolved to Poweramp folder_files table row ID or System Library MediaStorage.Audio._ID. 630 | * Long. 631 | */ 632 | public static final String REAL_ID = "realId"; 633 | 634 | /** 635 | * Category type. 636 | * See Track.Type class. 637 | * Int. 638 | */ 639 | public static final String TYPE = "type"; 640 | 641 | /** 642 | * Category URI match. 643 | * Int. 644 | */ 645 | public static final String CAT = "cat"; 646 | 647 | /** 648 | * Boolean. 649 | */ 650 | public static final String IS_CUE = "isCue"; 651 | 652 | /** 653 | * Category URI. 654 | * Uri. 655 | */ 656 | public static final String CAT_URI = "catUri"; 657 | 658 | /** 659 | * File type. See Track.FileType. 660 | * Integer. 661 | */ 662 | public static final String FILE_TYPE = "fileType"; 663 | 664 | /** 665 | * Song file path. 666 | * String 667 | */ 668 | public static final String PATH = "path"; 669 | 670 | /** 671 | * Song title. 672 | * String 673 | */ 674 | public static final String TITLE = "title"; 675 | 676 | /** 677 | * Song album. 678 | * String. 679 | */ 680 | public static final String ALBUM = "album"; 681 | 682 | /** 683 | * Song artist. 684 | * String. 685 | */ 686 | public static final String ARTIST = "artist"; 687 | 688 | /** 689 | * Song duration in seconds. 690 | * Int. 691 | */ 692 | public static final String DURATION = "dur"; 693 | 694 | /** 695 | * Position in song in seconds. 696 | * Int. 697 | */ 698 | public static final String POSITION = "pos"; 699 | 700 | /** 701 | * Position in a list. 702 | * Int. 703 | */ 704 | public static final String POS_IN_LIST = "posInList"; 705 | 706 | /** 707 | * List size. 708 | * Int. 709 | */ 710 | public static final String LIST_SIZE = "listSize"; 711 | 712 | /** 713 | * Song sample rate. 714 | * Int. 715 | */ 716 | public static final String SAMPLE_RATE = "sampleRate"; 717 | 718 | /** 719 | * Song number of channels. 720 | * Int. 721 | */ 722 | public static final String CHANNELS = "channels"; 723 | 724 | /** 725 | * Song average bitrate. 726 | * Int. 727 | */ 728 | public static final String BITRATE = "bitRate"; 729 | 730 | /** 731 | * Resolved codec name for the song. 732 | * Int. 733 | */ 734 | public static final String CODEC = "codec"; 735 | 736 | /** 737 | * Track flags. 738 | * Int. 739 | */ 740 | public static final String FLAGS = "flags"; 741 | 742 | /** 743 | * Track.fileType values. 744 | */ 745 | public static final class FileType { 746 | public static final int mp3 = 0; 747 | public static final int flac = 1; 748 | public static final int m4a = 2; 749 | public static final int mp4 = 3; 750 | public static final int ogg = 4; 751 | public static final int wma = 5; 752 | public static final int wav = 6; 753 | public static final int tta = 7; 754 | public static final int ape = 8; 755 | public static final int wv = 9; 756 | public static final int aac = 10; 757 | public static final int mpga = 11; 758 | public static final int amr = 12; 759 | public static final int _3gp = 13; 760 | public static final int mpc = 14; 761 | public static final int aiff = 15; 762 | public static final int aif = 16; 763 | } 764 | 765 | /** 766 | * Track.flags bitset values. First 3 bits = FLAG_ADVANCE_* 767 | */ 768 | public static final class Flags { 769 | public static final int FLAG_ADVANCE_NONE = 0; 770 | public static final int FLAG_ADVANCE_FORWARD = 1; 771 | public static final int FLAG_ADVANCE_BACKWARD = 2; 772 | public static final int FLAG_ADVANCE_FORWARD_CAT = 3; 773 | public static final int FLAG_ADVANCE_BACKWARD_CAT = 4; 774 | 775 | public static final int FLAG_ADVANCE_MASK = 0x7; // 111 776 | 777 | public static final int FLAG_NOTIFICATION_UI = 0x20; 778 | public static final int FLAG_FIRST_IN_PLAYER_SESSION = 0x40; // Currently used just to indicate that track is first in playerservice session. 779 | } 780 | } 781 | 782 | public static final class Cats { 783 | public static final int ROOT = 0; 784 | public static final int FOLDERS = 10; 785 | public static final int GENRES_ID_ALBUMS = 210; 786 | public static final int ALBUMS = 200; 787 | public static final int GENRES = 320; 788 | public static final int ARTISTS = 500; 789 | public static final int ARTISTS_ID_ALBUMS = 220; 790 | public static final int ARTISTS__ALBUMS = 250; 791 | public static final int COMPOSERS = 600; 792 | public static final int COMPOSERS_ID_ALBUMS = 230; 793 | public static final int PLAYLISTS = 100; 794 | public static final int QUEUE = 800; 795 | public static final int MOST_PLAYED = 43; 796 | public static final int TOP_RATED = 48; 797 | public static final int RECENTLY_ADDED = 53; 798 | public static final int RECENTLY_PLAYED = 58; 799 | } 800 | 801 | 802 | public static final class Scanner { 803 | 804 | /** 805 | * Poweramp Scanner action. 806 | * 807 | * Poweramp Scanner scanning process is 2 step: 808 | * 1. Folders scan. 809 | * Checks filesystem and updates DB with folders/files structure. 810 | * 2. Tags scan. 811 | * Iterates over files in DB with TAG_STATUS == TAG_NOT_SCANNED and scans them with tag scanner. 812 | * 813 | * Poweramp Scanner is a IntentService, this means multiple scan requests at the same time (or during another scans) are queued. 814 | * ACTION_SCAN_DIRS actions are prioritized and executed before ACTION_SCAN_TAGS. 815 | * 816 | * Poweramp main scan action, which scans either incrementally or from scratch the set of folders, which is configured by user in Poweramp Settings. 817 | * Poweramp will always do ACTION_SCAN_TAGS automatically after ACTION_SCAN_DIRS is finished and some changes are required to song tags in DB. 818 | * Unless, fullRescan specified, Poweramp will not remove songs if they are missing from filesystem due to unmounted storages. 819 | * Normal menu => Rescan calls ACTION_SCAN_DIRS without extras 820 | * 821 | * Poweramp Scanner sends appropriate broadcast intents: 822 | * ACTION_DIRS_SCAN_STARTED (sticky), ACTION_DIRS_SCAN_FINISHED, ACTION_TAGS_SCAN_STARTED (sticky), ACTION_TAGS_SCAN_PROGRESS, ACTION_TAGS_SCAN_FINISHED, or ACTION_FAST_TAGS_SCAN_FINISHED. 823 | * 824 | * Extras: 825 | * - fastScan - Poweramp will not check folders and scan files which hasn't been modified from previous scan. Based on files last modified timestamp. 826 | * Poweramp doesn;t send 827 | * 828 | * - eraseTags - Poweramp will clean all tags from exisiting songs. This causes each song to be re-scanned for tags. 829 | * Warning: as a side effect, cleans CUE tracks from user created playlists. 830 | * This is because scanner can't incrementaly re-scan CUE sheets, so they are deleted from DB. 831 | * 832 | * - fullRescan - Poweramp will also check for folders/files from missing/unmounted storages and will remove them from DB. 833 | * Warning: removed songs also disappear from user created playlists. 834 | * Used in Poweramp only when user specificaly goes to Settings and does Full Rescan (after e.g. SD card change). 835 | * 836 | */ 837 | public static final String ACTION_SCAN_DIRS = "com.maxmpz.audioplayer.ACTION_SCAN_DIRS"; 838 | 839 | /** 840 | * Poweramp Scanner action. 841 | * Secondary action, only checks songs with TAG_STATUS set to TAG_NOT_SCANNED. Useful for rescanning just songs (which are already in Poweramp DB) with editied file tag info. 842 | * 843 | * Extras: 844 | * - fastScan - If true, scanner doesn't send ACTION_TAGS_SCAN_STARTED/ACTION_TAGS_SCAN_PROGRESS/ACTION_TAGS_SCAN_FINISHED intents, 845 | * just sends ACTION_FAST_TAGS_SCAN_FINISHED when done. 846 | * It doesn't modify scanning logic otherwise. 847 | */ 848 | public static final String ACTION_SCAN_TAGS = "com.maxmpz.audioplayer.ACTION_SCAN_TAGS"; 849 | 850 | 851 | /** 852 | * Broadcast. 853 | * Poweramp Scanner started folders scan. 854 | * This is sticky broadcast, so Poweramp folder scanner running status can be polled via registerReceiver() return value. 855 | */ 856 | public static final String ACTION_DIRS_SCAN_STARTED = "com.maxmpz.audioplayer.ACTION_DIRS_SCAN_STARTED"; 857 | /** 858 | * Broadcast. 859 | * Poweramp Scanner finished folders scan. 860 | */ 861 | public static final String ACTION_DIRS_SCAN_FINISHED = "com.maxmpz.audioplayer.ACTION_DIRS_SCAN_FINISHED"; 862 | /** 863 | * Broadcast. 864 | * Poweramp Scanner started tag scan. 865 | * This is sticky broadcast, so Poweramp tag scanner running status can be polled via registerReceiver() return value. 866 | */ 867 | public static final String ACTION_TAGS_SCAN_STARTED = "com.maxmpz.audioplayer.ACTION_TAGS_SCAN_STARTED"; 868 | /** 869 | * Broadcast. 870 | * Poweramp Scanner tag scan in progess. 871 | * Extras: 872 | * - progress - 0-100 progress of scanning. 873 | */ 874 | public static final String ACTION_TAGS_SCAN_PROGRESS = "com.maxmpz.audioplayer.ACTION_TAGS_SCAN_PROGRESS"; 875 | /** 876 | * Broadcast. 877 | * Poweramp Scanner finished tag scan. 878 | * Extras: 879 | * - track_content_changed - boolean - true if at least on track has been scanned, false if no tags scanned (probably, because all files are up-to-date). 880 | */ 881 | public static final String ACTION_TAGS_SCAN_FINISHED = "com.maxmpz.audioplayer.ACTION_TAGS_SCAN_FINISHED"; 882 | /** 883 | * Broadcast. 884 | * Poweramp Scanner finished fast tag scan. Only fired when ACTION_SCAN_TAGS is called with extra fastScan = true. 885 | * Extras: 886 | * - trackContentChanged - boolean - true if at least on track has been scanned, false if no tags scanned (probably, because all files are up-to-date). 887 | */ 888 | public static final String ACTION_FAST_TAGS_SCAN_FINISHED = "com.maxmpz.audioplayer.ACTION_FAST_TAGS_SCAN_FINISHED"; 889 | 890 | /** 891 | * Extra. 892 | * Boolean. 893 | */ 894 | public static final String EXTRA_FAST_SCAN = "fastScan"; 895 | /** 896 | * Extra. 897 | * Int. 898 | */ 899 | public static final String EXTRA_PROGRESS = "progress"; 900 | /** 901 | * Extra. 902 | * Boolean. 903 | */ 904 | public static final String EXTRA_TRACK_CONTENT_CHANGED = "trackContentChanged"; 905 | 906 | /** 907 | * Extra. 908 | * Boolean. 909 | */ 910 | public static final String EXTRA_ERASE_TAGS = "eraseTags"; 911 | 912 | /** 913 | * Extra. 914 | * Boolean. 915 | */ 916 | public static final String EXTRA_FULL_RESCAN = "fullRescan"; 917 | 918 | /** 919 | * Extra. 920 | * String. 921 | */ 922 | public static final String EXTRA_CAUSE = "cause"; 923 | } 924 | 925 | public static final class Settings { 926 | public static final String ACTION_EXPORT_SETTINGS = "com.maxmpz.audioplayer.ACTION_EXPORT_SETTINGS"; 927 | public static final String ACTION_IMPORT_SETTINGS = "com.maxmpz.audioplayer.ACTION_IMPORT_SETTINGS"; 928 | 929 | public static final String EXTRA_UI = "ui"; 930 | } 931 | 932 | } -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/PagerAdapter.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon; 2 | 3 | import android.support.v4.app.Fragment; 4 | import android.support.v4.app.FragmentManager; 5 | import android.support.v4.app.FragmentStatePagerAdapter; 6 | 7 | import org.json.JSONException; 8 | 9 | import java.io.IOException; 10 | import java.io.OutputStream; 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.List; 14 | 15 | /** 16 | * Holds the fragments for the configuration of all {@link SynchronizeConfig}s 17 | */ 18 | public class PagerAdapter extends FragmentStatePagerAdapter { 19 | 20 | private final List configs = new ArrayList<>(); 21 | 22 | public PagerAdapter(Collection configs , FragmentManager fm) { 23 | super(fm); 24 | 25 | this.configs.addAll(configs); 26 | } 27 | 28 | public void save(OutputStream os) throws JSONException, IOException { 29 | SynchronizeConfig.save(configs, os); 30 | } 31 | 32 | public void add(String name) { 33 | configs.add(new SynchronizeConfig(name)); 34 | notifyDataSetChanged(); 35 | } 36 | 37 | public void remove(String name) { 38 | int index = indexOf(name); 39 | 40 | if (index < 0) { 41 | return; 42 | } 43 | 44 | configs.remove(index); 45 | notifyDataSetChanged(); 46 | } 47 | 48 | public void rename(String name, String newName) { 49 | int index = indexOf(name); 50 | 51 | if (index < 0) { 52 | return; 53 | } 54 | 55 | SynchronizeConfig config = configs.get(index); 56 | 57 | config.setName(newName); 58 | 59 | notifyDataSetChanged(); 60 | } 61 | 62 | public int indexOf(String name) { 63 | for (int i = 0, size = configs.size(); i < size; i++) { 64 | SynchronizeConfig config = configs.get(i); 65 | if (config.getName().equals(name)) { 66 | return i; 67 | } 68 | } 69 | 70 | return -1; 71 | } 72 | 73 | @Override 74 | public Fragment getItem(int i) { 75 | SynchronizeConfigFragment fragment = new SynchronizeConfigFragment(); 76 | 77 | SynchronizeConfig config = getConfigs().get(i); 78 | 79 | fragment.setName(config.getName()); 80 | fragment.setPagerAdapter(this); 81 | fragment.setConfig(config); 82 | return fragment; 83 | } 84 | 85 | @Override 86 | public int getCount() { 87 | return getConfigs().size(); 88 | } 89 | 90 | @Override 91 | public int getItemPosition(Object object) { 92 | // http://stackoverflow.com/a/10399127 93 | return PagerAdapter.POSITION_NONE; 94 | } 95 | 96 | public List getConfigs() { 97 | return configs; 98 | } 99 | 100 | @Override 101 | public CharSequence getPageTitle(int position) { 102 | return configs.get(position).getName(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/RenameDialogFragment.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon; 2 | 3 | 4 | import android.app.Dialog; 5 | import android.content.DialogInterface; 6 | import android.os.Bundle; 7 | import android.support.annotation.NonNull; 8 | import android.support.v4.app.DialogFragment; 9 | import android.support.v7.app.AlertDialog; 10 | import android.widget.EditText; 11 | 12 | public class RenameDialogFragment extends DialogFragment { 13 | 14 | public static final DialogInterface.OnClickListener STUB_CLICK = new DialogInterface.OnClickListener() { 15 | public void onClick(DialogInterface dialog, int id) { 16 | } 17 | }; 18 | 19 | private PagerAdapter adapter; 20 | private String previousName; 21 | 22 | public void setAdapter(PagerAdapter adapter) { 23 | this.adapter = adapter; 24 | } 25 | 26 | public void setPreviousName(String previousName) { 27 | this.previousName = previousName; 28 | } 29 | 30 | private EditText newName; 31 | 32 | 33 | @NonNull 34 | @Override 35 | public Dialog onCreateDialog(Bundle savedInstanceState) { 36 | newName = new EditText(getActivity()); 37 | 38 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) 39 | .setTitle(getActivity().getString(R.string.rename)) 40 | .setView(newName) 41 | .setPositiveButton(android.R.string.ok, new ApplyRename()) 42 | .setNegativeButton(android.R.string.cancel, STUB_CLICK); 43 | 44 | return builder.create(); 45 | } 46 | 47 | private class ApplyRename implements DialogInterface.OnClickListener { 48 | @Override 49 | public void onClick(DialogInterface dialog, int which) { 50 | adapter.rename(previousName, newName.getText().toString()); 51 | adapter.notifyDataSetChanged(); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/SynchronizeActivity.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon; 2 | 3 | 4 | import android.Manifest; 5 | import android.app.ActivityManager; 6 | import android.app.Dialog; 7 | import android.app.ProgressDialog; 8 | import android.content.ComponentName; 9 | import android.content.Context; 10 | import android.content.DialogInterface; 11 | import android.content.Intent; 12 | import android.content.ServiceConnection; 13 | import android.content.pm.PackageManager; 14 | import android.database.DataSetObserver; 15 | import android.os.Bundle; 16 | import android.os.Handler; 17 | import android.os.IBinder; 18 | import android.os.Message; 19 | import android.os.Messenger; 20 | import android.os.RemoteException; 21 | import android.support.v4.app.ActivityCompat; 22 | import android.support.v4.content.ContextCompat; 23 | import android.support.v4.view.ViewPager; 24 | import android.support.v7.app.AlertDialog; 25 | import android.support.v7.app.AppCompatActivity; 26 | import android.support.v7.widget.Toolbar; 27 | import android.util.Log; 28 | import android.view.Menu; 29 | import android.view.MenuItem; 30 | import android.view.View; 31 | import android.widget.TextView; 32 | import android.widget.Toast; 33 | 34 | import com.google.samples.apps.iosched.ui.widget.SlidingTabLayout; 35 | 36 | import org.json.JSONException; 37 | 38 | import java.io.FileInputStream; 39 | import java.io.FileOutputStream; 40 | import java.io.IOException; 41 | import java.lang.ref.WeakReference; 42 | import java.util.Collections; 43 | import java.util.List; 44 | import java.util.UUID; 45 | 46 | import max.music_cyclon.preference.MainPreferenceActivity; 47 | import max.music_cyclon.service.LibraryService; 48 | 49 | /** 50 | * The main activity for synchronisation 51 | *

52 | * This class manages: 53 | *

61 | */ 62 | public class SynchronizeActivity extends AppCompatActivity { 63 | 64 | public static final String DEFAULT_CONFIG_PATH = "configs.json"; 65 | 66 | private PagerAdapter pagerAdapter; 67 | 68 | /** 69 | * Messenger for communicating with service. 70 | */ 71 | private Messenger serviceObject = null; 72 | /** 73 | * Flag indicating whether we have called bind on the service. 74 | */ 75 | private boolean isBound; 76 | 77 | /** 78 | * The dialog which is being displayed while a sync is in progress 79 | */ 80 | private ProgressDialog syncProgress = null; 81 | 82 | /** 83 | * Target we publish for clients to send messages to IncomingHandler. 84 | */ 85 | private final Messenger mMessenger = new Messenger( 86 | new IncomingHandler(new WeakReference<>(this)) 87 | ); 88 | 89 | @Override 90 | protected void onCreate(Bundle savedInstanceState) { 91 | super.onCreate(savedInstanceState); 92 | setContentView(R.layout.activity_synchronize); 93 | 94 | Toolbar toolbar = (Toolbar) findViewById(R.id.my_toolbar); 95 | setSupportActionBar(toolbar); 96 | 97 | List configs = Collections.emptyList(); 98 | try { 99 | FileInputStream in = openFileInput(DEFAULT_CONFIG_PATH); 100 | configs = SynchronizeConfig.load(in); 101 | in.close(); 102 | } catch (IOException | JSONException e) { 103 | Log.e("CONFIG", "Failed loading the config", e); 104 | } 105 | 106 | pagerAdapter = new PagerAdapter(configs, getSupportFragmentManager()); 107 | 108 | if (pagerAdapter.getCount() == 0) { 109 | pagerAdapter.add(getString(R.string.default_config_name)); 110 | } 111 | 112 | final ViewPager pager = (ViewPager) findViewById(R.id.container); 113 | assert pager != null; 114 | pager.setAdapter(pagerAdapter); 115 | 116 | 117 | // Initialize tabs 118 | final SlidingTabLayout tabs = (SlidingTabLayout) findViewById(R.id.tabs); 119 | assert tabs != null; 120 | 121 | tabs.setTabLongClickListener(new View.OnLongClickListener() { 122 | @Override 123 | public boolean onLongClick(View v) { 124 | CharSequence name = ((TextView) v).getText(); 125 | RenameDialogFragment dialog = new RenameDialogFragment(); 126 | dialog.setPreviousName(name.toString()); 127 | dialog.setAdapter(getPagerAdapter()); 128 | dialog.show(getSupportFragmentManager(), dialog.getClass().getName()); 129 | return true; 130 | } 131 | }); 132 | 133 | tabs.setDistributeEvenly(true); 134 | tabs.setCustomTabColorizer(new SlidingTabLayout.TabColorizer() { 135 | @Override 136 | public int getIndicatorColor(int position) { 137 | return ContextCompat.getColor(SynchronizeActivity.this, R.color.accentColor); 138 | } 139 | }); 140 | 141 | tabs.setViewPager(pager); 142 | 143 | // Update tabs on dataset change 144 | pagerAdapter.registerDataSetObserver(new DataSetObserver() { 145 | @Override 146 | public void onChanged() { 147 | tabs.setViewPager(pager); 148 | } 149 | }); 150 | 151 | View addButton = findViewById(R.id.add_button); 152 | assert addButton != null; 153 | addButton.setOnClickListener(new View.OnClickListener() { 154 | @Override 155 | public void onClick(View v) { 156 | pagerAdapter.add(UUID.randomUUID().toString().substring(0, 5)); 157 | pagerAdapter.notifyDataSetChanged(); 158 | } 159 | }); 160 | 161 | // Request permissions 162 | requestPermissions(); 163 | } 164 | 165 | private void requestPermissions() { 166 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 167 | != PackageManager.PERMISSION_GRANTED) { 168 | ActivityCompat.requestPermissions( 169 | this, 170 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 171 | 0 172 | ); 173 | } 174 | } 175 | 176 | @Override 177 | protected void onStop() { 178 | super.onStop(); 179 | 180 | unbindLibraryService(); 181 | 182 | try { 183 | FileOutputStream fos = openFileOutput(DEFAULT_CONFIG_PATH, Context.MODE_PRIVATE); 184 | getPagerAdapter().save(fos); 185 | fos.close(); 186 | } catch (IOException | JSONException e) { 187 | Log.e("CONFIG", "Failed saving the config", e); 188 | } 189 | } 190 | 191 | public PagerAdapter getPagerAdapter() { 192 | return pagerAdapter; 193 | } 194 | 195 | public Dialog getSyncProgress() { 196 | return syncProgress; 197 | } 198 | 199 | public void clearSyncProgress() { 200 | syncProgress = null; 201 | } 202 | 203 | @Override 204 | public boolean onCreateOptionsMenu(Menu menu) { 205 | getMenuInflater().inflate(R.menu.menu_main, menu); 206 | return true; 207 | } 208 | 209 | @Override 210 | public boolean onOptionsItemSelected(MenuItem item) { 211 | switch (item.getItemId()) { 212 | case R.id.action_settings: 213 | Intent preferenceIntent = new Intent(this, MainPreferenceActivity.class); 214 | startActivity(preferenceIntent); 215 | return true; 216 | case R.id.action_version: 217 | notYetImplemented(this); 218 | break; 219 | case R.id.action_help: 220 | notYetImplemented(this); 221 | break; 222 | case R.id.action_sync: 223 | if (isServiceRunning(LibraryService.class)) { 224 | Toast.makeText(this, R.string.already_synchronizing, Toast.LENGTH_LONG).show(); 225 | return false; 226 | } 227 | 228 | startLibraryService(); 229 | 230 | bindLibraryService(); 231 | 232 | // Show sync control dialog 233 | syncProgress = new ProgressDialog(SynchronizeActivity.this); 234 | syncProgress.setMessage(getString(R.string.synchronizing)); 235 | syncProgress.setCancelable(false); 236 | // syncProgress.setButton( 237 | // DialogInterface.BUTTON_NEGATIVE, 238 | // getString(android.R.string.cancel), 239 | // new DialogInterface.OnClickListener() { 240 | // @Override 241 | // public void onClick(DialogInterface dialog, int which) { 242 | // dialog.dismiss(); 243 | // } 244 | // }); 245 | // syncProgress.show(); 246 | return true; 247 | default: 248 | return super.onOptionsItemSelected(item); 249 | } 250 | 251 | return false; 252 | } 253 | 254 | public static void notYetImplemented(Context context) { 255 | new AlertDialog.Builder(context).setMessage("Not yet implemented!").show(); 256 | } 257 | 258 | private static class IncomingHandler extends Handler { 259 | private final WeakReference activity; 260 | 261 | private IncomingHandler(WeakReference activity) { 262 | this.activity = activity; 263 | } 264 | 265 | @Override 266 | public void handleMessage(Message msg) { 267 | SynchronizeActivity activity = this.activity.get(); 268 | if (activity == null) { 269 | return; 270 | } 271 | 272 | switch (msg.what) { 273 | case LibraryService.MSG_FINISHED: 274 | activity.unbindLibraryService(); 275 | 276 | Dialog dialog = activity.getSyncProgress(); 277 | if (dialog != null) { 278 | dialog.dismiss(); 279 | activity.clearSyncProgress(); 280 | } 281 | 282 | break; 283 | default: 284 | super.handleMessage(msg); 285 | } 286 | } 287 | } 288 | 289 | /** 290 | * Class for interacting with the main interface of the service. 291 | */ 292 | private ServiceConnection mConnection = new ServiceConnection() { 293 | public void onServiceConnected(ComponentName className, IBinder service) { 294 | serviceObject = new Messenger(service); 295 | 296 | try { 297 | Message msg = Message.obtain(null, LibraryService.MSG_REGISTER_CLIENT); 298 | msg.replyTo = mMessenger; 299 | serviceObject.send(msg); 300 | } catch (RemoteException ignored) { 301 | // In this case the service has crashed before we could even 302 | // do anything with it; we can count on soon being 303 | // disconnected (and then reconnected if it can be restarted) 304 | // so there is no need to do anything here. 305 | } 306 | } 307 | 308 | public void onServiceDisconnected(ComponentName className) { 309 | serviceObject = null; 310 | } 311 | }; 312 | 313 | private void startLibraryService() { 314 | Intent intent = new Intent(SynchronizeActivity.this, LibraryService.class); 315 | List configs = getPagerAdapter().getConfigs(); 316 | 317 | // Update last updated 318 | for (SynchronizeConfig config : configs) { 319 | config.updateLastUpdated(); // fixme the result of LibraryService does not affect this (for example: Remote not available) 320 | } 321 | 322 | intent.putExtra( 323 | LibraryService.ARGUMENT_CONFIGS, 324 | configs.toArray(new SynchronizeConfig[configs.size()]) 325 | ); 326 | startService(intent); 327 | } 328 | 329 | private boolean isServiceRunning(Class serviceClass) { 330 | ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); 331 | List services = 332 | manager.getRunningServices(Integer.MAX_VALUE); 333 | 334 | for (ActivityManager.RunningServiceInfo service : services) { 335 | if (serviceClass.getName().equals(service.service.getClassName())) { 336 | return true; 337 | } 338 | } 339 | 340 | return false; 341 | } 342 | 343 | private void bindLibraryService() { 344 | bindService(new Intent( 345 | SynchronizeActivity.this, 346 | LibraryService.class 347 | ), mConnection, Context.BIND_AUTO_CREATE); 348 | isBound = true; 349 | } 350 | 351 | private void unbindLibraryService() { 352 | if (isBound) { 353 | if (serviceObject != null) { 354 | try { 355 | Message msg = Message.obtain(null, LibraryService.MSG_UNREGISTER_CLIENT); 356 | msg.replyTo = mMessenger; 357 | serviceObject.send(msg); 358 | } catch (RemoteException ignored) { 359 | } 360 | } 361 | 362 | // Detach our existing connection. 363 | unbindService(mConnection); 364 | isBound = false; 365 | } 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/SynchronizeConfig.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon; 2 | 3 | import android.content.res.Resources; 4 | import android.os.Parcel; 5 | import android.os.Parcelable; 6 | 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | import java.util.ArrayList; 14 | import java.util.Iterator; 15 | import java.util.List; 16 | import java.util.Random; 17 | import java.util.Scanner; 18 | 19 | /** 20 | * A synchronize config which holds the information which tracks/albums should be queried 21 | */ 22 | public class SynchronizeConfig implements Parcelable { 23 | 24 | private static final Random RANDOM = new Random(); 25 | 26 | private static final String ID_KEY = "id"; 27 | private static final String SIZE_KEY = "size"; 28 | private static final String RANDOM_KEY = "random"; 29 | private static final String ALBUM_KEY = "use_albums"; 30 | private static final String QUERY_KEY = "query"; 31 | private static final String START_CHARGING_KEY = "start_charging"; 32 | private static final String DOWNLOAD_INTERVAL_KEY = "download_interval"; 33 | 34 | /** 35 | * The name of this config 36 | */ 37 | private String name; 38 | 39 | /** 40 | * The data structure 41 | */ 42 | private JSONObject json; 43 | 44 | public SynchronizeConfig(String name, JSONObject json) { 45 | this.name = name; 46 | this.json = json; 47 | } 48 | 49 | public SynchronizeConfig(String name, long id) { 50 | this(name, new JSONObject()); 51 | try { 52 | json.put(ID_KEY, id); 53 | } catch (JSONException e) { 54 | throw new RuntimeException("Failed setting id!"); 55 | } 56 | } 57 | 58 | public SynchronizeConfig(String name) { 59 | this(name, randomID(name)); 60 | } 61 | 62 | public long getID() { 63 | try { 64 | return json.getLong(ID_KEY); 65 | } catch (JSONException e) { 66 | throw new RuntimeException("Config has no id!"); 67 | } 68 | } 69 | 70 | public String getName() { 71 | return name; 72 | } 73 | 74 | public void setName(String name) { 75 | this.name = name; 76 | } 77 | 78 | public long getLastUpdated() { 79 | return json.optLong("last_update", 0); 80 | } 81 | 82 | public void updateLastUpdated() { 83 | try { 84 | json.put("last_update", System.currentTimeMillis()); 85 | } catch (JSONException ignored) { 86 | throw new RuntimeException("Failed to set last_update!"); 87 | } 88 | } 89 | 90 | public int getSize(Resources resources) { 91 | return json.optInt(SIZE_KEY, resources.getInteger(R.integer.size)); 92 | } 93 | 94 | public boolean isRandom(Resources resources) { 95 | return json.optBoolean(RANDOM_KEY, resources.getBoolean(R.bool.random)); 96 | } 97 | 98 | public boolean isAlbum(Resources resources) { 99 | return json.optBoolean(ALBUM_KEY, resources.getBoolean(R.bool.use_albums)); 100 | } 101 | 102 | public String getQuery(Resources resources) { 103 | return json.optString(QUERY_KEY, resources.getString(R.string.query)); 104 | } 105 | 106 | public boolean isStartCharging(Resources resources) { 107 | return json.optBoolean(START_CHARGING_KEY, resources.getBoolean(R.bool.start_charging)); 108 | } 109 | 110 | public int getDownloadInterval(Resources resources) { 111 | return json.optInt(DOWNLOAD_INTERVAL_KEY, 112 | resources.getInteger(R.integer.download_interval)); 113 | } 114 | 115 | public JSONObject getJson() { 116 | return json; 117 | } 118 | 119 | @Override 120 | public int describeContents() { 121 | return 0; 122 | } 123 | 124 | @Override 125 | public void writeToParcel(Parcel dest, int flags) { 126 | dest.writeString(name); 127 | dest.writeString(json.toString()); 128 | } 129 | 130 | public static final Creator CREATOR = new SynchronizeConfigCreator(); 131 | 132 | public static List load(InputStream in) throws JSONException { 133 | String data = convertStreamToString(in); 134 | return load(data); 135 | } 136 | 137 | public static List load(String data) throws JSONException { 138 | JSONObject jsonConfigs = new JSONObject(data); 139 | 140 | ArrayList configs = new ArrayList<>(); 141 | 142 | Iterator keys = jsonConfigs.keys(); 143 | while (keys.hasNext()) { 144 | String key = (String) keys.next(); 145 | configs.add(new SynchronizeConfig(key, jsonConfigs.getJSONObject(key))); 146 | } 147 | 148 | return configs; 149 | } 150 | 151 | public static long randomID(String name) { 152 | int rnd = RANDOM.nextInt(); 153 | int hash = name.hashCode(); 154 | 155 | return rnd + hash; 156 | } 157 | 158 | public static void save(Iterable configs, OutputStream fos) throws JSONException, IOException { 159 | JSONObject jsonConfigs = new JSONObject(); 160 | 161 | for (SynchronizeConfig config : configs) { 162 | jsonConfigs.put(config.getName(), config.getJson()); 163 | } 164 | 165 | fos.write(jsonConfigs.toString().getBytes("UTF-8")); 166 | } 167 | 168 | private static String convertStreamToString(InputStream is) { 169 | Scanner s = new Scanner(is).useDelimiter("\\A"); 170 | return s.hasNext() ? s.next() : ""; 171 | } 172 | 173 | private static class SynchronizeConfigCreator implements Creator { 174 | public SynchronizeConfig createFromParcel(Parcel in) { 175 | try { 176 | return new SynchronizeConfig(in.readString(), new JSONObject(in.readString())); 177 | } catch (JSONException e) { 178 | return new SynchronizeConfig("none"); 179 | } 180 | } 181 | 182 | public SynchronizeConfig[] newArray(int size) { 183 | return new SynchronizeConfig[size]; 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/SynchronizeConfigFragment.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.preference.Preference; 5 | import android.support.v7.preference.PreferenceScreen; 6 | import android.support.v7.preference.TwoStatePreference; 7 | 8 | import com.takisoft.fix.support.v7.preference.EditTextPreference; 9 | import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat; 10 | 11 | import org.json.JSONException; 12 | import org.json.JSONObject; 13 | 14 | /** 15 | * A fragment which holds one {@link SynchronizeConfig} and displays it's content for 16 | * simple editing. 17 | */ 18 | public class SynchronizeConfigFragment extends PreferenceFragmentCompat { 19 | 20 | private String name; 21 | private PagerAdapter pagerAdapter; 22 | private SynchronizeConfig config; 23 | 24 | @Override 25 | public void onCreate(final Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | addPreferencesFromResource(R.xml.sync_config); 28 | 29 | setRetainInstance(true); 30 | 31 | ConfigUpdater updater = new ConfigUpdater(); 32 | 33 | JSONObject json = config.getJson(); 34 | 35 | PreferenceScreen screen = getPreferenceScreen(); 36 | for (int i = 0; i < screen.getPreferenceCount(); i++) { 37 | Preference preference = screen.getPreference(i); 38 | 39 | if (json.has(preference.getKey())) { 40 | if (preference instanceof TwoStatePreference) { 41 | boolean data = json.optBoolean(preference.getKey()); 42 | ((TwoStatePreference) preference).setChecked(data); 43 | } else if (preference instanceof EditTextPreference) { 44 | String data = json.optString(preference.getKey()); 45 | ((EditTextPreference) preference).setText(data); 46 | } 47 | } 48 | 49 | preference.setOnPreferenceChangeListener(updater); 50 | } 51 | 52 | // The remove listener 53 | Preference removePreference = findPreference("remove"); 54 | removePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 55 | @Override 56 | public boolean onPreferenceClick(Preference preference) { 57 | if (getPagerAdapter().getCount() == 1) { 58 | return false; 59 | } 60 | getPagerAdapter().remove(getName()); 61 | getPagerAdapter().notifyDataSetChanged(); 62 | return true; 63 | } 64 | }); 65 | } 66 | 67 | @Override 68 | public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) { 69 | 70 | } 71 | 72 | public void setName(String name) { 73 | this.name = name; 74 | } 75 | 76 | public String getName() { 77 | return name; 78 | } 79 | 80 | public PagerAdapter getPagerAdapter() { 81 | return pagerAdapter; 82 | } 83 | 84 | public void setPagerAdapter(PagerAdapter pagerAdapter) { 85 | this.pagerAdapter = pagerAdapter; 86 | } 87 | 88 | public void setConfig(SynchronizeConfig config) { 89 | this.config = config; 90 | } 91 | 92 | public SynchronizeConfig getConfig() { 93 | return config; 94 | } 95 | 96 | private class ConfigUpdater implements Preference.OnPreferenceChangeListener { 97 | @Override 98 | public boolean onPreferenceChange(Preference preference, Object o) { 99 | String key = preference.getKey(); 100 | JSONObject config = getConfig().getJson(); 101 | 102 | try { 103 | config.put(key, o); 104 | return true; 105 | } catch (JSONException e) { 106 | return false; 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/preference/MainPreferenceActivity.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon.preference; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v14.preference.PreferenceFragment; 6 | import android.support.v7.app.ActionBar; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.view.MenuItem; 10 | 11 | import max.music_cyclon.R; 12 | import max.music_cyclon.SynchronizeActivity; 13 | 14 | /** 15 | * The activity to set general activities 16 | */ 17 | public class MainPreferenceActivity extends AppCompatActivity { 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_main_preference); 23 | Toolbar toolbar = (Toolbar) findViewById(R.id.preference_toolbar); 24 | setSupportActionBar(toolbar); 25 | ActionBar actionBar = getSupportActionBar(); 26 | assert actionBar != null; 27 | actionBar.setDisplayHomeAsUpEnabled(true); 28 | } 29 | 30 | @Override 31 | public boolean onOptionsItemSelected(MenuItem item) { 32 | if (item.getItemId() == android.R.id.home) { 33 | Intent myIntent = new Intent(getApplicationContext(), SynchronizeActivity.class); 34 | startActivityForResult(myIntent, 0); 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public static class MainPreferenceFragment extends PreferenceFragment { 42 | 43 | @Override 44 | public void onCreate(final Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | addPreferencesFromResource(R.xml.preferences); 47 | } 48 | 49 | @Override 50 | public void onCreatePreferences(Bundle bundle, String s) { 51 | 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/service/BeetsFetcher.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon.service; 2 | 3 | import android.content.res.Resources; 4 | import android.util.JsonReader; 5 | import android.util.Log; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.HashSet; 14 | import java.util.List; 15 | import java.util.Random; 16 | import java.util.Set; 17 | 18 | import max.music_cyclon.SynchronizeConfig; 19 | import okhttp3.OkHttpClient; 20 | import okhttp3.Request; 21 | import okhttp3.Response; 22 | 23 | public class BeetsFetcher { 24 | public static final Random RANDOM = new Random(); 25 | 26 | private final String address; 27 | private final Resources resources; 28 | 29 | public BeetsFetcher(String address, Resources resources) { 30 | this.address = address; 31 | this.resources = resources; 32 | } 33 | 34 | public Set fetch(SynchronizeConfig config, 35 | String username, String password) throws IOException { 36 | StringBuilder get; 37 | 38 | if (config.isAlbum(resources)) { 39 | get = new StringBuilder("/album/"); 40 | } else { 41 | get = new StringBuilder("/item/"); 42 | } 43 | 44 | String query = config.getQuery(resources); 45 | if (!query.isEmpty()) { 46 | get.append("query/").append(query); 47 | } 48 | 49 | get.append("?expand"); 50 | 51 | OkHttpClient client = new OkHttpClient(); 52 | String auth = okhttp3.Credentials.basic(username != null ? username : "", 53 | password != null ? password : ""); 54 | Request request = new Request.Builder() 55 | .url(address + get) 56 | .header("Authorization", auth) 57 | .build(); 58 | 59 | Response response = client.newCall(request).execute(); 60 | 61 | if (response.code() != 200) { 62 | Log.e("ERROR", "Server returned HTTP " + response.message()); 63 | return Collections.emptySet(); 64 | } 65 | 66 | 67 | InputStream stream = response.body().byteStream(); 68 | Set items = parseJson(stream, config.getSize(resources), config.isAlbum(resources)); 69 | stream.close(); 70 | 71 | return items; 72 | } 73 | 74 | private Set parseJson(InputStream stream, int size, boolean isAlbums) throws IOException { 75 | JsonReader reader = new JsonReader(new BufferedReader(new InputStreamReader(stream, "UTF-8"))); 76 | List items = new ArrayList<>(); 77 | List> albums = new ArrayList<>(); 78 | 79 | reader.beginObject(); 80 | String root = reader.nextName(); 81 | // boolean isAlbums = root.equals("albums"); 82 | reader.beginArray(); 83 | while (reader.hasNext()) { 84 | if (isAlbums) { 85 | albums.add(parseAlbum(reader)); 86 | } else { 87 | items.add(parseItem(reader)); 88 | } 89 | } 90 | reader.endArray(); 91 | reader.endObject(); 92 | 93 | // Select random 94 | if (isAlbums) { 95 | Set> randomAlbums = selectRandom(albums, size); 96 | 97 | for (List album : randomAlbums) { 98 | items.addAll(album); 99 | } 100 | 101 | return Collections.unmodifiableSet(new HashSet(items)); 102 | } else { 103 | return selectRandom(items, size); 104 | } 105 | } 106 | 107 | public Set selectRandom(List list, int n) { 108 | if (list.isEmpty()) { 109 | return Collections.emptySet(); 110 | } 111 | 112 | Set out = new HashSet(); 113 | 114 | for (int i = 0; i < n; i++) { 115 | int item = list.size() > 1 ? RANDOM.nextInt(list.size() - 1) : 0; 116 | out.add(list.get(item)); 117 | } 118 | 119 | return Collections.unmodifiableSet(out); 120 | } 121 | 122 | private ArrayList parseAlbum(JsonReader reader) throws IOException { 123 | reader.beginObject(); 124 | 125 | ArrayList items = new ArrayList<>(); 126 | 127 | while (reader.hasNext()) { 128 | String tag = reader.nextName(); 129 | if (tag.equals("items")) { 130 | reader.beginArray(); 131 | while (reader.hasNext()) { 132 | items.add(parseItem(reader)); 133 | } 134 | reader.endArray(); 135 | } else { 136 | reader.skipValue(); 137 | } 138 | } 139 | 140 | reader.endObject(); 141 | 142 | return items; 143 | } 144 | 145 | private Item parseItem(JsonReader reader) throws IOException { 146 | reader.beginObject(); 147 | Item item = new Item(); 148 | 149 | while (reader.hasNext()) { 150 | String tag = reader.nextName(); 151 | switch (tag) { 152 | case "id": 153 | item.setID(reader.nextInt()); 154 | break; 155 | case "format": 156 | item.setFormat(reader.nextString()); 157 | break; 158 | case "title": 159 | item.setTitle(reader.nextString()); 160 | break; 161 | case "artist": 162 | item.setArtist(reader.nextString()); 163 | break; 164 | default: 165 | reader.skipValue(); 166 | break; 167 | } 168 | } 169 | 170 | reader.endObject(); 171 | 172 | return item; 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/service/DownloadTask.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon.service; 2 | 3 | import android.os.Environment; 4 | import android.util.Log; 5 | 6 | import org.apache.commons.io.FileUtils; 7 | 8 | import java.io.File; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.util.concurrent.CountDownLatch; 13 | import java.util.zip.Adler32; 14 | 15 | import max.music_cyclon.SynchronizeConfig; 16 | import max.music_cyclon.tracker.FileTracker; 17 | import okhttp3.OkHttpClient; 18 | import okhttp3.Request; 19 | import okhttp3.Response; 20 | 21 | public class DownloadTask implements Runnable { 22 | 23 | private final SynchronizeConfig config; 24 | private final String url; 25 | private final String itemPath; 26 | private final String libraryPath; 27 | private final String username; 28 | private final String password; 29 | private final FileTracker tracker; 30 | private final ProgressUpdater progressUpdater; 31 | private CountDownLatch itemsLeftLatch; 32 | public static final OkHttpClient CLIENT = new OkHttpClient(); 33 | 34 | 35 | public DownloadTask(SynchronizeConfig config, String url, 36 | String libraryPath, String itemPath, 37 | FileTracker tracker, ProgressUpdater progressUpdater, 38 | String username, String password 39 | ) { 40 | this.config = config; 41 | this.url = url; 42 | this.itemPath = itemPath; 43 | this.libraryPath = libraryPath; 44 | 45 | this.username = username; 46 | this.password = password; 47 | 48 | this.tracker = tracker; 49 | this.progressUpdater = progressUpdater; 50 | } 51 | 52 | private InputStream prepareConnection() throws IOException { 53 | String auth = okhttp3.Credentials.basic(username != null ? username : "", 54 | password != null ? password : ""); 55 | Request request = new Request.Builder() 56 | .url(url) 57 | .header("Authorization", auth) 58 | .build(); 59 | 60 | Response response = CLIENT.newCall(request).execute(); 61 | 62 | if (response.code() != 200) { 63 | Log.e("ERROR", "Server returned HTTP " + response.message()); 64 | return null; 65 | } 66 | 67 | return response.body().byteStream(); 68 | } 69 | 70 | public void setItemsLeftLatch(CountDownLatch itemsLeftLatch) { 71 | this.itemsLeftLatch = itemsLeftLatch; 72 | } 73 | 74 | @Override 75 | public void run() { 76 | File root = new File(Environment.getExternalStorageDirectory(), 77 | libraryPath); 78 | if (itemPath != null) { 79 | try { 80 | File target = new File(root, itemPath); 81 | if (! target.exists()) { 82 | Adler32 checksum = new Adler32(); 83 | 84 | InputStream input = prepareConnection(); 85 | 86 | if (input != null) { 87 | Log.d("DOWNLOAD", "Writing file: " + target); 88 | FileOutputStream output = FileUtils.openOutputStream(target); 89 | 90 | byte[] buffer = new byte[4 * 1024]; 91 | int n; 92 | while (-1 != (n = input.read(buffer))) { 93 | output.write(buffer, 0, n); 94 | checksum.update(buffer, 0, n); 95 | } 96 | 97 | output.flush(); 98 | output.close(); 99 | input.close(); 100 | } 101 | 102 | tracker.track(config, target, checksum.getValue()); 103 | } 104 | } catch (IOException e) { 105 | Log.e("DOWNLOAD", "Failed to download", e); 106 | } 107 | Log.i("DOWNLOAD", "Success"); 108 | } else { 109 | Log.e("DOWNLOAD", "Missing download path, FAILED!"); 110 | } 111 | progressUpdater.increment(); 112 | itemsLeftLatch.countDown(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/service/Item.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon.service; 2 | 3 | public class Item { 4 | 5 | private int id; 6 | private String path; 7 | private String artist; 8 | private String title; 9 | private String format; 10 | 11 | public int getID() { 12 | return id; 13 | } 14 | 15 | public void setID(int id) { 16 | this.id = id; 17 | } 18 | 19 | public String getPath() { 20 | if (path != null) { 21 | return path; 22 | } else { 23 | return "/" + artist + "/" + title + "_" + id + "." + format.toLowerCase(); 24 | } 25 | } 26 | public String getArtist() { 27 | return artist; 28 | } 29 | public String getTitle() { 30 | return title; 31 | } 32 | public String getFormat() { 33 | return format; 34 | } 35 | 36 | public void setPath(String path) { 37 | this.path = path; 38 | } 39 | public void setTitle(String title) { 40 | this.title = title; 41 | } 42 | public void setFormat(String format) { 43 | this.format = format; 44 | } 45 | public void setArtist(String artist) { 46 | this.artist = artist; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/service/LibraryService.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon.service; 2 | 3 | import android.Manifest; 4 | import android.app.IntentService; 5 | import android.app.PendingIntent; 6 | import android.content.Intent; 7 | import android.content.SharedPreferences; 8 | import android.content.pm.PackageManager; 9 | import android.os.Environment; 10 | import android.os.Handler; 11 | import android.os.IBinder; 12 | import android.os.Message; 13 | import android.os.Messenger; 14 | import android.os.Parcelable; 15 | import android.os.RemoteException; 16 | import android.preference.PreferenceManager; 17 | import android.support.v4.app.NotificationCompat; 18 | import android.support.v4.content.ContextCompat; 19 | import android.util.Log; 20 | 21 | // Poweramp support 22 | // import com.maxmpz.poweramp.player.PowerampAPI; 23 | 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.lang.ref.WeakReference; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | import java.util.Set; 30 | import java.util.concurrent.CountDownLatch; 31 | import java.util.concurrent.ExecutorService; 32 | import java.util.concurrent.Executors; 33 | import java.util.concurrent.TimeUnit; 34 | 35 | import max.music_cyclon.R; 36 | import max.music_cyclon.SynchronizeConfig; 37 | import max.music_cyclon.tracker.FileTracker; 38 | 39 | public class LibraryService extends IntentService { 40 | 41 | /** 42 | * Command to the serviceReference to register a client, receiving callbacks 43 | * from the serviceReference. The Message's replyTo field must be a Messenger of 44 | * the client where callbacks should be sent. 45 | */ 46 | public static final int MSG_REGISTER_CLIENT = 1; 47 | 48 | /** 49 | * Command to the serviceReference to unregister a client, ot stop receiving callbacks 50 | * from the serviceReference. The Message's replyTo field must be a Messenger of 51 | * the client as previously given with MSG_REGISTER_CLIENT. 52 | */ 53 | public static final int MSG_UNREGISTER_CLIENT = 2; 54 | 55 | 56 | public static final int MSG_CANCEL = 3; 57 | public static final int MSG_STARTED = 4; 58 | public static final int MSG_FINISHED = 5; 59 | 60 | public static final String ARGUMENT_CONFIGS = "configs"; 61 | 62 | /** 63 | * Keeps track of all current registered clients. 64 | */ 65 | private ArrayList mClients = new ArrayList<>(); 66 | 67 | /** 68 | * Target we publish for clients to send messages to IncomingHandler. 69 | */ 70 | private final Messenger mMessenger = new Messenger( 71 | new IncomingHandler(new WeakReference<>(this)) 72 | ); 73 | 74 | public LibraryService() { 75 | super(LibraryService.class.getName()); 76 | } 77 | 78 | 79 | @Override 80 | protected void onHandleIntent(Intent intent) { 81 | Parcelable[] configs = intent.getParcelableArrayExtra(ARGUMENT_CONFIGS); 82 | 83 | ProgressUpdater updater = new ProgressUpdater(this); 84 | 85 | broadcast(Message.obtain(null, MSG_STARTED)); 86 | 87 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 88 | != PackageManager.PERMISSION_GRANTED) { 89 | updater.showMessage("No permission to write!"); 90 | finished(); 91 | return; 92 | } 93 | 94 | SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(this); 95 | int threads = Integer.parseInt(globalSettings.getString("threads", Integer.toString(getResources().getInteger(R.integer.threads)))); 96 | String address = globalSettings.getString("address", getResources().getString(R.string.address)); 97 | 98 | ExecutorService executor = Executors.newFixedThreadPool(threads); 99 | File root = new File(Environment.getExternalStorageDirectory(), "library"); 100 | 101 | BeetsFetcher fetcher = new BeetsFetcher(address, getResources()); 102 | 103 | if (root.exists() && !root.isDirectory()) { 104 | updater.showMessage("Library is no dictionary! Fix manually"); 105 | finished(); 106 | return; 107 | } 108 | 109 | root.mkdirs(); 110 | 111 | FileTracker tracker = new FileTracker(getApplicationContext()); 112 | 113 | try { 114 | updater.showOngoingMessage("Cleaning library"); 115 | tracker.delete(); 116 | } catch (IOException e) { 117 | e.printStackTrace(); 118 | } 119 | 120 | if (root.exists() && root.list().length != 0) { 121 | NotificationCompat.Builder builder = updater.notificationBuilder(); 122 | builder.setContentTitle("Library not empty! Clean in manually"); 123 | Intent libraryIntent = new Intent(); 124 | libraryIntent.setAction("max.music_cyclon.force_clear"); 125 | 126 | PendingIntent pending = PendingIntent.getBroadcast(this, 0, libraryIntent, 0); 127 | builder.addAction(android.R.drawable.ic_delete, "Clear now", pending); 128 | updater.updateNotification(builder); 129 | 130 | finished(); 131 | return; 132 | } 133 | 134 | ArrayList tasks = new ArrayList<>(); 135 | 136 | for (Parcelable parcelable : configs) { 137 | SynchronizeConfig config = (SynchronizeConfig) parcelable; 138 | Set items; 139 | try { 140 | updater.showOngoingMessage("Fetching music information for %s", config.getName()); 141 | items = fetcher.fetch(config, 142 | globalSettings.getString("username", null), 143 | globalSettings.getString("password", null)); 144 | Log.d("LISTOUT", "Length: " + items.size()); 145 | 146 | } catch (IOException e) { 147 | Log.wtf("WTF", e); 148 | updater.showMessage("Remote not available"); 149 | finished(); 150 | return; 151 | } 152 | 153 | updater.showOngoingMessage("Mixing new music for %s!", config.getName()); 154 | updater.setMaximumProgress(items.size()); 155 | 156 | for (Item item : items) { 157 | String url = address + "/item/" + item.getID() + "/file"; 158 | tasks.add(new DownloadTask(config, url, 159 | globalSettings.getString("library_path", "library"), 160 | config.getName() + item.getPath(), tracker, updater, 161 | globalSettings.getString("username", null), 162 | globalSettings.getString("password", null) 163 | )); 164 | } 165 | } 166 | 167 | CountDownLatch itemsLeftLatch = new CountDownLatch(tasks.size()); 168 | 169 | for (DownloadTask task : tasks) { 170 | task.setItemsLeftLatch(itemsLeftLatch); 171 | executor.submit(task); 172 | } 173 | 174 | try { 175 | itemsLeftLatch.await(); 176 | executor.shutdown(); 177 | executor.awaitTermination(5, TimeUnit.SECONDS); 178 | } catch (InterruptedException e) { 179 | e.printStackTrace(); 180 | } 181 | 182 | updater.showMessage(getResources().getString(R.string.music_updated)); 183 | 184 | // I don't want to support proprietary things 185 | // If you need to enable Poweramp support, uncomment this 186 | // Intent poweramp = new Intent(PowerampAPI.Scanner.ACTION_SCAN_DIRS); 187 | // poweramp.setPackage(PowerampAPI.PACKAGE_NAME); 188 | // poweramp.putExtra(PowerampAPI.Scanner.EXTRA_FULL_RESCAN, true); 189 | // startService(poweramp); 190 | 191 | finished(); 192 | } 193 | 194 | public void finished() { 195 | broadcast(Message.obtain(null, MSG_FINISHED)); 196 | } 197 | 198 | /** 199 | * Handler of incoming messages from clients. 200 | */ 201 | private static class IncomingHandler extends Handler { 202 | 203 | private final WeakReference serviceReference; 204 | 205 | private IncomingHandler(WeakReference serviceReference) { 206 | this.serviceReference = serviceReference; 207 | } 208 | 209 | @Override 210 | public void handleMessage(Message msg) { 211 | LibraryService service = serviceReference.get(); 212 | 213 | if (service == null) { 214 | return; 215 | } 216 | 217 | switch (msg.what) { 218 | case MSG_REGISTER_CLIENT: 219 | service.mClients.add(msg.replyTo); 220 | break; 221 | case MSG_UNREGISTER_CLIENT: 222 | service.mClients.remove(msg.replyTo); 223 | break; 224 | case MSG_CANCEL: 225 | //todo 226 | break; 227 | default: 228 | super.handleMessage(msg); 229 | } 230 | } 231 | } 232 | 233 | private void broadcast(Message msg) { 234 | for (int i = mClients.size() - 1; i >= 0; i--) { 235 | try { 236 | mClients.get(i).send(msg); 237 | } catch (RemoteException e) { 238 | // The client is dead. Remove it from the list; 239 | // we are going through the list from back to front 240 | // so this is safe to do inside the loop. 241 | mClients.remove(i); 242 | } 243 | } 244 | } 245 | 246 | @Override 247 | public IBinder onBind(Intent intent) { 248 | return mMessenger.getBinder(); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/service/PowerConnectionReceiver.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon.service; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.os.BatteryManager; 8 | import android.util.Log; 9 | 10 | import org.json.JSONException; 11 | 12 | import java.io.FileInputStream; 13 | import java.io.FileOutputStream; 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.Collections; 17 | import java.util.List; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | import max.music_cyclon.SynchronizeActivity; 21 | import max.music_cyclon.SynchronizeConfig; 22 | 23 | public class PowerConnectionReceiver extends BroadcastReceiver { 24 | 25 | @Override 26 | public void onReceive(Context context, Intent intent) { 27 | IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); 28 | Intent batteryStatus = context.registerReceiver(null, ifilter); 29 | 30 | if (batteryStatus == null) { 31 | return; 32 | } 33 | 34 | int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); 35 | int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); 36 | 37 | boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || 38 | status == BatteryManager.BATTERY_STATUS_FULL; 39 | boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC; 40 | 41 | if (acCharge && isCharging) { 42 | // todo the latest changes, if the app is running are not available at this point 43 | 44 | List loadedConfigs = Collections.emptyList(); 45 | try { 46 | FileInputStream in = context.openFileInput(SynchronizeActivity.DEFAULT_CONFIG_PATH); 47 | loadedConfigs = SynchronizeConfig.load(in); 48 | in.close(); 49 | } catch (IOException | JSONException e) { 50 | Log.e("CONFIG", "Failed loading the config", e); 51 | } 52 | 53 | List configs = new ArrayList<>(); 54 | 55 | for (SynchronizeConfig config : loadedConfigs) { 56 | if (!config.isStartCharging(context.getResources())) { 57 | continue; 58 | } 59 | 60 | long lastUpdated = config.getLastUpdated(); 61 | 62 | if (lastUpdated < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(config.getDownloadInterval(context.getResources()))) { 63 | configs.add(config); 64 | config.updateLastUpdated(); // fixme the result of LibraryService does not affect this (for example: Remote not available) 65 | } 66 | } 67 | 68 | if (configs.isEmpty()) { 69 | return; 70 | } 71 | 72 | Intent serviceIntend = new Intent(context, LibraryService.class); 73 | serviceIntend.putExtra( 74 | LibraryService.ARGUMENT_CONFIGS, 75 | configs.toArray(new SynchronizeConfig[configs.size()]) 76 | ); 77 | 78 | context.startService(serviceIntend); 79 | 80 | try { 81 | FileOutputStream fos = context.openFileOutput(SynchronizeActivity.DEFAULT_CONFIG_PATH, Context.MODE_PRIVATE); 82 | SynchronizeConfig.save(loadedConfigs, fos); 83 | fos.close(); 84 | } catch (IOException | JSONException e) { 85 | Log.e("CONFIG", "Failed saving the config", e); 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/service/ProgressUpdater.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon.service; 2 | 3 | import android.content.Context; 4 | import android.support.v4.app.NotificationCompat; 5 | import android.support.v4.app.NotificationManagerCompat; 6 | 7 | import java.util.Random; 8 | 9 | import max.music_cyclon.R; 10 | 11 | 12 | public class ProgressUpdater { 13 | 14 | public static int NOTIFICATION_ID = new Random().nextInt(); 15 | 16 | private Context context; 17 | 18 | private int maximum = 0; 19 | private final NotificationManagerCompat notificationManager; 20 | 21 | private int downloadCount = 0; 22 | 23 | public ProgressUpdater(Context context) { 24 | this.context = context; 25 | 26 | this.notificationManager = NotificationManagerCompat.from(context); 27 | } 28 | 29 | public void showMessage(String message, Object... args) { 30 | showMessage(String.format(message, args)); 31 | } 32 | 33 | public void showMessage(String message) { 34 | NotificationCompat.Builder builder = notificationBuilder(); 35 | builder.setContentTitle(message); 36 | 37 | updateNotification(builder); 38 | } 39 | 40 | public void showOngoingMessage(String message, Object... args) { 41 | showOngoingMessage(String.format(message, args)); 42 | } 43 | 44 | 45 | public void showOngoingMessage(String message) { 46 | NotificationCompat.Builder builder = notificationBuilder(); 47 | builder.setContentTitle(message); 48 | builder.setProgress(0, 0, true); 49 | builder.setOngoing(true); 50 | 51 | updateNotification(builder); 52 | } 53 | 54 | public synchronized void increment() { 55 | NotificationCompat.Builder builder = progressNotificationBuilder(); 56 | downloadCount++; 57 | 58 | builder.setContentTitle("Aktualisiere Musik"); 59 | builder.setContentText(downloadCount + "/" + maximum); 60 | builder.setProgress(maximum, downloadCount, false); 61 | builder.setOngoing(true); 62 | updateNotification(builder); 63 | } 64 | 65 | public void setMaximumProgress(int maximum) { 66 | this.maximum = maximum; 67 | } 68 | 69 | public void updateNotification(NotificationCompat.Builder builder) { 70 | notificationManager.notify(NOTIFICATION_ID, builder.build()); 71 | } 72 | 73 | public NotificationCompat.Builder notificationBuilder() { 74 | return new NotificationCompat.Builder(context) 75 | .setSmallIcon(R.drawable.ic_sync_white_24dp); 76 | } 77 | 78 | private NotificationCompat.Builder progressNotificationBuilder() { 79 | return notificationBuilder().setUsesChronometer(true) 80 | .setOngoing(true) 81 | .setProgress(0, 0, true); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/tracker/FileTracker.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon.tracker; 2 | 3 | 4 | import android.content.ContentValues; 5 | import android.content.Context; 6 | import android.database.Cursor; 7 | import android.database.sqlite.SQLiteDatabase; 8 | 9 | import org.apache.commons.io.FileUtils; 10 | 11 | import java.io.File; 12 | import java.io.FileInputStream; 13 | import java.io.IOException; 14 | import java.util.zip.Adler32; 15 | 16 | import max.music_cyclon.SynchronizeConfig; 17 | 18 | public class FileTracker { 19 | 20 | private final LibraryDBOpenHelper helper; 21 | 22 | public FileTracker(Context context) { 23 | helper = new LibraryDBOpenHelper(context); 24 | } 25 | 26 | public void track(SynchronizeConfig config, File file, long checksum) { 27 | SQLiteDatabase db = helper.getWritableDatabase(); 28 | ContentValues values = new ContentValues(); 29 | values.put("path", file.getAbsolutePath()); 30 | values.put("checksum", checksum); 31 | values.put("config", config.getID()); 32 | 33 | db.insert("library", null, values); 34 | db.close(); 35 | } 36 | 37 | public void delete() throws IOException { 38 | SQLiteDatabase db = helper.getReadableDatabase(); 39 | 40 | Cursor cursor = db.query("library", null, null, null, null, null, null); 41 | int pathIndex = cursor.getColumnIndex("path"); 42 | int checksumIndex = cursor.getColumnIndex("checksum"); 43 | 44 | while (cursor.moveToNext()) { 45 | long checksum = cursor.getLong(checksumIndex); 46 | String path = cursor.getString(pathIndex); 47 | 48 | 49 | File file = new File(path); 50 | 51 | if (checksum != checksum(file)) { 52 | continue; 53 | } 54 | 55 | removeFile(file); 56 | } 57 | 58 | cursor.close(); 59 | db.close(); 60 | } 61 | 62 | private long checksum(File file) throws IOException { 63 | if (!file.exists()) { 64 | return 0; 65 | } 66 | 67 | Adler32 checksum = new Adler32(); 68 | byte[] buffer = new byte[4 * 1024]; 69 | int n; 70 | 71 | FileInputStream input = FileUtils.openInputStream(file); 72 | 73 | while (-1 != (n = input.read(buffer))) { 74 | checksum.update(buffer, 0, n); 75 | } 76 | 77 | input.close(); 78 | 79 | return checksum.getValue(); 80 | } 81 | 82 | public void removeFile(File path) throws IOException { 83 | if (path == null) return; 84 | 85 | if (path.isFile()) { 86 | FileUtils.deleteQuietly(path); 87 | } else if (path.isDirectory()) { 88 | if (!path.delete()) { 89 | return; 90 | } 91 | } 92 | 93 | removeFile(path.getParentFile()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/tracker/ForceClearReceiver.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon.tracker; 2 | 3 | import android.app.NotificationManager; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.os.Environment; 8 | import android.util.Log; 9 | 10 | import org.apache.commons.io.FileUtils; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | 15 | import max.music_cyclon.service.ProgressUpdater; 16 | 17 | 18 | public class ForceClearReceiver extends BroadcastReceiver { 19 | 20 | @Override 21 | public void onReceive(Context context, Intent intent) { 22 | File root = new File(Environment.getExternalStorageDirectory(), "library"); 23 | try { 24 | FileUtils.deleteDirectory(root); 25 | } catch (IOException e) { 26 | Log.e("LIBRARY", "Failed to delete library", e); 27 | } 28 | 29 | NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 30 | manager.cancel(ProgressUpdater.NOTIFICATION_ID); 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/max/music_cyclon/tracker/LibraryDBOpenHelper.java: -------------------------------------------------------------------------------- 1 | package max.music_cyclon.tracker; 2 | 3 | import android.content.Context; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.database.sqlite.SQLiteOpenHelper; 6 | 7 | 8 | public class LibraryDBOpenHelper extends SQLiteOpenHelper { 9 | private static final String SQL_CREATE_ENTRIES = 10 | "CREATE TABLE library (config INTEGER, path TEXT, checksum INTEGER)"; 11 | 12 | public static final int DATABASE_VERSION = 1; 13 | public static final String DATABASE_NAME = "database.db"; 14 | 15 | public LibraryDBOpenHelper(Context context) { 16 | super(context, DATABASE_NAME, null, DATABASE_VERSION); 17 | } 18 | 19 | public void onCreate(SQLiteDatabase db) { 20 | db.execSQL(SQL_CREATE_ENTRIES); 21 | } 22 | 23 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 24 | } 25 | 26 | public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_add_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-hdpi/ic_add_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_music_note_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-hdpi/ic_music_note_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_sync_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-hdpi/ic_sync_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_add_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-mdpi/ic_add_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_music_note_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-mdpi/ic_music_note_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_sync_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-mdpi/ic_sync_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_add_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-xhdpi/ic_add_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_music_note_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-xhdpi/ic_music_note_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_sync_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-xhdpi/ic_sync_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_add_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-xxhdpi/ic_add_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_music_note_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-xxhdpi/ic_music_note_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_sync_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-xxhdpi/ic_sync_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_add_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-xxxhdpi/ic_add_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_music_note_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-xxxhdpi/ic_music_note_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_sync_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/drawable-xxxhdpi/ic_sync_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main_preference.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_synchronize.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | 20 | 25 | 26 | 32 | 33 | 34 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | 13 | 14 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #607D8B 4 | #FF9800 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/default_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://localhost:8337 4 | 2 5 | 6 | 7 | 8 | 10 9 | true 10 | true 11 | 12 | false 13 | 7 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | music-cyclon 3 | Synchronizing 4 | Synchronize 5 | Settings 6 | Help 7 | Version 8 | Synchronizing 9 | Already synchronizing! 10 | Music Updated! 11 | 12 | library 13 | Default 14 | Rename 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 23 | 30 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/xml/sync_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 13 | 14 | 20 | 21 | 27 | 28 | 35 | 36 | 42 | 43 | 50 | 51 | 55 | 56 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.1.2' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | jcenter() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu May 24 16:50:41 GMT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /img/screenshot/Screenshot_20160614-205250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/img/screenshot/Screenshot_20160614-205250.png -------------------------------------------------------------------------------- /img/screenshot/Screenshot_20160614-205254.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/img/screenshot/Screenshot_20160614-205254.png -------------------------------------------------------------------------------- /img/screenshot/Screenshot_20160614-205302.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxammann/music-cyclon/d5712e845ab7e210bf3c5d95b048c2c49e47065c/img/screenshot/Screenshot_20160614-205302.png -------------------------------------------------------------------------------- /music-cyclon.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------