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'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 22
|
compileSdkVersion 23
|
||||||
buildToolsVersion "22.0.1"
|
buildToolsVersion "22.0.1"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "max.music_cyclon"
|
applicationId "max.music_cyclon"
|
||||||
minSdkVersion 21
|
minSdkVersion 14
|
||||||
targetSdkVersion 22
|
targetSdkVersion 23
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
}
|
}
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_7
|
sourceCompatibility JavaVersion.VERSION_1_7
|
||||||
targetCompatibility JavaVersion.VERSION_1_7
|
targetCompatibility JavaVersion.VERSION_1_7
|
||||||
|
@ -24,8 +18,13 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile fileTree(include: ['*.jar'], dir: 'libs')
|
compile "cz.msebera.android:httpclient:4.4.1.2"
|
||||||
compile 'com.android.support:appcompat-v7:22.0.0'
|
|
||||||
compile 'commons-io:commons-io:2.4'
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
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
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SynchronizeActivity"
|
||||||
android:label="Music Cyclon" >
|
android:label="@string/title_activity_synchronize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name=".MainPreferenceActivity"></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>
|
|
||||||
|
|
||||||
</application>
|
</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.BroadcastReceiver;
|
||||||
import android.content.Context;
|
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.ContentValues;
|
||||||
import android.content.SharedPreferences;
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.zip.Adler32;
|
import java.util.zip.Adler32;
|
||||||
|
|
||||||
public class FileTracker {
|
public class FileTracker {
|
||||||
|
|
||||||
private SharedPreferences.Editor editor;
|
|
||||||
private SharedPreferences preferences;
|
|
||||||
|
|
||||||
@SuppressLint("CommitPrefEdits")
|
private final LibraryDBOpenHelper helper;
|
||||||
public FileTracker(SharedPreferences preferences) {
|
|
||||||
this.preferences = preferences;
|
public FileTracker(Context context) {
|
||||||
this.editor = preferences.edit();
|
helper = new LibraryDBOpenHelper(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void track(File file, long checksum) {
|
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 {
|
public void delete() throws IOException {
|
||||||
Map<String, ?> preferences = this.preferences.getAll();
|
SQLiteDatabase db = helper.getReadableDatabase();
|
||||||
|
|
||||||
for (Map.Entry<String, ?> entry : preferences.entrySet()) {
|
Cursor cursor = db.query("library", null, null, null, null, null, null);
|
||||||
if (!(entry.getValue() instanceof Long)) {
|
int pathIndex = cursor.getColumnIndex("path");
|
||||||
continue;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFile(file);
|
removeFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cursor.close();
|
||||||
|
db.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private long checksum(File file) throws IOException {
|
private long checksum(File file) throws IOException {
|
||||||
|
@ -78,8 +90,4 @@ public class FileTracker {
|
||||||
|
|
||||||
removeFile(path.getParentFile());
|
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>
|
<resources>
|
||||||
<string name="app_name">music-cyclon</string>
|
<string name="app_name">music-cyclon</string>
|
||||||
|
<string name="title_activity_synchronize">Synchronizing</string>
|
||||||
|
<string name="synchronize">Synchronize</string>
|
||||||
</resources>
|
</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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:key="start"
|
|
||||||
android:summary="Starts the download"
|
|
||||||
android:title="Start Service" />
|
|
||||||
|
|
||||||
|
|
||||||
<EditTextPreference
|
<EditTextPreference
|
||||||
android:defaultValue="http://localhost:5785"
|
android:defaultValue="http://localhost:5785"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
|
@ -21,29 +15,4 @@
|
||||||
android:summary="Number of threads to use for downloading"
|
android:summary="Number of threads to use for downloading"
|
||||||
android:title="Threads" />
|
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>
|
</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()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
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
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// 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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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