Rewrote music-cyclon to integrate it with beets library, very alpha and not optimized and tested!
parent
7a2cc7bee4
commit
5e5862e059
|
@ -1,22 +1,16 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 22
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "22.0.1"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "max.music_cyclon"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 22
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 23
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
|
@ -24,8 +18,13 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile fileTree(include: ['*.jar'], dir: 'libs')
|
||||
compile 'com.android.support:appcompat-v7:22.0.0'
|
||||
compile "cz.msebera.android:httpclient:4.4.1.2"
|
||||
compile 'commons-io:commons-io:2.4'
|
||||
compile group: 'org.apache.httpcomponents' , name: 'httpclient-android' , version: '4.3.5.1'
|
||||
|
||||
// compile 'com.android.support:appcompat-v7:23.4.0'
|
||||
// compile 'com.android.support:preference-v14:23.4.0'
|
||||
compile 'com.takisoft.fix:preference-v7:23.4.0.4'
|
||||
|
||||
compile 'com.android.support:appcompat-v7:23.4.0'
|
||||
compile 'com.android.support:design:23.4.0'
|
||||
}
|
||||
|
|
|
@ -1,35 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="max.music_cyclon" >
|
||||
package="max.music_cyclon">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme" >
|
||||
android:theme="@style/AppTheme">
|
||||
<service
|
||||
android:name=".service.LibraryService"
|
||||
android:enabled="true" />
|
||||
|
||||
<receiver android:name=".service.PowerConnectionReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="Music Cyclon" >
|
||||
android:name=".SynchronizeActivity"
|
||||
android:label="@string/title_activity_synchronize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".LibraryService"
|
||||
android:enabled="true" />
|
||||
|
||||
<receiver android:name=".PowerConnectionReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity android:name=".MainPreferenceActivity"></activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
|
@ -0,0 +1,932 @@
|
|||
/*
|
||||
Copyright (C) 2011-2013 Maksim Petrov
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted for widgets, plugins, applications and other software
|
||||
which communicate with Poweramp application on Android platform.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package com.maxmpz.poweramp.player;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
|
||||
/**
|
||||
* Poweramp intent based API.
|
||||
*/
|
||||
public final class PowerampAPI {
|
||||
/**
|
||||
* Defines PowerampAPI version, which could be also 200 and 210 for older Poweramps.
|
||||
*/
|
||||
public static final int VERSION = 533;
|
||||
|
||||
/**
|
||||
* No id flag.
|
||||
*/
|
||||
public static final int NO_ID = 0;
|
||||
|
||||
public static final String AUTHORITY = "com.maxmpz.audioplayer.data";
|
||||
|
||||
public static final Uri ROOT_URI = new Uri.Builder().scheme("content").authority(AUTHORITY).build();
|
||||
|
||||
/**
|
||||
* Uri query parameter - filter.
|
||||
*/
|
||||
public static final String PARAM_FILTER = "flt";
|
||||
/**
|
||||
* Uri query parameter - shuffle mode.
|
||||
*/
|
||||
public static final String PARAM_SHUFFLE = "shf";
|
||||
|
||||
|
||||
/**
|
||||
* Poweramp Control action.
|
||||
* Should be sent with sendBroadcast().
|
||||
* Extras:
|
||||
* - cmd - int - command to execute.
|
||||
*/
|
||||
public static final String ACTION_API_COMMAND = "com.maxmpz.audioplayer.API_COMMAND";
|
||||
|
||||
public static Intent newAPIIntent() {
|
||||
return new Intent(ACTION_API_COMMAND).setComponent(PLAYER_SERVICE_COMPONENT_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* ACTION_API_COMMAND extra.
|
||||
* Int.
|
||||
*/
|
||||
public static final String COMMAND = "cmd";
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Commonm extras:
|
||||
* - beep - boolean - (optional) if true, Poweramp will beep on playback command
|
||||
*/
|
||||
public static final class Commands {
|
||||
/**
|
||||
* Extras:
|
||||
* - keepService - boolean - (optional) if true, Poweramp won't unload player service. Notification will be appropriately updated.
|
||||
*/
|
||||
public static final int TOGGLE_PLAY_PAUSE = 1;
|
||||
/**
|
||||
* Extras:
|
||||
* - keepService - boolean - (optional) if true, Poweramp won't unload player service. Notification will be appropriately updated.
|
||||
*/
|
||||
public static final int PAUSE = 2;
|
||||
public static final int RESUME = 3;
|
||||
/**
|
||||
* NOTE: subject to 200ms throttling.
|
||||
*/
|
||||
public static final int NEXT = 4;
|
||||
/**
|
||||
* NOTE: subject to 200ms throttling.
|
||||
*/
|
||||
public static final int PREVIOUS = 5;
|
||||
/**
|
||||
* NOTE: subject to 200ms throttling.
|
||||
*/
|
||||
public static final int NEXT_IN_CAT = 6;
|
||||
/**
|
||||
* NOTE: subject to 200ms throttling.
|
||||
*/
|
||||
public static final int PREVIOUS_IN_CAT = 7;
|
||||
/**
|
||||
* Extras:
|
||||
* - showToast - boolean - (optional) if false, no toast will be shown. Applied for cycle only.
|
||||
* - repeat - int - (optional) if exists, appropriate mode will be directly selected, otherwise modes will be cycled, see Repeat class.
|
||||
*/
|
||||
public static final int REPEAT = 8;
|
||||
/**
|
||||
* Extras:
|
||||
* - showToast - boolean - (optional) if false, no toast will be shown. Applied for cycle only.
|
||||
* - shuffle - int - (optional) if exists, appropriate mode will be directly selected, otherwise modes will be cycled, see Shuffle class.
|
||||
*/
|
||||
public static final int SHUFFLE = 9;
|
||||
public static final int BEGIN_FAST_FORWARD = 10;
|
||||
public static final int END_FAST_FORWARD = 11;
|
||||
public static final int BEGIN_REWIND = 12;
|
||||
public static final int END_REWIND = 13;
|
||||
public static final int STOP = 14;
|
||||
/**
|
||||
* Extras:
|
||||
* - pos - int - seek position in seconds.
|
||||
*/
|
||||
public static final int SEEK = 15;
|
||||
public static final int POS_SYNC = 16;
|
||||
|
||||
/**
|
||||
* Data:
|
||||
* - uri, following URIs are recognized:
|
||||
* - file://path
|
||||
* - content://com.maxmpz.audioplayer.data/... (see below)
|
||||
*
|
||||
* # means some numeric id (track id for queries ending with /files, otherwise - appropriate category id).
|
||||
* If song id (in place of #) is not specified, Poweramp plays whole list starting from the specified song,
|
||||
* or from first one, or from random one in shuffle mode.
|
||||
*
|
||||
* All queries support following params (added as URL encoded params, e.g. content://com.maxmpz.audioplayer.data/files?lim=10&flt=foo):
|
||||
* lim - integer - SQL LIMIT, which limits number of rows returned
|
||||
* flt - string - filter substring. Poweramp will return only matching rows (the same way as returned in Poweramp lists UI when filter is used).
|
||||
* 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
|
||||
* which will be shuffled with the all subfolders in it as one list.
|
||||
* shf - integer - shuffle mode (see ShuffleMode class)
|
||||
* ssid - long - shuffle session id (for internal use)
|
||||
*
|
||||
* 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).
|
||||
|
||||
- All Songs:
|
||||
content://com.maxmpz.audioplayer.data/files
|
||||
content://com.maxmpz.audioplayer.data/files/meta
|
||||
content://com.maxmpz.audioplayer.data/files/#
|
||||
|
||||
- Most Played
|
||||
content://com.maxmpz.audioplayer.data/most_played
|
||||
content://com.maxmpz.audioplayer.data/most_played/files
|
||||
content://com.maxmpz.audioplayer.data/most_played/files/meta
|
||||
content://com.maxmpz.audioplayer.data/most_played/files/#
|
||||
|
||||
- Top Rated
|
||||
content://com.maxmpz.audioplayer.data/top_rated
|
||||
content://com.maxmpz.audioplayer.data/top_rated/files
|
||||
content://com.maxmpz.audioplayer.data/top_rated/files/meta
|
||||
content://com.maxmpz.audioplayer.data/top_rated/files/#
|
||||
|
||||
- Recently Added
|
||||
content://com.maxmpz.audioplayer.data/recently_added
|
||||
content://com.maxmpz.audioplayer.data/recently_added/files
|
||||
content://com.maxmpz.audioplayer.data/recently_added/files/meta
|
||||
content://com.maxmpz.audioplayer.data/recently_added/files/#
|
||||
|
||||
- Recently Played
|
||||
content://com.maxmpz.audioplayer.data/recently_played
|
||||
content://com.maxmpz.audioplayer.data/recently_played/files
|
||||
content://com.maxmpz.audioplayer.data/recently_played/files/meta
|
||||
content://com.maxmpz.audioplayer.data/recently_played/files/#
|
||||
|
||||
- Plain folders view (just files in plain folders list)
|
||||
content://com.maxmpz.audioplayer.data/folders
|
||||
content://com.maxmpz.audioplayer.data/folders/#
|
||||
content://com.maxmpz.audioplayer.data/folders/#/files
|
||||
content://com.maxmpz.audioplayer.data/folders/#/files/meta
|
||||
content://com.maxmpz.audioplayer.data/folders/#/files/#
|
||||
|
||||
- Hierarchy folders view (files and folders intermixed in one cursor)
|
||||
content://com.maxmpz.audioplayer.data/folders/#/folders_and_files
|
||||
content://com.maxmpz.audioplayer.data/folders/#/folders_and_files/meta
|
||||
content://com.maxmpz.audioplayer.data/folders/#/folders_and_files/#
|
||||
content://com.maxmpz.audioplayer.data/folders/files // All folder files, sorted as folders_files sort (for mass ops).
|
||||
|
||||
- Genres
|
||||
content://com.maxmpz.audioplayer.data/genres
|
||||
content://com.maxmpz.audioplayer.data/genres/#/files
|
||||
content://com.maxmpz.audioplayer.data/genres/#/files/meta
|
||||
content://com.maxmpz.audioplayer.data/genres/#/files/#
|
||||
content://com.maxmpz.audioplayer.data/genres/files
|
||||
|
||||
- Artists
|
||||
content://com.maxmpz.audioplayer.data/artists
|
||||
content://com.maxmpz.audioplayer.data/artists/#
|
||||
content://com.maxmpz.audioplayer.data/artists/#/files
|
||||
content://com.maxmpz.audioplayer.data/artists/#/files/meta
|
||||
content://com.maxmpz.audioplayer.data/artists/#/files/#
|
||||
content://com.maxmpz.audioplayer.data/artists/files
|
||||
|
||||
- Composers
|
||||
content://com.maxmpz.audioplayer.data/composers
|
||||
content://com.maxmpz.audioplayer.data/composers/#
|
||||
content://com.maxmpz.audioplayer.data/composers/#/files
|
||||
content://com.maxmpz.audioplayer.data/composers/#/files/#
|
||||
content://com.maxmpz.audioplayer.data/composers/#/files/meta
|
||||
content://com.maxmpz.audioplayer.data/composers/files
|
||||
|
||||
- Albums
|
||||
content://com.maxmpz.audioplayer.data/albums
|
||||
content://com.maxmpz.audioplayer.data/albums/#/files
|
||||
content://com.maxmpz.audioplayer.data/albums/#/files/#
|
||||
content://com.maxmpz.audioplayer.data/albums/#/files/meta
|
||||
content://com.maxmpz.audioplayer.data/albums/files
|
||||
|
||||
- Albums by Genres
|
||||
content://com.maxmpz.audioplayer.data/genres/#/albums
|
||||
content://com.maxmpz.audioplayer.data/genres/#/albums/meta
|
||||
content://com.maxmpz.audioplayer.data/genres/#/albums/#/files
|
||||
content://com.maxmpz.audioplayer.data/genres/#/albums/#/files/#
|
||||
content://com.maxmpz.audioplayer.data/genres/#/albums/#/files/meta
|
||||
content://com.maxmpz.audioplayer.data/genres/#/albums/files
|
||||
content://com.maxmpz.audioplayer.data/genres/albums
|
||||
|
||||
- Albums by Artists
|
||||
content://com.maxmpz.audioplayer.data/artists/#/albums
|
||||
content://com.maxmpz.audioplayer.data/artists/#/albums/meta
|
||||
content://com.maxmpz.audioplayer.data/artists/#/albums/#/files
|
||||
content://com.maxmpz.audioplayer.data/artists/#/albums/#/files/#
|
||||
content://com.maxmpz.audioplayer.data/artists/#/albums/#/files/meta
|
||||
content://com.maxmpz.audioplayer.data/artists/#/albums/files
|
||||
content://com.maxmpz.audioplayer.data/artists/albums
|
||||
|
||||
- Albums by Composers
|
||||
content://com.maxmpz.audioplayer.data/composers/#/albums
|
||||
content://com.maxmpz.audioplayer.data/composers/#/albums/meta
|
||||
content://com.maxmpz.audioplayer.data/composers/#/albums/#/files
|
||||
content://com.maxmpz.audioplayer.data/composers/#/albums/#/files/#
|
||||
content://com.maxmpz.audioplayer.data/composers/#/albums/#/files/meta
|
||||
content://com.maxmpz.audioplayer.data/composers/#/albums/files
|
||||
content://com.maxmpz.audioplayer.data/composers/albums
|
||||
|
||||
- Artists Albums
|
||||
content://com.maxmpz.audioplayer.data/artists_albums
|
||||
content://com.maxmpz.audioplayer.data/artists_albums/meta
|
||||
content://com.maxmpz.audioplayer.data/artists_albums/#/files
|
||||
content://com.maxmpz.audioplayer.data/artists_albums/#/files/#
|
||||
content://com.maxmpz.audioplayer.data/artists_albums/#/files/meta
|
||||
content://com.maxmpz.audioplayer.data/artists_albums/files
|
||||
|
||||
- Playlists
|
||||
content://com.maxmpz.audioplayer.data/playlists
|
||||
content://com.maxmpz.audioplayer.data/playlists/#
|
||||
content://com.maxmpz.audioplayer.data/playlists/#/files
|
||||
content://com.maxmpz.audioplayer.data/playlists/#/files/#
|
||||
content://com.maxmpz.audioplayer.data/playlists/#/files/meta
|
||||
content://com.maxmpz.audioplayer.data/playlists/files
|
||||
|
||||
- Library Search
|
||||
content://com.maxmpz.audioplayer.data/search
|
||||
|
||||
- Equalizer Presets
|
||||
content://com.maxmpz.audioplayer.data/eq_presets
|
||||
content://com.maxmpz.audioplayer.data/eq_presets/#
|
||||
content://com.maxmpz.audioplayer.data/eq_presets_songs
|
||||
content://com.maxmpz.audioplayer.data/queue
|
||||
content://com.maxmpz.audioplayer.data/queue/#
|
||||
|
||||
*
|
||||
* Extras:
|
||||
* - paused - boolean - (optional) default false. OPEN_TO_PLAY command starts playing the file immediately, unless "paused" extra is true.
|
||||
* (see PowerampAPI.PAUSED)
|
||||
*
|
||||
* - pos - int - (optional) seek to this position in song before playing (see PowerampAPI.Track.POSITION)
|
||||
*/
|
||||
public static final int OPEN_TO_PLAY = 20;
|
||||
|
||||
/**
|
||||
* Extras:
|
||||
* - id - long - preset ID
|
||||
*/
|
||||
public static final int SET_EQU_PRESET = 50;
|
||||
|
||||
/**
|
||||
* Extras:
|
||||
* - value - string - equalizer values, see ACTION_EQU_CHANGED description.
|
||||
*/
|
||||
public static final int SET_EQU_STRING = 51;
|
||||
|
||||
/**
|
||||
* Extras:
|
||||
* - name - string - equalizer band (bass/treble/preamp/31/62../8K/16K) name
|
||||
* - value - float - equalizer band value (bass/treble/, 31/62../8K/16K => -1.0...1.0, preamp => 0..2.0)
|
||||
*/
|
||||
public static final int SET_EQU_BAND = 52;
|
||||
|
||||
/**
|
||||
* Extras:
|
||||
* - equ - boolean - if exists and true, equalizer is enabled
|
||||
* - tone - boolean - if exists and true, tone is enabled
|
||||
*/
|
||||
public static final int SET_EQU_ENABLED = 53;
|
||||
|
||||
/**
|
||||
* Used by Notification controls to stop pending/paused service/playback and unload/remove notification.
|
||||
* Since 2.0.6
|
||||
*/
|
||||
public static final int STOP_SERVICE = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Mixed.
|
||||
*/
|
||||
public static final String API_VERSION = "api";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Mixed.
|
||||
*/
|
||||
public static final String CONTENT = "content";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* String.
|
||||
*/
|
||||
public static final String PACKAGE = "pak";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* String.
|
||||
*/
|
||||
public static final String LABEL = "label";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean.
|
||||
*/
|
||||
public static final String AUTO_HIDE = "autoHide";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Bitmap.
|
||||
*/
|
||||
public static final String ICON = "icon";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean.
|
||||
*/
|
||||
public static final String MATCH_FILE = "matchFile";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean
|
||||
*/
|
||||
public static final String SHOW_TOAST = "showToast";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* String.
|
||||
*/
|
||||
public static final String NAME = "name";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Mixed.
|
||||
*/
|
||||
public static final String VALUE = "value";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean.
|
||||
*/
|
||||
public static final String EQU = "equ";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean.
|
||||
*/
|
||||
public static final String TONE = "tone";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean.
|
||||
* Since 2.0.6
|
||||
*/
|
||||
public static final String KEEP_SERVICE = "keepService";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean
|
||||
* Since build 533
|
||||
*/
|
||||
public static final String BEEP = "beep";
|
||||
|
||||
|
||||
/**
|
||||
* Poweramp track changed.
|
||||
* Sticky intent.
|
||||
* Extras:
|
||||
* - track - bundle - Track bundle, see Track class.
|
||||
* - ts - long - timestamp of the event (System.currentTimeMillis()).
|
||||
* Note, that by default Poweramp won't search/download album art when screen is OFF, but will do that on next screen ON event.
|
||||
*/
|
||||
public static final String ACTION_TRACK_CHANGED = "com.maxmpz.audioplayer.TRACK_CHANGED";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* If both aaPath and aaBitmap extras are missing that means no album art exists for the current track(s).
|
||||
* Note that there is no direct Album Art to track relation, i.e. both track and album art can change independently from each other -
|
||||
* for example - when new album art asynchronously downloaded from internet or selected by user.
|
||||
* Sticky intent.
|
||||
* Extras:
|
||||
* - aaPath - String - (optional) if exists, direct path to the cached album art is available.
|
||||
* - aaBitmap - Bitmap - (optional) if exists, some rescaled up to 500x500 px album art bitmap is available.
|
||||
* There will be aaBitmap if aaPath is available, but image is bigger than 600x600 px.
|
||||
* - delayed - boolean - (optional) if true, this album art was downloaded or selected later by user.
|
||||
|
||||
* - ts - long - timestamp of the event (System.currentTimeMillis()).
|
||||
*/
|
||||
public static final String ACTION_AA_CHANGED = "com.maxmpz.audioplayer.AA_CHANGED";
|
||||
|
||||
/**
|
||||
* Poweramp playing status changed (track started/paused/resumed/ended, playing ended).
|
||||
* Sticky intent.
|
||||
* Extras:
|
||||
* - status - string - one of the STATUS_* values
|
||||
* - pos - int - (optional) current in-track position in seconds.
|
||||
* - ts - long - timestamp of the event (System.currentTimeMillis()).
|
||||
* - additional extras - depending on STATUS_ value (see STATUS_* description below).
|
||||
*/
|
||||
public static final String ACTION_STATUS_CHANGED = "com.maxmpz.audioplayer.STATUS_CHANGED";
|
||||
|
||||
/**
|
||||
* NON sticky intent.
|
||||
* - pos - int - current in-track position in seconds.
|
||||
*/
|
||||
public static final String ACTION_TRACK_POS_SYNC = "com.maxmpz.audioplayer.TPOS_SYNC";
|
||||
|
||||
/**
|
||||
* Poweramp repeat or shuffle mode changed.
|
||||
* Sticky intent.
|
||||
* Extras:
|
||||
* - repeat - int - new repeat mode. See RepeatMode class.
|
||||
* - shuffle - int - new shuffle mode. See ShuffleMode class.
|
||||
* - ts - long - timestamp of the event (System.currentTimeMillis()). *
|
||||
*/
|
||||
public static final String ACTION_PLAYING_MODE_CHANGED = "com.maxmpz.audioplayer.PLAYING_MODE_CHANGED";
|
||||
|
||||
/**
|
||||
* Poweramp equalizer settings changed.
|
||||
* Sticky intent.
|
||||
* Extras:
|
||||
* - name - string - preset name. If no name extra exists, it's not a preset.
|
||||
* - id - long - preset id. If no id extra exists, it's not a preset.
|
||||
* - value - string - equalizer and tone values in format:
|
||||
* bass=pos_float|treble=pos_float|31=float|62=float|....|16K=float|preamp=0.0 ... 2.0
|
||||
* where float = -1.0 ... 1.0, pos_float = 0.0 ... 1.0
|
||||
* - equ - boolean - true if equalizer bands are enabled
|
||||
* - tone - boolean - truel if tone bands are enabled
|
||||
* - ts - long - timestamp of the event (System.currentTimeMillis()).
|
||||
*/
|
||||
public static final String ACTION_EQU_CHANGED = "com.maxmpz.audioplayer.EQU_CHANGED";
|
||||
|
||||
/**
|
||||
* Special actions for com.maxmpz.audioplayer.PlayerUIActivity only.
|
||||
*/
|
||||
public static final String ACTION_SHOW_CURRENT = "com.maxmpz.audioplayer.ACTION_SHOW_CURRENT";
|
||||
public static final String ACTION_SHOW_LIST = "com.maxmpz.audioplayer.ACTION_SHOW_LIST";
|
||||
|
||||
|
||||
public static final String PACKAGE_NAME = "com.maxmpz.audioplayer";
|
||||
public static final String PLAYER_SERVICE_NAME = "com.maxmpz.audioplayer.player.PlayerService";
|
||||
|
||||
public static final ComponentName PLAYER_SERVICE_COMPONENT_NAME = new ComponentName(PACKAGE_NAME, PLAYER_SERVICE_NAME);
|
||||
|
||||
public static final String ACTIVITY_PLAYER_UI = "com.maxmpz.audioplayer.PlayerUIActivity";
|
||||
public static final String ACTIVITY_EQ = "com.maxmpz.audioplayer.EqActivity";
|
||||
|
||||
/**
|
||||
* If com.maxmpz.audioplayer.ACTION_SHOW_LIST action is sent to this activity, it will react to some extras.
|
||||
* Extras:
|
||||
* Data:
|
||||
* - uri - uri of the list to display.
|
||||
*/
|
||||
public static final String ACTIVITY_PLAYLIST = "com.maxmpz.audioplayer.PlayListActivity";
|
||||
public static final String ACTIVITY_SETTINGS = "com.maxmpz.audioplayer.preference.SettingsActivity";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* String.
|
||||
*/
|
||||
public static final String ALBUM_ART_PATH = "aaPath";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Bitmap.
|
||||
*/
|
||||
public static final String ALBUM_ART_BITMAP = "aaBitmap";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* boolean.
|
||||
*/
|
||||
public static final String DELAYED = "delayed";
|
||||
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* long.
|
||||
*/
|
||||
public static final String TIMESTAMP = "ts";
|
||||
|
||||
/**
|
||||
* STATUS_CHANGED extra. See Status class for values.
|
||||
* Int.
|
||||
*/
|
||||
public static final String STATUS = "status";
|
||||
|
||||
/**
|
||||
* STATUS extra values.
|
||||
*/
|
||||
public static final class Status {
|
||||
/**
|
||||
* STATUS_CHANGED status value - track has been started to play or has been paused.
|
||||
* Note that Poweramp will start track immediately into this state when it's just loaded to avoid STARTED => PAUSED transition.
|
||||
* Additional extras:
|
||||
* track - bundle - track info
|
||||
* paused - boolean - true if track paused, false if track resumed
|
||||
*/
|
||||
public static final int TRACK_PLAYING = 1;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Additional extras:
|
||||
* track - bundle - track info
|
||||
* failed - boolean - true if track failed to play
|
||||
*/
|
||||
public static final int TRACK_ENDED = 2;
|
||||
|
||||
/**
|
||||
* STATUS_CHANGED status value - Poweramp finished playing some list and stopped.
|
||||
*/
|
||||
public static final int PLAYING_ENDED = 3;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* STATUS_CHANGED trackEnded extra.
|
||||
* Boolean. True if track failed to play.
|
||||
*/
|
||||
public static final String FAILED = "failed";
|
||||
|
||||
/**
|
||||
* STATUS_CHANGED trackStarted/trackPausedResumed extra.
|
||||
* Boolean. True if track is paused.
|
||||
*/
|
||||
public static final String PAUSED = "paused";
|
||||
|
||||
/**
|
||||
* PLAYING_MODE_CHANGED extra. See ShuffleMode class.
|
||||
* Integer.
|
||||
*/
|
||||
public static final String SHUFFLE = "shuffle";
|
||||
|
||||
/**
|
||||
* PLAYING_MODE_CHANGED extra. See RepeatMode class.
|
||||
* Integer.
|
||||
*/
|
||||
public static final String REPEAT = "repeat";
|
||||
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Long.
|
||||
*/
|
||||
public static final String ID = "id";
|
||||
|
||||
/**
|
||||
* STATUS_CHANGED track extra.
|
||||
* Bundle.
|
||||
*/
|
||||
public static final String TRACK = "track";
|
||||
|
||||
|
||||
/**
|
||||
* shuffle extras values.
|
||||
*/
|
||||
public static final class ShuffleMode {
|
||||
public static final int SHUFFLE_NONE = 0;
|
||||
public static final int SHUFFLE_ALL = 1;
|
||||
public static final int SHUFFLE_SONGS = 2;
|
||||
public static final int SHUFFLE_CATS = 3; // Songs in order.
|
||||
public static final int SHUFFLE_SONGS_AND_CATS = 4; // Songs shuffled.
|
||||
}
|
||||
|
||||
/**
|
||||
* repeat extras values.
|
||||
*/
|
||||
public static final class RepeatMode {
|
||||
public static final int REPEAT_NONE = 0;
|
||||
public static final int REPEAT_ON = 1;
|
||||
public static final int REPEAT_ADVANCE = 2;
|
||||
public static final int REPEAT_SONG = 3;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* STATUS_CHANGED track extra fields.
|
||||
*/
|
||||
public static final class Track {
|
||||
/**
|
||||
* Id of the current track.
|
||||
* Can be a playlist entry id.
|
||||
* Long.
|
||||
*/
|
||||
public static final String ID = "id";
|
||||
|
||||
/**
|
||||
* "Real" id. In case of playlist entry, this is always resolved to Poweramp folder_files table row ID or System Library MediaStorage.Audio._ID.
|
||||
* Long.
|
||||
*/
|
||||
public static final String REAL_ID = "realId";
|
||||
|
||||
/**
|
||||
* Category type.
|
||||
* See Track.Type class.
|
||||
* Int.
|
||||
*/
|
||||
public static final String TYPE = "type";
|
||||
|
||||
/**
|
||||
* Category URI match.
|
||||
* Int.
|
||||
*/
|
||||
public static final String CAT = "cat";
|
||||
|
||||
/**
|
||||
* Boolean.
|
||||
*/
|
||||
public static final String IS_CUE = "isCue";
|
||||
|
||||
/**
|
||||
* Category URI.
|
||||
* Uri.
|
||||
*/
|
||||
public static final String CAT_URI = "catUri";
|
||||
|
||||
/**
|
||||
* File type. See Track.FileType.
|
||||
* Integer.
|
||||
*/
|
||||
public static final String FILE_TYPE = "fileType";
|
||||
|
||||
/**
|
||||
* Song file path.
|
||||
* String
|
||||
*/
|
||||
public static final String PATH = "path";
|
||||
|
||||
/**
|
||||
* Song title.
|
||||
* String
|
||||
*/
|
||||
public static final String TITLE = "title";
|
||||
|
||||
/**
|
||||
* Song album.
|
||||
* String.
|
||||
*/
|
||||
public static final String ALBUM = "album";
|
||||
|
||||
/**
|
||||
* Song artist.
|
||||
* String.
|
||||
*/
|
||||
public static final String ARTIST = "artist";
|
||||
|
||||
/**
|
||||
* Song duration in seconds.
|
||||
* Int.
|
||||
*/
|
||||
public static final String DURATION = "dur";
|
||||
|
||||
/**
|
||||
* Position in song in seconds.
|
||||
* Int.
|
||||
*/
|
||||
public static final String POSITION = "pos";
|
||||
|
||||
/**
|
||||
* Position in a list.
|
||||
* Int.
|
||||
*/
|
||||
public static final String POS_IN_LIST = "posInList";
|
||||
|
||||
/**
|
||||
* List size.
|
||||
* Int.
|
||||
*/
|
||||
public static final String LIST_SIZE = "listSize";
|
||||
|
||||
/**
|
||||
* Song sample rate.
|
||||
* Int.
|
||||
*/
|
||||
public static final String SAMPLE_RATE = "sampleRate";
|
||||
|
||||
/**
|
||||
* Song number of channels.
|
||||
* Int.
|
||||
*/
|
||||
public static final String CHANNELS = "channels";
|
||||
|
||||
/**
|
||||
* Song average bitrate.
|
||||
* Int.
|
||||
*/
|
||||
public static final String BITRATE = "bitRate";
|
||||
|
||||
/**
|
||||
* Resolved codec name for the song.
|
||||
* Int.
|
||||
*/
|
||||
public static final String CODEC = "codec";
|
||||
|
||||
/**
|
||||
* Track flags.
|
||||
* Int.
|
||||
*/
|
||||
public static final String FLAGS = "flags";
|
||||
|
||||
/**
|
||||
* Track.fileType values.
|
||||
*/
|
||||
public static final class FileType {
|
||||
public static final int mp3 = 0;
|
||||
public static final int flac = 1;
|
||||
public static final int m4a = 2;
|
||||
public static final int mp4 = 3;
|
||||
public static final int ogg = 4;
|
||||
public static final int wma = 5;
|
||||
public static final int wav = 6;
|
||||
public static final int tta = 7;
|
||||
public static final int ape = 8;
|
||||
public static final int wv = 9;
|
||||
public static final int aac = 10;
|
||||
public static final int mpga = 11;
|
||||
public static final int amr = 12;
|
||||
public static final int _3gp = 13;
|
||||
public static final int mpc = 14;
|
||||
public static final int aiff = 15;
|
||||
public static final int aif = 16;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track.flags bitset values. First 3 bits = FLAG_ADVANCE_*
|
||||
*/
|
||||
public static final class Flags {
|
||||
public static final int FLAG_ADVANCE_NONE = 0;
|
||||
public static final int FLAG_ADVANCE_FORWARD = 1;
|
||||
public static final int FLAG_ADVANCE_BACKWARD = 2;
|
||||
public static final int FLAG_ADVANCE_FORWARD_CAT = 3;
|
||||
public static final int FLAG_ADVANCE_BACKWARD_CAT = 4;
|
||||
|
||||
public static final int FLAG_ADVANCE_MASK = 0x7; // 111
|
||||
|
||||
public static final int FLAG_NOTIFICATION_UI = 0x20;
|
||||
public static final int FLAG_FIRST_IN_PLAYER_SESSION = 0x40; // Currently used just to indicate that track is first in playerservice session.
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Cats {
|
||||
public static final int ROOT = 0;
|
||||
public static final int FOLDERS = 10;
|
||||
public static final int GENRES_ID_ALBUMS = 210;
|
||||
public static final int ALBUMS = 200;
|
||||
public static final int GENRES = 320;
|
||||
public static final int ARTISTS = 500;
|
||||
public static final int ARTISTS_ID_ALBUMS = 220;
|
||||
public static final int ARTISTS__ALBUMS = 250;
|
||||
public static final int COMPOSERS = 600;
|
||||
public static final int COMPOSERS_ID_ALBUMS = 230;
|
||||
public static final int PLAYLISTS = 100;
|
||||
public static final int QUEUE = 800;
|
||||
public static final int MOST_PLAYED = 43;
|
||||
public static final int TOP_RATED = 48;
|
||||
public static final int RECENTLY_ADDED = 53;
|
||||
public static final int RECENTLY_PLAYED = 58;
|
||||
}
|
||||
|
||||
|
||||
public static final class Scanner {
|
||||
|
||||
/**
|
||||
* Poweramp Scanner action.
|
||||
*
|
||||
* Poweramp Scanner scanning process is 2 step:
|
||||
* 1. Folders scan.
|
||||
* Checks filesystem and updates DB with folders/files structure.
|
||||
* 2. Tags scan.
|
||||
* Iterates over files in DB with TAG_STATUS == TAG_NOT_SCANNED and scans them with tag scanner.
|
||||
*
|
||||
* Poweramp Scanner is a IntentService, this means multiple scan requests at the same time (or during another scans) are queued.
|
||||
* ACTION_SCAN_DIRS actions are prioritized and executed before ACTION_SCAN_TAGS.
|
||||
*
|
||||
* Poweramp main scan action, which scans either incrementally or from scratch the set of folders, which is configured by user in Poweramp Settings.
|
||||
* Poweramp will always do ACTION_SCAN_TAGS automatically after ACTION_SCAN_DIRS is finished and some changes are required to song tags in DB.
|
||||
* Unless, fullRescan specified, Poweramp will not remove songs if they are missing from filesystem due to unmounted storages.
|
||||
* Normal menu => Rescan calls ACTION_SCAN_DIRS without extras
|
||||
*
|
||||
* Poweramp Scanner sends appropriate broadcast intents:
|
||||
* 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.
|
||||
*
|
||||
* Extras:
|
||||
* - fastScan - Poweramp will not check folders and scan files which hasn't been modified from previous scan. Based on files last modified timestamp.
|
||||
* Poweramp doesn;t send
|
||||
*
|
||||
* - eraseTags - Poweramp will clean all tags from exisiting songs. This causes each song to be re-scanned for tags.
|
||||
* Warning: as a side effect, cleans CUE tracks from user created playlists.
|
||||
* This is because scanner can't incrementaly re-scan CUE sheets, so they are deleted from DB.
|
||||
*
|
||||
* - fullRescan - Poweramp will also check for folders/files from missing/unmounted storages and will remove them from DB.
|
||||
* Warning: removed songs also disappear from user created playlists.
|
||||
* Used in Poweramp only when user specificaly goes to Settings and does Full Rescan (after e.g. SD card change).
|
||||
*
|
||||
*/
|
||||
public static final String ACTION_SCAN_DIRS = "com.maxmpz.audioplayer.ACTION_SCAN_DIRS";
|
||||
|
||||
/**
|
||||
* Poweramp Scanner action.
|
||||
* 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.
|
||||
*
|
||||
* Extras:
|
||||
* - fastScan - If true, scanner doesn't send ACTION_TAGS_SCAN_STARTED/ACTION_TAGS_SCAN_PROGRESS/ACTION_TAGS_SCAN_FINISHED intents,
|
||||
* just sends ACTION_FAST_TAGS_SCAN_FINISHED when done.
|
||||
* It doesn't modify scanning logic otherwise.
|
||||
*/
|
||||
public static final String ACTION_SCAN_TAGS = "com.maxmpz.audioplayer.ACTION_SCAN_TAGS";
|
||||
|
||||
|
||||
/**
|
||||
* Broadcast.
|
||||
* Poweramp Scanner started folders scan.
|
||||
* This is sticky broadcast, so Poweramp folder scanner running status can be polled via registerReceiver() return value.
|
||||
*/
|
||||
public static final String ACTION_DIRS_SCAN_STARTED = "com.maxmpz.audioplayer.ACTION_DIRS_SCAN_STARTED";
|
||||
/**
|
||||
* Broadcast.
|
||||
* Poweramp Scanner finished folders scan.
|
||||
*/
|
||||
public static final String ACTION_DIRS_SCAN_FINISHED = "com.maxmpz.audioplayer.ACTION_DIRS_SCAN_FINISHED";
|
||||
/**
|
||||
* Broadcast.
|
||||
* Poweramp Scanner started tag scan.
|
||||
* This is sticky broadcast, so Poweramp tag scanner running status can be polled via registerReceiver() return value.
|
||||
*/
|
||||
public static final String ACTION_TAGS_SCAN_STARTED = "com.maxmpz.audioplayer.ACTION_TAGS_SCAN_STARTED";
|
||||
/**
|
||||
* Broadcast.
|
||||
* Poweramp Scanner tag scan in progess.
|
||||
* Extras:
|
||||
* - progress - 0-100 progress of scanning.
|
||||
*/
|
||||
public static final String ACTION_TAGS_SCAN_PROGRESS = "com.maxmpz.audioplayer.ACTION_TAGS_SCAN_PROGRESS";
|
||||
/**
|
||||
* Broadcast.
|
||||
* Poweramp Scanner finished tag scan.
|
||||
* Extras:
|
||||
* - 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).
|
||||
*/
|
||||
public static final String ACTION_TAGS_SCAN_FINISHED = "com.maxmpz.audioplayer.ACTION_TAGS_SCAN_FINISHED";
|
||||
/**
|
||||
* Broadcast.
|
||||
* Poweramp Scanner finished fast tag scan. Only fired when ACTION_SCAN_TAGS is called with extra fastScan = true.
|
||||
* Extras:
|
||||
* - trackContentChanged - boolean - true if at least on track has been scanned, false if no tags scanned (probably, because all files are up-to-date).
|
||||
*/
|
||||
public static final String ACTION_FAST_TAGS_SCAN_FINISHED = "com.maxmpz.audioplayer.ACTION_FAST_TAGS_SCAN_FINISHED";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean.
|
||||
*/
|
||||
public static final String EXTRA_FAST_SCAN = "fastScan";
|
||||
/**
|
||||
* Extra.
|
||||
* Int.
|
||||
*/
|
||||
public static final String EXTRA_PROGRESS = "progress";
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean.
|
||||
*/
|
||||
public static final String EXTRA_TRACK_CONTENT_CHANGED = "trackContentChanged";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean.
|
||||
*/
|
||||
public static final String EXTRA_ERASE_TAGS = "eraseTags";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* Boolean.
|
||||
*/
|
||||
public static final String EXTRA_FULL_RESCAN = "fullRescan";
|
||||
|
||||
/**
|
||||
* Extra.
|
||||
* String.
|
||||
*/
|
||||
public static final String EXTRA_CAUSE = "cause";
|
||||
}
|
||||
|
||||
public static final class Settings {
|
||||
public static final String ACTION_EXPORT_SETTINGS = "com.maxmpz.audioplayer.ACTION_EXPORT_SETTINGS";
|
||||
public static final String ACTION_IMPORT_SETTINGS = "com.maxmpz.audioplayer.ACTION_IMPORT_SETTINGS";
|
||||
|
||||
public static final String EXTRA_UI = "ui";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package max.music_cyclon;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class Config implements Parcelable {
|
||||
|
||||
private final String name;
|
||||
private final JSONObject json;
|
||||
|
||||
public Config(String name, JSONObject json) {
|
||||
this.name = name;
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
public Config(String name) {
|
||||
this(name, new JSONObject());
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getSize(Resources resources) {
|
||||
return json.optInt("size", resources.getInteger(R.integer.size));
|
||||
}
|
||||
|
||||
public boolean isRandom(Resources resources) {
|
||||
return json.optBoolean("size", resources.getBoolean(R.bool.random));
|
||||
}
|
||||
|
||||
public boolean isAlbum(Resources resources) {
|
||||
return json.optBoolean("use_albums", resources.getBoolean(R.bool.use_albums));
|
||||
}
|
||||
|
||||
public String getQuery(Resources resources) {
|
||||
return json.optString("query", resources.getString(R.string.query));
|
||||
}
|
||||
|
||||
public boolean isStartCharging(Resources resources) {
|
||||
return json.optBoolean("start_charging", resources.getBoolean(R.bool.start_charging));
|
||||
}
|
||||
|
||||
public int getDownloadInterval(Resources resources) {
|
||||
return json.optInt("download_interval", resources.getInteger(R.integer.download_interval));
|
||||
}
|
||||
|
||||
public JSONObject getJson() {
|
||||
return json;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(name);
|
||||
dest.writeString(json.toString());
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Config> CREATOR = new Parcelable.Creator<Config>() {
|
||||
public Config createFromParcel(Parcel in) {
|
||||
try {
|
||||
return new Config(in.readString(), new JSONObject(in.readString()));
|
||||
} catch (JSONException e) {
|
||||
return new Config("none");
|
||||
}
|
||||
}
|
||||
|
||||
public Config[] newArray(int size) {
|
||||
return new Config[size];
|
||||
}
|
||||
};
|
||||
|
||||
public static List<Config> load(InputStream in) throws JSONException {
|
||||
String data = convertStreamToString(in);
|
||||
return load(data);
|
||||
}
|
||||
|
||||
public static List<Config> load(String data) throws JSONException {
|
||||
JSONObject jsonConfigs = new JSONObject(data);
|
||||
|
||||
ArrayList<Config> configs = new ArrayList<>();
|
||||
|
||||
Iterator keys = jsonConfigs.keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = (String) keys.next();
|
||||
configs.add(new Config(key, jsonConfigs.getJSONObject(key)));
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
public static void save(Iterable<Config> configs, OutputStream fos) throws JSONException, IOException {
|
||||
JSONObject jsonConfigs = new JSONObject();
|
||||
|
||||
for (Config config : configs) {
|
||||
jsonConfigs.put(config.getName(), config.getJson());
|
||||
}
|
||||
|
||||
fos.write(jsonConfigs.toString().getBytes("UTF-8"));
|
||||
}
|
||||
|
||||
private static String convertStreamToString(InputStream is) {
|
||||
Scanner s = new Scanner(is).useDelimiter("\\A");
|
||||
return s.hasNext() ? s.next() : "";
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package max.music_cyclon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class InfoPreference extends DialogPreference {
|
||||
|
||||
public InfoPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setDialogMessage("Info");
|
||||
}
|
||||
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
package max.music_cyclon;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.util.JsonReader;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class LibraryService extends IntentService {
|
||||
|
||||
private final String host;
|
||||
private final int port;
|
||||
|
||||
public static int NOTIFICATION_ID = new Random().nextInt();
|
||||
|
||||
public LibraryService() {
|
||||
this("max-arch", 5000);
|
||||
}
|
||||
|
||||
public LibraryService(String host, int port) {
|
||||
super("max.music_cyclon.LibraryService");
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public List<String> fetchRandom(String address, int amount) throws IOException {
|
||||
CloseableHttpClient httpclient = HttpClients.createDefault();
|
||||
|
||||
HttpGet httpGet = new HttpGet(address + "/random/" + amount);
|
||||
|
||||
|
||||
CloseableHttpResponse response = httpclient.execute(httpGet);
|
||||
|
||||
if (response.getStatusLine().getStatusCode() != 200) {
|
||||
Log.e("ERROR", "Server returned HTTP " + response.getStatusLine().getStatusCode());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
JsonReader reader = new JsonReader(new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8")));
|
||||
|
||||
ArrayList<String> items = new ArrayList<>();
|
||||
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
items.add(reader.nextString());
|
||||
}
|
||||
reader.endArray();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder notificationBuilder() {
|
||||
return new NotificationCompat.Builder(this)
|
||||
.setSmallIcon(R.mipmap.ic_launcher);
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder progressNotificationBuilder() {
|
||||
return notificationBuilder().setUsesChronometer(true)
|
||||
.setOngoing(true)
|
||||
.setProgress(0, 0, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(Integer.parseInt(settings.getString("threads", "2")));
|
||||
|
||||
String address = settings.getString("address", "127.0.0.1");
|
||||
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
NotificationCompat.Builder builder = progressNotificationBuilder().setContentTitle("Aktualisiere Musik");
|
||||
File root = new File(Environment.getExternalStorageDirectory(), "library");
|
||||
|
||||
if (root.exists() && !root.isDirectory()) {
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder().setContentTitle("Library is no dictionary! Fix manually").build());
|
||||
return;
|
||||
}
|
||||
|
||||
AtomicInteger current = new AtomicInteger();
|
||||
|
||||
FileTracker tracker = new FileTracker(getSharedPreferences("library", MODE_PRIVATE));
|
||||
|
||||
|
||||
List<String> items;
|
||||
try {
|
||||
builder.setContentTitle("Fetching music information");
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||
items = fetchRandom(address, Integer.parseInt(settings.getString("download", "10")));
|
||||
|
||||
builder.setContentTitle("Cleaning library");
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||
tracker.delete();
|
||||
} catch (IOException e) {
|
||||
Log.wtf("WTF", e);
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder().setContentTitle("Remote not available").build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (root.exists() && root.list().length != 0) {
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder().setContentTitle("Library not empty! Clean in manually").build());
|
||||
return;
|
||||
}
|
||||
|
||||
builder.setContentTitle("Mixing new music!");
|
||||
builder.setProgress(items.size(), 0, false);
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(items.size());
|
||||
|
||||
for (int i = 0, size = items.size(); i < size; i++) {
|
||||
executor.submit(new ProcessTask(address, latch, items.get(i), tracker,
|
||||
current, items.size(), builder, notificationManager));
|
||||
}
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
tracker.commit();
|
||||
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
|
||||
SharedPreferences preferences = getSharedPreferences("info", MODE_PRIVATE);
|
||||
preferences.edit().putLong("last_updated", System.currentTimeMillis()).apply();
|
||||
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder().setContentTitle("Musik aktualisiert").build());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package max.music_cyclon;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v14.preference.PreferenceFragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat;
|
||||
|
||||
public class MainPreferenceActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main_preference);
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.preference_toolbar);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
Intent myIntent = new Intent(getApplicationContext(), SynchronizeActivity.class);
|
||||
startActivityForResult(myIntent, 0);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
public static class MainPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle bundle, String s) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package max.music_cyclon;
|
||||
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
|
||||
private final List<String> configs = new ArrayList<>();
|
||||
|
||||
private final Map<String, Config> configData = new HashMap<>();
|
||||
|
||||
public PagerAdapter(List<Config> configs , FragmentManager fm) {
|
||||
super(fm);
|
||||
|
||||
|
||||
for (Config config : configs) {
|
||||
this.configs.add(config.getName());
|
||||
this.configData.put(config.getName(), config);
|
||||
}
|
||||
}
|
||||
|
||||
public void save(OutputStream os) throws JSONException, IOException {
|
||||
Config.save(configData.values(), os);
|
||||
}
|
||||
|
||||
public boolean add(String name) {
|
||||
configData.put(name, new Config(name));
|
||||
return configs.add(name);
|
||||
}
|
||||
|
||||
public void remove(String name) {
|
||||
configData.remove(name);
|
||||
configs.remove(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int i) {
|
||||
SynchronizeConfigFragment fragment = new SynchronizeConfigFragment();
|
||||
|
||||
String name = getConfigs().get(i);
|
||||
|
||||
fragment.setName(name);
|
||||
fragment.setPagerAdapter(this);
|
||||
fragment.setConfig(configData.get(name));
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return getConfigs().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(Object object) {
|
||||
// http://stackoverflow.com/a/10399127
|
||||
return PagerAdapter.POSITION_NONE;
|
||||
}
|
||||
|
||||
public List<String> getConfigs() {
|
||||
return configs;
|
||||
}
|
||||
|
||||
public List<Config> getConfigData() {
|
||||
return new ArrayList<>(configData.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return getConfigs().get(position);
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
package max.music_cyclon;
|
||||
|
||||
import android.os.Environment;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.zip.Adler32;
|
||||
|
||||
public class ProcessTask implements Runnable {
|
||||
|
||||
private String address;
|
||||
private final CountDownLatch latch;
|
||||
|
||||
private final String item;
|
||||
private final FileTracker tracker;
|
||||
|
||||
private final AtomicInteger current;
|
||||
private final int maximum;
|
||||
private final NotificationCompat.Builder builder;
|
||||
private final NotificationManagerCompat notificationManager;
|
||||
|
||||
public ProcessTask(String address, CountDownLatch latch, String item, FileTracker tracker,
|
||||
AtomicInteger current, int maximum, NotificationCompat.Builder builder, NotificationManagerCompat notificationCompat) {
|
||||
this.address = address;
|
||||
this.latch = latch;
|
||||
this.item = item;
|
||||
|
||||
this.tracker = tracker;
|
||||
this.current = current;
|
||||
this.maximum = maximum;
|
||||
this.builder = builder;
|
||||
this.notificationManager = notificationCompat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
File root = new File(Environment.getExternalStorageDirectory(), "library");
|
||||
|
||||
|
||||
try {
|
||||
File target = new File(root, item);
|
||||
Adler32 checksum = new Adler32();
|
||||
|
||||
InputStream input = prepareConnection(address, item);
|
||||
|
||||
if (input != null) {
|
||||
|
||||
FileOutputStream output = FileUtils.openOutputStream(target);
|
||||
|
||||
byte[] buffer = new byte[4 * 1024];
|
||||
int n;
|
||||
while (-1 != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
checksum.update(buffer, 0, n);
|
||||
}
|
||||
|
||||
output.flush();
|
||||
output.close();
|
||||
input.close();
|
||||
}
|
||||
|
||||
// todo else error
|
||||
|
||||
tracker.track(target, checksum.getValue());
|
||||
|
||||
latch.countDown();
|
||||
|
||||
int current = this.current.incrementAndGet();
|
||||
builder.setProgress(maximum, current, false);
|
||||
builder.setContentText(current + "/" + maximum);
|
||||
notificationManager.notify(LibraryService.NOTIFICATION_ID, builder.build());
|
||||
} catch (IOException e) {
|
||||
Log.wtf("WTF", e);
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream prepareConnection(String address, String item) throws IOException {
|
||||
CloseableHttpClient httpclient = HttpClients.createDefault();
|
||||
|
||||
HttpPost httpPost = new HttpPost(address + "/get");
|
||||
|
||||
httpPost.setEntity(new ByteArrayEntity(item.getBytes("UTF-8")));
|
||||
|
||||
CloseableHttpResponse response = httpclient.execute(httpPost);
|
||||
|
||||
if (response.getStatusLine().getStatusCode() != 200) {
|
||||
Log.e("ERROR", "Server returned HTTP " + response.getStatusLine().getStatusCode());
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.getEntity().getContent();
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package max.music_cyclon;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.preference.PreferenceFragment;
|
||||
|
||||
|
||||
public class SettingsActivity extends PreferenceActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getFragmentManager().beginTransaction().replace(android.R.id.content, new MyPreferenceFragment()).commit();
|
||||
}
|
||||
|
||||
|
||||
public static class MyPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
findPreference("start").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent intent = new Intent(getActivity(), LibraryService.class);
|
||||
getActivity().startService(intent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
package max.music_cyclon;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.DataSetObserver;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import max.music_cyclon.service.LibraryService;
|
||||
import max.music_cyclon.slidingtab.SlidingTabLayout;
|
||||
|
||||
|
||||
public class SynchronizeActivity extends AppCompatActivity {
|
||||
|
||||
private PagerAdapter pagerAdapter;
|
||||
|
||||
/** Messenger for communicating with service. */
|
||||
Messenger mService = null;
|
||||
/** Flag indicating whether we have called bind on the service. */
|
||||
private boolean mIsBound;
|
||||
|
||||
/**
|
||||
* Target we publish for clients to send messages to IncomingHandler.
|
||||
*/
|
||||
private final Messenger mMessenger = new Messenger(new IncomingHandler());
|
||||
private ProgressDialog syncProgress;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_synchronize);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.my_toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
List<Config> configs = Collections.emptyList();
|
||||
try {
|
||||
FileInputStream in = openFileInput("configs.json");
|
||||
configs = Config.load(in);
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
pagerAdapter = new PagerAdapter(configs, getSupportFragmentManager());
|
||||
|
||||
|
||||
final ViewPager pager = (ViewPager) findViewById(R.id.container);
|
||||
assert pager != null;
|
||||
pager.setAdapter(pagerAdapter);
|
||||
|
||||
|
||||
// Initialize tabs
|
||||
final SlidingTabLayout tabs = (SlidingTabLayout) findViewById(R.id.tabs);
|
||||
assert tabs != null;
|
||||
tabs.setDistributeEvenly(true);
|
||||
tabs.setCustomTabColorizer(new SlidingTabLayout.TabColorizer() {
|
||||
@Override
|
||||
public int getIndicatorColor(int position) {
|
||||
return getResources().getColor(R.color.accentColor);
|
||||
}
|
||||
});
|
||||
|
||||
tabs.setViewPager(pager);
|
||||
|
||||
// Update tabs on dataset change
|
||||
pagerAdapter.registerDataSetObserver(new DataSetObserver() {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
tabs.setViewPager(pager);
|
||||
}
|
||||
});
|
||||
|
||||
View addButton = findViewById(R.id.add_button);
|
||||
assert addButton != null;
|
||||
addButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
pagerAdapter.add(UUID.randomUUID().toString().substring(0, 5));
|
||||
pagerAdapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
// Should we show an explanation?
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
|
||||
// Show an expanation to the user *asynchronously* -- don't block
|
||||
// this thread waiting for the user's response! After the user
|
||||
// sees the explanation, try again to request the permission.
|
||||
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
try {
|
||||
FileOutputStream fos = openFileOutput("configs.json", Context.MODE_PRIVATE);
|
||||
getPagerAdapter().save(fos);
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public PagerAdapter getPagerAdapter() {
|
||||
return pagerAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings:
|
||||
Intent preferenceIntent = new Intent(this, MainPreferenceActivity.class);
|
||||
startActivity(preferenceIntent);
|
||||
return true;
|
||||
|
||||
case R.id.action_sync:
|
||||
Intent intent = new Intent(SynchronizeActivity.this, LibraryService.class);
|
||||
List<Config> configs = getPagerAdapter().getConfigData();
|
||||
intent.putExtra("configs", configs.toArray(new Config[configs.size()]));
|
||||
SynchronizeActivity.this.startService(intent);
|
||||
|
||||
doBindService();
|
||||
|
||||
syncProgress = new ProgressDialog(SynchronizeActivity.this);
|
||||
syncProgress.setMessage("Synchronizing");
|
||||
syncProgress.setCancelable(false);
|
||||
syncProgress.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
syncProgress.show();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class IncomingHandler extends Handler {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case LibraryService.MSG_FINISHED:
|
||||
syncProgress.dismiss();
|
||||
break;
|
||||
default:
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for interacting with the main interface of the service.
|
||||
*/
|
||||
private ServiceConnection mConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
mService = new Messenger(service);
|
||||
|
||||
try {
|
||||
Message msg = Message.obtain(null, LibraryService.MSG_REGISTER_CLIENT);
|
||||
msg.replyTo = mMessenger;
|
||||
mService.send(msg);
|
||||
} catch (RemoteException ignored) {
|
||||
// In this case the service has crashed before we could even
|
||||
// do anything with it; we can count on soon being
|
||||
// disconnected (and then reconnected if it can be restarted)
|
||||
// so there is no need to do anything here.
|
||||
}
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
mService = null;
|
||||
}
|
||||
};
|
||||
|
||||
void doBindService() {
|
||||
bindService(new Intent(
|
||||
SynchronizeActivity.this,
|
||||
LibraryService.class
|
||||
), mConnection, Context.BIND_AUTO_CREATE);
|
||||
mIsBound = true;
|
||||
}
|
||||
|
||||
void doUnbindService() {
|
||||
if (mIsBound) {
|
||||
// If we have received the service, and hence registered with
|
||||
// it, then now is the time to unregister.
|
||||
if (mService != null) {
|
||||
try {
|
||||
Message msg = Message.obtain(null,
|
||||
LibraryService.MSG_UNREGISTER_CLIENT);
|
||||
msg.replyTo = mMessenger;
|
||||
mService.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
// There is nothing special we need to do if the service
|
||||
// has crashed.
|
||||
}
|
||||
}
|
||||
|
||||
// Detach our existing connection.
|
||||
unbindService(mConnection);
|
||||
mIsBound = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package max.music_cyclon;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.support.v7.preference.TwoStatePreference;
|
||||
|
||||
import com.takisoft.fix.support.v7.preference.EditTextPreference;
|
||||
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
public class SynchronizeConfigFragment extends PreferenceFragmentCompat {
|
||||
|
||||
private String name;
|
||||
private PagerAdapter pagerAdapter;
|
||||
private Config config;
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.sync_config);
|
||||
|
||||
setRetainInstance(true);
|
||||
|
||||
ConfigUpdater updater = new ConfigUpdater();
|
||||
|
||||
for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) {
|
||||
Preference preference = getPreferenceScreen().getPreference(i);
|
||||
if (config.getJson().has(preference.getKey())) {
|
||||
if (preference instanceof TwoStatePreference) {
|
||||
((TwoStatePreference) preference).setChecked(config.getJson().optBoolean(preference.getKey()));
|
||||
} else if (preference instanceof EditTextPreference) {
|
||||
((EditTextPreference) preference).setText(config.getJson().optString(preference.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
preference.setOnPreferenceChangeListener(updater);
|
||||
}
|
||||
|
||||
Preference removePreference = findPreference("remove");
|
||||
removePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (getPagerAdapter().getCount() == 1) {
|
||||
return false;
|
||||
}
|
||||
getPagerAdapter().remove(getName());
|
||||
getPagerAdapter().notifyDataSetChanged();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
|
||||
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
public PagerAdapter getPagerAdapter() {
|
||||
return pagerAdapter;
|
||||
}
|
||||
|
||||
public void setPagerAdapter(PagerAdapter pagerAdapter) {
|
||||
this.pagerAdapter = pagerAdapter;
|
||||
}
|
||||
|
||||
public void setConfig(Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private class ConfigUpdater implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object o) {
|
||||
String key = preference.getKey();
|
||||
try {
|
||||
config.getJson().put(key, o);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package max.music_cyclon.service;
|
||||
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.zip.Adler32;
|
||||
|
||||
import cz.msebera.android.httpclient.client.methods.CloseableHttpResponse;
|
||||
import cz.msebera.android.httpclient.client.methods.HttpGet;
|
||||
import cz.msebera.android.httpclient.impl.client.CloseableHttpClient;
|
||||
import cz.msebera.android.httpclient.impl.client.HttpClients;
|
||||
import max.music_cyclon.service.db.FileTracker;
|
||||
|
||||
public class DownloadTask implements Runnable {
|
||||
|
||||
private final URI uri;
|
||||
private final String itemPath;
|
||||
|
||||
private final FileTracker tracker;
|
||||
private final ProgressUpdater progressUpdater;
|
||||
private CountDownLatch itemsLeftLatch;
|
||||
|
||||
private static final CloseableHttpClient httpclient = HttpClients.createDefault();
|
||||
|
||||
public DownloadTask(URI uri, String itemPath,
|
||||
FileTracker tracker, ProgressUpdater progressUpdater,
|
||||
CountDownLatch itemsLeftLatch) {
|
||||
this.uri = uri;
|
||||
this.itemPath = itemPath;
|
||||
|
||||
this.tracker = tracker;
|
||||
this.progressUpdater = progressUpdater;
|
||||
this.itemsLeftLatch = itemsLeftLatch;
|
||||
}
|
||||
|
||||
private InputStream prepareConnection() throws IOException {
|
||||
HttpGet httpGet = new HttpGet(uri);
|
||||
|
||||
CloseableHttpResponse response = httpclient.execute(httpGet);
|
||||
|
||||
if (response.getStatusLine().getStatusCode() != 200) {
|
||||
Log.e("ERROR", "Server returned HTTP " + response.getStatusLine().getStatusCode());
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.getEntity().getContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
File root = new File(Environment.getExternalStorageDirectory(), "library");
|
||||
|
||||
try {
|
||||
File target = new File(root, itemPath);
|
||||
Adler32 checksum = new Adler32();
|
||||
|
||||
InputStream input = prepareConnection();
|
||||
|
||||
if (input != null) {
|
||||
|
||||
FileOutputStream output = FileUtils.openOutputStream(target);
|
||||
|
||||
byte[] buffer = new byte[4 * 1024];
|
||||
int n;
|
||||
while (-1 != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
checksum.update(buffer, 0, n);
|
||||
}
|
||||
|
||||
output.flush();
|
||||
output.close();
|
||||
input.close();
|
||||
}
|
||||
|
||||
tracker.track(target, checksum.getValue());
|
||||
} catch (IOException e) {
|
||||
Log.wtf("WTF", e);
|
||||
}
|
||||
|
||||
progressUpdater.increment();
|
||||
itemsLeftLatch.countDown();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package max.music_cyclon.service;
|
||||
|
||||
public class Item {
|
||||
|
||||
private int id;
|
||||
private String name;
|
||||
private String artist;
|
||||
private String album;
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getAlbum() {
|
||||
return album;
|
||||
}
|
||||
|
||||
public void setAlbum(String album) {
|
||||
this.album = album;
|
||||
}
|
||||
|
||||
public String getArtist() {
|
||||
return artist;
|
||||
}
|
||||
|
||||
public void setArtist(String artist) {
|
||||
this.artist = artist;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,341 @@
|
|||
package max.music_cyclon.service;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.JsonReader;
|
||||
import android.util.Log;
|
||||
|
||||
import com.maxmpz.poweramp.player.PowerampAPI;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Array;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import cz.msebera.android.httpclient.client.methods.CloseableHttpResponse;
|
||||
import cz.msebera.android.httpclient.client.methods.HttpGet;
|
||||
import cz.msebera.android.httpclient.impl.client.CloseableHttpClient;
|
||||
import cz.msebera.android.httpclient.impl.client.HttpClients;
|
||||
import max.music_cyclon.Config;
|
||||
import max.music_cyclon.service.db.FileTracker;
|
||||
|
||||
public class LibraryService extends IntentService {
|
||||
|
||||
|
||||
public static final FilenameFilter NOMEDIA_FILTER = new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File file, String s) {
|
||||
return !s.equals(".nomedia");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Command to the service to register a client, receiving callbacks
|
||||
* from the service. The Message's replyTo field must be a Messenger of
|
||||
* the client where callbacks should be sent.
|
||||
*/
|
||||
public static final int MSG_REGISTER_CLIENT = 1;
|
||||
|
||||
/**
|
||||
* Command to the service to unregister a client, ot stop receiving callbacks
|
||||
* from the service. The Message's replyTo field must be a Messenger of
|
||||
* the client as previously given with MSG_REGISTER_CLIENT.
|
||||
*/
|
||||
public static final int MSG_UNREGISTER_CLIENT = 2;
|
||||
|
||||
|
||||
public static final int MSG_CANCEL = 3;
|
||||
public static final int MSG_STARTED = 4;
|
||||
public static final int MSG_FINISHED = 5;
|
||||
public static final Random RANDOM = new Random();
|
||||
|
||||
/**
|
||||
* Keeps track of all current registered clients.
|
||||
*/
|
||||
private ArrayList<Messenger> mClients = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Target we publish for clients to send messages to IncomingHandler.
|
||||
*/
|
||||
private final Messenger mMessenger = new Messenger(new IncomingHandler());
|
||||
|
||||
public LibraryService() {
|
||||
super("max.music_cyclon.service.LibraryService");
|
||||
}
|
||||
|
||||
public List<Item> fetchRandom(String address, Config config, Resources resources) throws IOException {
|
||||
StringBuilder get;
|
||||
|
||||
if (config.isAlbum(resources)) {
|
||||
get = new StringBuilder("/album");
|
||||
} else {
|
||||
get = new StringBuilder("/item");
|
||||
}
|
||||
|
||||
String query = config.getQuery(resources);
|
||||
if (!query.isEmpty()) {
|
||||
get.append("/query/").append(query);
|
||||
}
|
||||
|
||||
get.append("?expand");
|
||||
|
||||
CloseableHttpClient httpclient = HttpClients.createDefault();
|
||||
HttpGet httpGet = new HttpGet(address + get);
|
||||
CloseableHttpResponse response = httpclient.execute(httpGet);
|
||||
|
||||
if (response.getStatusLine().getStatusCode() != 200) {
|
||||
Log.e("ERROR", "Server returned HTTP " + response.getStatusLine().getStatusCode());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
InputStream stream = response.getEntity().getContent();
|
||||
ArrayList<Item> items = parseJson(stream, config.getSize(resources));
|
||||
stream.close();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private ArrayList<Item> parseJson(InputStream stream, int size) throws IOException {
|
||||
JsonReader reader = new JsonReader(new BufferedReader(new InputStreamReader(stream, "UTF-8")));
|
||||
|
||||
ArrayList<Item> items = new ArrayList<>();
|
||||
ArrayList<ArrayList<Item>> albums = new ArrayList<>();
|
||||
|
||||
reader.beginObject();
|
||||
boolean isAlbums = reader.nextName().equals("albums");
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
if (isAlbums) {
|
||||
albums.add(parseAlbum(reader));
|
||||
} else {
|
||||
items.add(parseItem(reader));
|
||||
}
|
||||
}
|
||||
reader.endArray();
|
||||
reader.endObject();
|
||||
|
||||
items = selectRandom(items, size);
|
||||
|
||||
ArrayList<ArrayList<Item>> randomAlbums = selectRandom(albums, size);
|
||||
|
||||
for (ArrayList<Item> album : randomAlbums) {
|
||||
items.addAll(album);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public <T> ArrayList<T> selectRandom(ArrayList<T> list, int n) {
|
||||
ArrayList<T> out = new ArrayList<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
out.add(list.get(RANDOM.nextInt(list.size() - 1)));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private ArrayList<Item> parseAlbum(JsonReader reader) throws IOException {
|
||||
reader.beginObject();
|
||||
|
||||
ArrayList<Item> items = new ArrayList<>();
|
||||
|
||||
while (reader.hasNext()) {
|
||||
String tag = reader.nextName();
|
||||
if (tag.equals("items")) {
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
items.add(parseItem(reader));
|
||||
}
|
||||
reader.endArray();
|
||||
} else {
|
||||
reader.skipValue();
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private Item parseItem(JsonReader reader) throws IOException {
|
||||
reader.beginObject();
|
||||
Item item = new Item();
|
||||
|
||||
while (reader.hasNext()) {
|
||||
String tag = reader.nextName();
|
||||
switch (tag) {
|
||||
case "id":
|
||||
item.setId(reader.nextInt());
|
||||
break;
|
||||
case "title":
|
||||
item.setName(reader.nextString());
|
||||
break;
|
||||
case "album":
|
||||
item.setAlbum(reader.nextString());
|
||||
break;
|
||||
case "artist":
|
||||
item.setArtist(reader.nextString());
|
||||
break;
|
||||
default:
|
||||
reader.skipValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject();
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
broadcast(Message.obtain(null, MSG_STARTED));
|
||||
|
||||
Parcelable[] configs = intent.getParcelableArrayExtra("configs");
|
||||
|
||||
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(Integer.parseInt(globalSettings.getString("threads", "2")));
|
||||
String address = globalSettings.getString("address", "127.0.0.1");
|
||||
File root = new File(Environment.getExternalStorageDirectory(), "library");
|
||||
ProgressUpdater updater = new ProgressUpdater(this);
|
||||
|
||||
if (root.exists() && !root.isDirectory()) {
|
||||
updater.showMessage("Library is no dictionary! Fix manually");
|
||||
return;
|
||||
}
|
||||
|
||||
root.mkdirs();
|
||||
|
||||
FileTracker tracker = new FileTracker(getApplicationContext());
|
||||
|
||||
try {
|
||||
updater.showMessage("Cleaning library");
|
||||
tracker.delete();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (root.exists() && root.list(NOMEDIA_FILTER).length != 0) {
|
||||
updater.showMessage("Library not empty! Clean in manually");
|
||||
return;
|
||||
}
|
||||
|
||||
for (Parcelable parcelable : configs) {
|
||||
Config config = (Config) parcelable;
|
||||
List<Item> items;
|
||||
try {
|
||||
updater.showMessage("Fetching music information for %s", config.getName());
|
||||
items = fetchRandom(address, config, getResources());
|
||||
} catch (IOException e) {
|
||||
Log.wtf("WTF", e);
|
||||
updater.showMessage("Remote not available");
|
||||
return;
|
||||
}
|
||||
|
||||
updater.showMessage("Mixing new music for %s!", config.getName());
|
||||
updater.setMaximumProgress(items.size());
|
||||
|
||||
CountDownLatch itemsLeftLatch = new CountDownLatch(items.size());
|
||||
|
||||
for (Item item : items) {
|
||||
try {
|
||||
executor.submit(new DownloadTask(new URL(address + "/item/" + item.getId() + "/file").toURI(), item.getArtist() + "/" + item.getAlbum() + "/" + item.getName() + ".mp3", tracker, updater, itemsLeftLatch));
|
||||
} catch (URISyntaxException e) {
|
||||
e.printStackTrace();
|
||||
} catch (MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
itemsLeftLatch.await();
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
updater.showMessage("Musik aktualisiert");
|
||||
|
||||
// Update last_updated info
|
||||
SharedPreferences preferences = getSharedPreferences("info", MODE_PRIVATE);
|
||||
preferences.edit().putLong("last_updated", System.currentTimeMillis()).apply();
|
||||
|
||||
// Poweramp support
|
||||
Intent poweramp = new Intent(PowerampAPI.Scanner.ACTION_SCAN_DIRS);
|
||||
poweramp.setPackage(PowerampAPI.PACKAGE_NAME);
|
||||
poweramp.putExtra(PowerampAPI.Scanner.EXTRA_FULL_RESCAN, true);
|
||||
startService(poweramp);
|
||||
|
||||
broadcast(Message.obtain(null, MSG_FINISHED));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler of incoming messages from clients.
|
||||
*/
|
||||
private class IncomingHandler extends Handler {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_REGISTER_CLIENT:
|
||||
mClients.add(msg.replyTo);
|
||||
break;
|
||||
case MSG_UNREGISTER_CLIENT:
|
||||
mClients.remove(msg.replyTo);
|
||||
break;
|
||||
case MSG_CANCEL:
|
||||
//todo
|
||||
break;
|
||||
default:
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcast(Message msg) {
|
||||
for (int i = mClients.size() - 1; i >= 0; i--) {
|
||||
try {
|
||||
mClients.get(i).send(msg);
|
||||
} catch (RemoteException e) {
|
||||
// The client is dead. Remove it from the list;
|
||||
// we are going through the list from back to front
|
||||
// so this is safe to do inside the loop.
|
||||
mClients.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mMessenger.getBinder();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package max.music_cyclon;
|
||||
package max.music_cyclon.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
|
@ -0,0 +1,70 @@
|
|||
package max.music_cyclon.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import max.music_cyclon.R;
|
||||
|
||||
|
||||
public class ProgressUpdater {
|
||||
|
||||
public static int NOTIFICATION_ID = new Random().nextInt();
|
||||
|
||||
private Context context;
|
||||
|
||||
private int maximum = 0;
|
||||
private final NotificationManagerCompat notificationManager;
|
||||
|
||||
private int downloadCount = 0;
|
||||
|
||||
public ProgressUpdater(Context context) {
|
||||
this.context = context;
|
||||
|
||||
this.notificationManager = NotificationManagerCompat.from(context);
|
||||
}
|
||||
|
||||
public void showMessage(String message, Object... args) {
|
||||
showMessage(String.format(message, args));
|
||||
}
|
||||
|
||||
public void showMessage(String message) {
|
||||
NotificationCompat.Builder builder = notificationBuilder();
|
||||
|
||||
builder.setContentTitle(message);
|
||||
builder.setContentText("");
|
||||
builder.setProgress(0, 0, false);
|
||||
updateNotification(builder);
|
||||
}
|
||||
|
||||
public synchronized void increment() {
|
||||
NotificationCompat.Builder builder = progressNotificationBuilder();
|
||||
downloadCount++;
|
||||
|
||||
builder.setContentTitle("Aktualisiere Musik");
|
||||
builder.setContentText(downloadCount + "/" + maximum);
|
||||
builder.setProgress(maximum, downloadCount, false);
|
||||
updateNotification(builder);
|
||||
}
|
||||
|
||||
public void setMaximumProgress(int maximum) {
|
||||
this.maximum = maximum;
|
||||
}
|
||||
|
||||
private void updateNotification(NotificationCompat.Builder builder) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder notificationBuilder() {
|
||||
return new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.mipmap.ic_launcher);
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder progressNotificationBuilder() {
|
||||
return notificationBuilder().setUsesChronometer(true)
|
||||
.setOngoing(true)
|
||||
.setProgress(0, 0, true);
|
||||
}
|
||||
}
|
|
@ -1,48 +1,60 @@
|
|||
package max.music_cyclon;
|
||||
package max.music_cyclon.service.db;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.zip.Adler32;
|
||||
|
||||
public class FileTracker {
|
||||
|
||||
private SharedPreferences.Editor editor;
|
||||
private SharedPreferences preferences;
|
||||
|
||||
@SuppressLint("CommitPrefEdits")
|
||||
public FileTracker(SharedPreferences preferences) {
|
||||
this.preferences = preferences;
|
||||
this.editor = preferences.edit();
|
||||
private final LibraryDBOpenHelper helper;
|
||||
|
||||
public FileTracker(Context context) {
|
||||
helper = new LibraryDBOpenHelper(context);
|
||||
}
|
||||
|
||||
public void track(File file, long checksum) {
|
||||
editor.putLong(file.getAbsolutePath(), checksum);
|
||||
SQLiteDatabase db = helper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("path", file.getAbsolutePath());
|
||||
values.put("checksum", checksum);
|
||||
|
||||
db.insert("library", null, values);
|
||||
db.close();
|
||||
}
|
||||
|
||||
public void delete() throws IOException {
|
||||
Map<String, ?> preferences = this.preferences.getAll();
|
||||
SQLiteDatabase db = helper.getReadableDatabase();
|
||||
|
||||
for (Map.Entry<String, ?> entry : preferences.entrySet()) {
|
||||
if (!(entry.getValue() instanceof Long)) {
|
||||
continue;
|
||||
}
|
||||
Cursor cursor = db.query("library", null, null, null, null, null, null);
|
||||
int pathIndex = cursor.getColumnIndex("path");
|
||||
int checksumIndex = cursor.getColumnIndex("checksum");
|
||||
|
||||
File file = new File(entry.getKey());
|
||||
while (cursor.moveToNext()) {
|
||||
long checksum = cursor.getLong(checksumIndex);
|
||||
String path = cursor.getString(pathIndex);
|
||||
|
||||
if (((Long) entry.getValue()) != checksum(file)) {
|
||||
|
||||
File file = new File(path);
|
||||
|
||||
if (checksum != checksum(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
removeFile(file);
|
||||
}
|
||||
|
||||
cursor.close();
|
||||
db.close();
|
||||
}
|
||||
|
||||
private long checksum(File file) throws IOException {
|
||||
|
@ -78,8 +90,4 @@ public class FileTracker {
|
|||
|
||||
removeFile(path.getParentFile());
|
||||
}
|
||||
|
||||
public void commit() {
|
||||
editor.apply();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package max.music_cyclon.service.db;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
|
||||
public class LibraryDBOpenHelper extends SQLiteOpenHelper {
|
||||
private static final String SQL_CREATE_ENTRIES =
|
||||
"CREATE TABLE library (path TEXT, checksum INTEGER)";
|
||||
|
||||
public static final int DATABASE_VERSION = 1;
|
||||
public static final String DATABASE_NAME = "database.db";
|
||||
|
||||
public LibraryDBOpenHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SQL_CREATE_ENTRIES);
|
||||
}
|
||||
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
}
|
||||
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,324 @@
|
|||
/*
|
||||
* Copyright 2014 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package max.music_cyclon.slidingtab;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.support.v4.view.PagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.SparseArray;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* To be used with ViewPager to provide a tab indicator component which give constant feedback as to
|
||||
* the user's scroll progress.
|
||||
* <p>
|
||||
* To use the component, simply add it to your view hierarchy. Then in your
|
||||
* {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call
|
||||
* {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for.
|
||||
* <p>
|
||||
* The colors can be customized in two ways. The first and simplest is to provide an array of colors
|
||||
* via {@link #setSelectedIndicatorColors(int...)}. The
|
||||
* alternative is via the {@link TabColorizer} interface which provides you complete control over
|
||||
* which color is used for any individual position.
|
||||
* <p>
|
||||
* The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)},
|
||||
* providing the layout ID of your custom layout.
|
||||
*/
|
||||
public class SlidingTabLayout extends HorizontalScrollView {
|
||||
/**
|
||||
* Allows complete control over the colors drawn in the tab layout. Set with
|
||||
* {@link #setCustomTabColorizer(TabColorizer)}.
|
||||
*/
|
||||
public interface TabColorizer {
|
||||
|
||||
/**
|
||||
* @return return the color of the indicator used when {@code position} is selected.
|
||||
*/
|
||||
int getIndicatorColor(int position);
|
||||
|
||||
}
|
||||
|
||||
private static final int TITLE_OFFSET_DIPS = 24;
|
||||
private static final int TAB_VIEW_PADDING_DIPS = 16;
|
||||
private static final int TAB_VIEW_TEXT_SIZE_SP = 12;
|
||||
|
||||
private int mTitleOffset;
|
||||
|
||||
private int mTabViewLayoutId;
|
||||
private int mTabViewTextViewId;
|
||||
private boolean mDistributeEvenly;
|
||||
|
||||
private ViewPager mViewPager;
|
||||
private SparseArray<String> mContentDescriptions = new SparseArray<String>();
|
||||
private ViewPager.OnPageChangeListener mViewPagerPageChangeListener;
|
||||
|
||||
private final SlidingTabStrip mTabStrip;
|
||||
|
||||
public SlidingTabLayout(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SlidingTabLayout(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
// Disable the Scroll Bar
|
||||
setHorizontalScrollBarEnabled(false);
|
||||
// Make sure that the Tab Strips fills this View
|
||||
setFillViewport(true);
|
||||
|
||||
mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density);
|
||||
|
||||
mTabStrip = new SlidingTabStrip(context);
|
||||
addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom {@link TabColorizer} to be used.
|
||||
*
|
||||
* If you only require simple custmisation then you can use
|
||||
* {@link #setSelectedIndicatorColors(int...)} to achieve
|
||||
* similar effects.
|
||||
*/
|
||||
public void setCustomTabColorizer(TabColorizer tabColorizer) {
|
||||
mTabStrip.setCustomTabColorizer(tabColorizer);
|
||||
}
|
||||
|
||||
public void setDistributeEvenly(boolean distributeEvenly) {
|
||||
mDistributeEvenly = distributeEvenly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the colors to be used for indicating the selected tab. These colors are treated as a
|
||||
* circular array. Providing one color will mean that all tabs are indicated with the same color.
|
||||
*/
|
||||
public void setSelectedIndicatorColors(int... colors) {
|
||||
mTabStrip.setSelectedIndicatorColors(colors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are
|
||||
* required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so
|
||||
* that the layout can update it's scroll position correctly.
|
||||
*
|
||||
* @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener)
|
||||
*/
|
||||
public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
|
||||
mViewPagerPageChangeListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom layout to be inflated for the tab views.
|
||||
*
|
||||
* @param layoutResId Layout id to be inflated
|
||||
* @param textViewId id of the {@link TextView} in the inflated view
|
||||
*/
|
||||
public void setCustomTabView(int layoutResId, int textViewId) {
|
||||
mTabViewLayoutId = layoutResId;
|
||||
mTabViewTextViewId = textViewId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the associated view pager. Note that the assumption here is that the pager content
|
||||
* (number of tabs and tab titles) does not change after this call has been made.
|
||||
*/
|
||||
public void setViewPager(ViewPager viewPager) {
|
||||
mTabStrip.removeAllViews();
|
||||
|
||||
mViewPager = viewPager;
|
||||
if (viewPager != null) {
|
||||
viewPager.setOnPageChangeListener(new InternalViewPagerListener());
|
||||
populateTabStrip();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default view to be used for tabs. This is called if a custom tab view is not set via
|
||||
* {@link #setCustomTabView(int, int)}.
|
||||
*/
|
||||
protected TextView createDefaultTabView(Context context) {
|
||||
TextView textView = new TextView(context);
|
||||
textView.setGravity(Gravity.CENTER);
|
||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP);
|
||||
textView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
textView.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
TypedValue outValue = new TypedValue();
|
||||
getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
|
||||
outValue, true);
|
||||
textView.setBackgroundResource(outValue.resourceId);
|
||||
textView.setAllCaps(true);
|
||||
|
||||
int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density);
|
||||
textView.setPadding(padding, padding, padding, padding);
|
||||
|
||||
return textView;
|
||||
}
|
||||
|
||||
private void populateTabStrip() {
|
||||
final PagerAdapter adapter = mViewPager.getAdapter();
|
||||
final View.OnClickListener tabClickListener = new TabClickListener();
|
||||
|
||||
for (int i = 0; i < adapter.getCount(); i++) {
|
||||
View tabView = null;
|
||||
TextView tabTitleView = null;
|
||||
|
||||
if (mTabViewLayoutId != 0) {
|
||||
// If there is a custom tab view layout id set, try and inflate it
|
||||
tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip,
|
||||
false);
|
||||
tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId);
|
||||
}
|
||||
|
||||
if (tabView == null) {
|
||||
tabView = createDefaultTabView(getContext());
|
||||
}
|
||||
|
||||
if (tabTitleView == null && TextView.class.isInstance(tabView)) {
|
||||
tabTitleView = (TextView) tabView;
|
||||
}
|
||||
|
||||
if (mDistributeEvenly) {
|
||||
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) tabView.getLayoutParams();
|
||||
lp.width = 0;
|
||||
lp.weight = 1;
|
||||
}
|
||||
|
||||
tabTitleView.setText(adapter.getPageTitle(i));
|
||||
tabView.setOnClickListener(tabClickListener);
|
||||
String desc = mContentDescriptions.get(i, null);
|
||||
if (desc != null) {
|
||||
tabView.setContentDescription(desc);
|
||||
}
|
||||
|
||||
mTabStrip.addView(tabView);
|
||||
if (i == mViewPager.getCurrentItem()) {
|
||||
tabView.setSelected(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setContentDescription(int i, String desc) {
|
||||
mContentDescriptions.put(i, desc);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (mViewPager != null) {
|
||||
scrollToTab(mViewPager.getCurrentItem(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void scrollToTab(int tabIndex, int positionOffset) {
|
||||
final int tabStripChildCount = mTabStrip.getChildCount();
|
||||
if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
View selectedChild = mTabStrip.getChildAt(tabIndex);
|
||||
if (selectedChild != null) {
|
||||
int targetScrollX = selectedChild.getLeft() + positionOffset;
|
||||
|
||||
if (tabIndex > 0 || positionOffset > 0) {
|
||||
// If we're not at the first child and are mid-scroll, make sure we obey the offset
|
||||
targetScrollX -= mTitleOffset;
|
||||
}
|
||||
|
||||
scrollTo(targetScrollX, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private class InternalViewPagerListener implements ViewPager.OnPageChangeListener {
|
||||
private int mScrollState;
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
int tabStripChildCount = mTabStrip.getChildCount();
|
||||
if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mTabStrip.onViewPagerPageChanged(position, positionOffset);
|
||||
|
||||
View selectedTitle = mTabStrip.getChildAt(position);
|
||||
int extraOffset = (selectedTitle != null)
|
||||
? (int) (positionOffset * selectedTitle.getWidth())
|
||||
: 0;
|
||||
scrollToTab(position, extraOffset);
|
||||
|
||||
if (mViewPagerPageChangeListener != null) {
|
||||
mViewPagerPageChangeListener.onPageScrolled(position, positionOffset,
|
||||
positionOffsetPixels);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
mScrollState = state;
|
||||
|
||||
if (mViewPagerPageChangeListener != null) {
|
||||
mViewPagerPageChangeListener.onPageScrollStateChanged(state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
|
||||
mTabStrip.onViewPagerPageChanged(position, 0f);
|
||||
scrollToTab(position, 0);
|
||||
}
|
||||
for (int i = 0; i < mTabStrip.getChildCount(); i++) {
|
||||
mTabStrip.getChildAt(i).setSelected(position == i);
|
||||
}
|
||||
if (mViewPagerPageChangeListener != null) {
|
||||
mViewPagerPageChangeListener.onPageSelected(position);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class TabClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
for (int i = 0; i < mTabStrip.getChildCount(); i++) {
|
||||
if (v == mTabStrip.getChildAt(i)) {
|
||||
if (mViewPager.getChildAt(i) == null) {
|
||||
continue;
|
||||
}
|
||||
mViewPager.setCurrentItem(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Copyright 2014 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package max.music_cyclon.slidingtab;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
class SlidingTabStrip extends LinearLayout {
|
||||
|
||||
private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 0;
|
||||
private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26;
|
||||
private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 3;
|
||||
private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5;
|
||||
|
||||
private final int mBottomBorderThickness;
|
||||
private final Paint mBottomBorderPaint;
|
||||
|
||||
private final int mSelectedIndicatorThickness;
|
||||
private final Paint mSelectedIndicatorPaint;
|
||||
|
||||
private int mSelectedPosition;
|
||||
private float mSelectionOffset;
|
||||
|
||||
private SlidingTabLayout.TabColorizer mCustomTabColorizer;
|
||||
private final SimpleTabColorizer mDefaultTabColorizer;
|
||||
|
||||
SlidingTabStrip(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
SlidingTabStrip(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setWillNotDraw(false);
|
||||
|
||||
final float density = getResources().getDisplayMetrics().density;
|
||||
|
||||
TypedValue outValue = new TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.colorForeground, outValue, true);
|
||||
final int themeForegroundColor = outValue.data;
|
||||
|
||||
int defaultBottomBorderColor = setColorAlpha(themeForegroundColor,
|
||||
DEFAULT_BOTTOM_BORDER_COLOR_ALPHA);
|
||||
|
||||
mDefaultTabColorizer = new SimpleTabColorizer();
|
||||
mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR);
|
||||
|
||||
mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density);
|
||||
mBottomBorderPaint = new Paint();
|
||||
mBottomBorderPaint.setColor(defaultBottomBorderColor);
|
||||
|
||||
mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density);
|
||||
mSelectedIndicatorPaint = new Paint();
|
||||
}
|
||||
|
||||
void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) {
|
||||
mCustomTabColorizer = customTabColorizer;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void setSelectedIndicatorColors(int... colors) {
|
||||
// Make sure that the custom colorizer is removed
|
||||
mCustomTabColorizer = null;
|
||||
mDefaultTabColorizer.setIndicatorColors(colors);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void onViewPagerPageChanged(int position, float positionOffset) {
|
||||
mSelectedPosition = position;
|
||||
mSelectionOffset = positionOffset;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
final int height = getHeight();
|
||||
final int childCount = getChildCount();
|
||||
final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null
|
||||
? mCustomTabColorizer
|
||||
: mDefaultTabColorizer;
|
||||
|
||||
// Thick colored underline below the current selection
|
||||
if (childCount > 0) {
|
||||
View selectedTitle = getChildAt(mSelectedPosition);
|
||||
int left = selectedTitle.getLeft();
|
||||
int right = selectedTitle.getRight();
|
||||
int color = tabColorizer.getIndicatorColor(mSelectedPosition);
|
||||
|
||||
if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) {
|
||||
int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1);
|
||||
if (color != nextColor) {
|
||||
color = blendColors(nextColor, color, mSelectionOffset);
|
||||
}
|
||||
|
||||
// Draw the selection partway between the tabs
|
||||
View nextTitle = getChildAt(mSelectedPosition + 1);
|
||||
left = (int) (mSelectionOffset * nextTitle.getLeft() +
|
||||
(1.0f - mSelectionOffset) * left);
|
||||
right = (int) (mSelectionOffset * nextTitle.getRight() +
|
||||
(1.0f - mSelectionOffset) * right);
|
||||
}
|
||||
|
||||
mSelectedIndicatorPaint.setColor(color);
|
||||
|
||||
canvas.drawRect(left, height - mSelectedIndicatorThickness, right,
|
||||
height, mSelectedIndicatorPaint);
|
||||
}
|
||||
|
||||
// Thin underline along the entire bottom edge
|
||||
canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the alpha value of the {@code color} to be the given {@code alpha} value.
|
||||
*/
|
||||
private static int setColorAlpha(int color, byte alpha) {
|
||||
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Blend {@code color1} and {@code color2} using the given ratio.
|
||||
*
|
||||
* @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend,
|
||||
* 0.0 will return {@code color2}.
|
||||
*/
|
||||
private static int blendColors(int color1, int color2, float ratio) {
|
||||
final float inverseRation = 1f - ratio;
|
||||
float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation);
|
||||
float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation);
|
||||
float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation);
|
||||
return Color.rgb((int) r, (int) g, (int) b);
|
||||
}
|
||||
|
||||
private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer {
|
||||
private int[] mIndicatorColors;
|
||||
|
||||
@Override
|
||||
public final int getIndicatorColor(int position) {
|
||||
return mIndicatorColors[position % mIndicatorColors.length];
|
||||
}
|
||||
|
||||
void setIndicatorColors(int... colors) {
|
||||
mIndicatorColors = colors;
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 669 B |
Binary file not shown.
After Width: | Height: | Size: 467 B |
Binary file not shown.
Before Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 875 B |
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/preference_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:title="Settings"/>
|
||||
|
||||
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/viewer"
|
||||
android:name="max.music_cyclon.MainPreferenceActivity$MainPreferenceFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
</LinearLayout>
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/main_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/my_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary" />
|
||||
|
||||
<max.music_cyclon.slidingtab.SlidingTabLayout
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#FF9800" />
|
||||
|
||||
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".SynchronizeActivity" />
|
||||
</LinearLayout>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/add_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:clickable="true"
|
||||
android:src="@drawable/ic_add_white_48dp"
|
||||
app:layout_anchor="@id/container"
|
||||
app:layout_anchorGravity="bottom|end" />
|
||||
|
||||
</FrameLayout>
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_sync"
|
||||
android:icon="@drawable/ic_sync_white_24dp"
|
||||
android:title="@string/synchronize"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item android:id="@+id/action_settings"
|
||||
android:title="Settings"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item android:id="@+id/action_help"
|
||||
android:title="Hilfe"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item android:id="@+id/action_version"
|
||||
android:title="Version"
|
||||
app:showAsAction="never"/>
|
||||
</menu>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppTheme" parent="android:Theme.Material.Light">
|
||||
</style>
|
||||
</resources>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="accentColor">#607D8B</color>
|
||||
</resources>
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="size">10</integer>
|
||||
<bool name="random">true</bool>
|
||||
<bool name="use_albums">true</bool>
|
||||
<string name="query"/>
|
||||
<bool name="start_charging">false</bool>
|
||||
<integer name="download_interval">7</integer>
|
||||
|
||||
|
||||
</resources>
|
|
@ -1,3 +1,5 @@
|
|||
<resources>
|
||||
<string name="app_name">music-cyclon</string>
|
||||
<string name="title_activity_synchronize">Synchronizing</string>
|
||||
<string name="synchronize">Synchronize</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- inherit from the material theme -->
|
||||
<style name="AppTheme" parent="@style/PreferenceFixTheme.Light.NoActionBar">
|
||||
<!-- Main theme colors -->
|
||||
<!-- your app branding color for the app bar -->
|
||||
<item name="colorPrimary">#FF9800</item>
|
||||
<!-- darker variant for the status bar and contextual app bars -->
|
||||
<item name="colorPrimaryDark">#F57C00</item>
|
||||
|
||||
|
||||
<item name="android:textColorPrimary">#212121</item>
|
||||
<item name="android:textColorSecondary">#727272</item>
|
||||
<item name="android:divider">#B6B6B6</item>
|
||||
<item name="android:textColor">#212121</item>
|
||||
|
||||
<!-- theme UI controls like checkboxes and text fields -->
|
||||
<item name="colorAccent">@color/accentColor</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -1,8 +0,0 @@
|
|||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -1,12 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<Preference
|
||||
android:key="start"
|
||||
android:summary="Starts the download"
|
||||
android:title="Start Service" />
|
||||
|
||||
|
||||
<EditTextPreference
|
||||
android:defaultValue="http://localhost:5785"
|
||||
android:inputType="text"
|
||||
|
@ -21,29 +15,4 @@
|
|||
android:summary="Number of threads to use for downloading"
|
||||
android:title="Threads" />
|
||||
|
||||
<EditTextPreference
|
||||
android:defaultValue="1000"
|
||||
android:inputType="number"
|
||||
android:key="download"
|
||||
android:summary="Approximately amount in MB to download"
|
||||
android:title="Download size" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="start_charging"
|
||||
android:summary="This option if selected will allow to start the download if connected with a ac charger"
|
||||
android:title="Charging starts download" />
|
||||
|
||||
<EditTextPreference
|
||||
android:defaultValue="7"
|
||||
android:inputType="number"
|
||||
android:key="min_download_interval"
|
||||
android:summary="Minimum download interval in days"
|
||||
android:title="Download interval" />
|
||||
|
||||
|
||||
<max.music_cyclon.InfoPreference
|
||||
android:summary="0.1"
|
||||
android:title="Application info" />
|
||||
|
||||
</PreferenceScreen>
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen android:persistent="false" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
|
||||
<EditTextPreference
|
||||
android:persistent="false"
|
||||
android:defaultValue="@integer/size"
|
||||
android:inputType="number"
|
||||
android:key="size"
|
||||
android:summary="Approximately amount to download"
|
||||
android:title="Download size" />
|
||||
|
||||
<!--<SwitchPreference-->
|
||||
<!--android:persistent="false"-->
|
||||
<!--android:defaultValue="@bool/random"-->
|
||||
<!--android:key="random"-->
|
||||
<!--android:summary=""-->
|
||||
<!--android:title="Select title/albums randomly" />-->
|
||||
|
||||
<SwitchPreference
|
||||
android:persistent="false"
|
||||
android:defaultValue="@bool/use_albums"
|
||||
android:key="use_albums"
|
||||
android:summary=""
|
||||
android:title="Download only complete albums" />
|
||||
|
||||
<EditTextPreference
|
||||
android:persistent="false"
|
||||
android:defaultValue="@string/query"
|
||||
android:inputType="text"
|
||||
android:key="query"
|
||||
android:summary=""
|
||||
android:title="Query string" />
|
||||
|
||||
<SwitchPreference
|
||||
android:persistent="false"
|
||||
android:defaultValue="@bool/start_charging"
|
||||
android:key="start_charging"
|
||||
android:summary="This option if selected will allow to start the download if connected with a ac charger"
|
||||
android:title="Charging starts download" />
|
||||
|
||||
<EditTextPreference
|
||||
android:persistent="false"
|
||||
android:defaultValue="@integer/download_interval"
|
||||
android:inputType="number"
|
||||
android:key="download_interval"
|
||||
android:summary="Download interval in days"
|
||||
android:title="Download interval" />
|
||||
|
||||
<Preference
|
||||
android:persistent="false"
|
||||
android:key="remove"
|
||||
android:summary="Remove this config"
|
||||
android:title="Remove" />
|
||||
</PreferenceScreen>
|
|
@ -5,7 +5,7 @@ buildscript {
|
|||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.2.3'
|
||||
classpath 'com.android.tools.build:gradle:2.1.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#Wed Apr 10 15:27:10 PDT 2013
|
||||
#Wed Jun 01 15:28:51 CEST 2016
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
|
||||
|
|
Loading…
Reference in New Issue