Rewrote music-cyclon to integrate it with beets library, very alpha and not optimized and tested!

master
Max Ammann 2016-06-13 19:51:17 +02:00
parent 7a2cc7bee4
commit 5e5862e059
40 changed files with 2835 additions and 417 deletions

View File

@ -1,22 +1,16 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
compileSdkVersion 23
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "max.music_cyclon"
minSdkVersion 21
targetSdkVersion 22
minSdkVersion 14
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
@ -24,8 +18,13 @@ android {
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:22.0.0'
compile "cz.msebera.android:httpclient:4.4.1.2"
compile 'commons-io:commons-io:2.4'
compile group: 'org.apache.httpcomponents' , name: 'httpclient-android' , version: '4.3.5.1'
// compile 'com.android.support:appcompat-v7:23.4.0'
// compile 'com.android.support:preference-v14:23.4.0'
compile 'com.takisoft.fix:preference-v7:23.4.0.4'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
}

View File

@ -1,35 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="max.music_cyclon" >
package="max.music_cyclon">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
android:theme="@style/AppTheme">
<service
android:name=".service.LibraryService"
android:enabled="true" />
<receiver android:name=".service.PowerConnectionReceiver">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
</intent-filter>
</receiver>
<activity
android:name=".SettingsActivity"
android:label="Music Cyclon" >
android:name=".SynchronizeActivity"
android:label="@string/title_activity_synchronize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".LibraryService"
android:enabled="true" />
<receiver android:name=".PowerConnectionReceiver">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
</intent-filter>
</receiver>
<activity android:name=".MainPreferenceActivity"></activity>
</application>
</manifest>
</manifest>

View File

@ -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";
}
}

View File

@ -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() : "";
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package max.music_cyclon;
package max.music_cyclon.service;
import android.content.BroadcastReceiver;
import android.content.Context;

View File

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

View File

@ -1,48 +1,60 @@
package max.music_cyclon;
package max.music_cyclon.service.db;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Map;
import java.util.zip.Adler32;
public class FileTracker {
private SharedPreferences.Editor editor;
private SharedPreferences preferences;
@SuppressLint("CommitPrefEdits")
public FileTracker(SharedPreferences preferences) {
this.preferences = preferences;
this.editor = preferences.edit();
private final LibraryDBOpenHelper helper;
public FileTracker(Context context) {
helper = new LibraryDBOpenHelper(context);
}
public void track(File file, long checksum) {
editor.putLong(file.getAbsolutePath(), checksum);
SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("path", file.getAbsolutePath());
values.put("checksum", checksum);
db.insert("library", null, values);
db.close();
}
public void delete() throws IOException {
Map<String, ?> preferences = this.preferences.getAll();
SQLiteDatabase db = helper.getReadableDatabase();
for (Map.Entry<String, ?> entry : preferences.entrySet()) {
if (!(entry.getValue() instanceof Long)) {
continue;
}
Cursor cursor = db.query("library", null, null, null, null, null, null);
int pathIndex = cursor.getColumnIndex("path");
int checksumIndex = cursor.getColumnIndex("checksum");
File file = new File(entry.getKey());
while (cursor.moveToNext()) {
long checksum = cursor.getLong(checksumIndex);
String path = cursor.getString(pathIndex);
if (((Long) entry.getValue()) != checksum(file)) {
File file = new File(path);
if (checksum != checksum(file)) {
continue;
}
removeFile(file);
}
cursor.close();
db.close();
}
private long checksum(File file) throws IOException {
@ -78,8 +90,4 @@ public class FileTracker {
removeFile(path.getParentFile());
}
public void commit() {
editor.apply();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="accentColor">#607D8B</color>
</resources>

View File

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

View File

@ -1,3 +1,5 @@
<resources>
<string name="app_name">music-cyclon</string>
<string name="title_activity_synchronize">Synchronizing</string>
<string name="synchronize">Synchronize</string>
</resources>

View File

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

View File

@ -1,8 +0,0 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:key="start"
android:summary="Starts the download"
android:title="Start Service" />
<EditTextPreference
android:defaultValue="http://localhost:5785"
android:inputType="text"
@ -21,29 +15,4 @@
android:summary="Number of threads to use for downloading"
android:title="Threads" />
<EditTextPreference
android:defaultValue="1000"
android:inputType="number"
android:key="download"
android:summary="Approximately amount in MB to download"
android:title="Download size" />
<SwitchPreference
android:defaultValue="false"
android:key="start_charging"
android:summary="This option if selected will allow to start the download if connected with a ac charger"
android:title="Charging starts download" />
<EditTextPreference
android:defaultValue="7"
android:inputType="number"
android:key="min_download_interval"
android:summary="Minimum download interval in days"
android:title="Download interval" />
<max.music_cyclon.InfoPreference
android:summary="0.1"
android:title="Application info" />
</PreferenceScreen>

View File

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

View File

@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.android.tools.build:gradle:2.1.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@ -1,6 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
#Wed Jun 01 15:28:51 CEST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip