diff --git a/app/build.gradle b/app/build.gradle index 42b5482..f49b3b2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7cb28f9..0f4856b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,35 +1,36 @@ + package="max.music_cyclon"> + + android:theme="@style/AppTheme"> + + + + + + + + + android:name=".SynchronizeActivity" + android:label="@string/title_activity_synchronize"> - - - - - - - - - + - + \ No newline at end of file diff --git a/app/src/main/java/com/maxmpz/poweramp/player/PowerampAPI.java b/app/src/main/java/com/maxmpz/poweramp/player/PowerampAPI.java new file mode 100644 index 0000000..f100ad7 --- /dev/null +++ b/app/src/main/java/com/maxmpz/poweramp/player/PowerampAPI.java @@ -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"; + } + +} \ No newline at end of file diff --git a/app/src/main/java/max/music_cyclon/Config.java b/app/src/main/java/max/music_cyclon/Config.java new file mode 100644 index 0000000..1348048 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/Config.java @@ -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 CREATOR = new Parcelable.Creator() { + 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 load(InputStream in) throws JSONException { + String data = convertStreamToString(in); + return load(data); + } + + public static List load(String data) throws JSONException { + JSONObject jsonConfigs = new JSONObject(data); + + ArrayList 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 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() : ""; + } + +} diff --git a/app/src/main/java/max/music_cyclon/InfoPreference.java b/app/src/main/java/max/music_cyclon/InfoPreference.java deleted file mode 100644 index 34f9288..0000000 --- a/app/src/main/java/max/music_cyclon/InfoPreference.java +++ /dev/null @@ -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"); - } - -} diff --git a/app/src/main/java/max/music_cyclon/LibraryService.java b/app/src/main/java/max/music_cyclon/LibraryService.java deleted file mode 100644 index ebb8e2e..0000000 --- a/app/src/main/java/max/music_cyclon/LibraryService.java +++ /dev/null @@ -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 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 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 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()); - } -} diff --git a/app/src/main/java/max/music_cyclon/MainPreferenceActivity.java b/app/src/main/java/max/music_cyclon/MainPreferenceActivity.java new file mode 100644 index 0000000..78c3c18 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/MainPreferenceActivity.java @@ -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) { + + } + } +} diff --git a/app/src/main/java/max/music_cyclon/PagerAdapter.java b/app/src/main/java/max/music_cyclon/PagerAdapter.java new file mode 100644 index 0000000..9c95323 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/PagerAdapter.java @@ -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 configs = new ArrayList<>(); + + private final Map configData = new HashMap<>(); + + public PagerAdapter(List 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 getConfigs() { + return configs; + } + + public List getConfigData() { + return new ArrayList<>(configData.values()); + } + + @Override + public CharSequence getPageTitle(int position) { + return getConfigs().get(position); + } +} diff --git a/app/src/main/java/max/music_cyclon/ProcessTask.java b/app/src/main/java/max/music_cyclon/ProcessTask.java deleted file mode 100644 index 9fff230..0000000 --- a/app/src/main/java/max/music_cyclon/ProcessTask.java +++ /dev/null @@ -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(); - } -} diff --git a/app/src/main/java/max/music_cyclon/SettingsActivity.java b/app/src/main/java/max/music_cyclon/SettingsActivity.java deleted file mode 100644 index d8c2bcc..0000000 --- a/app/src/main/java/max/music_cyclon/SettingsActivity.java +++ /dev/null @@ -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; - } - }); - - } - } - -} diff --git a/app/src/main/java/max/music_cyclon/SynchronizeActivity.java b/app/src/main/java/max/music_cyclon/SynchronizeActivity.java new file mode 100644 index 0000000..d196069 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/SynchronizeActivity.java @@ -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 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 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; + } + } +} diff --git a/app/src/main/java/max/music_cyclon/SynchronizeConfigFragment.java b/app/src/main/java/max/music_cyclon/SynchronizeConfigFragment.java new file mode 100644 index 0000000..609734c --- /dev/null +++ b/app/src/main/java/max/music_cyclon/SynchronizeConfigFragment.java @@ -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; + } + } +} diff --git a/app/src/main/java/max/music_cyclon/service/DownloadTask.java b/app/src/main/java/max/music_cyclon/service/DownloadTask.java new file mode 100644 index 0000000..f999e83 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/service/DownloadTask.java @@ -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(); + } +} diff --git a/app/src/main/java/max/music_cyclon/service/Item.java b/app/src/main/java/max/music_cyclon/service/Item.java new file mode 100644 index 0000000..f408955 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/service/Item.java @@ -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; + } +} diff --git a/app/src/main/java/max/music_cyclon/service/LibraryService.java b/app/src/main/java/max/music_cyclon/service/LibraryService.java new file mode 100644 index 0000000..bf67985 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/service/LibraryService.java @@ -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 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 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 items = parseJson(stream, config.getSize(resources)); + stream.close(); + + return items; + } + + private ArrayList parseJson(InputStream stream, int size) throws IOException { + JsonReader reader = new JsonReader(new BufferedReader(new InputStreamReader(stream, "UTF-8"))); + + ArrayList items = new ArrayList<>(); + ArrayList> 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> randomAlbums = selectRandom(albums, size); + + for (ArrayList album : randomAlbums) { + items.addAll(album); + } + + return items; + } + + public ArrayList selectRandom(ArrayList list, int n) { + ArrayList out = new ArrayList<>(); + for (int i = 0; i < n; i++) { + out.add(list.get(RANDOM.nextInt(list.size() - 1))); + } + + return out; + } + + private ArrayList parseAlbum(JsonReader reader) throws IOException { + reader.beginObject(); + + ArrayList 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 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(); + } +} diff --git a/app/src/main/java/max/music_cyclon/PowerConnectionReceiver.java b/app/src/main/java/max/music_cyclon/service/PowerConnectionReceiver.java similarity index 98% rename from app/src/main/java/max/music_cyclon/PowerConnectionReceiver.java rename to app/src/main/java/max/music_cyclon/service/PowerConnectionReceiver.java index cec2421..d1647da 100644 --- a/app/src/main/java/max/music_cyclon/PowerConnectionReceiver.java +++ b/app/src/main/java/max/music_cyclon/service/PowerConnectionReceiver.java @@ -1,4 +1,4 @@ -package max.music_cyclon; +package max.music_cyclon.service; import android.content.BroadcastReceiver; import android.content.Context; diff --git a/app/src/main/java/max/music_cyclon/service/ProgressUpdater.java b/app/src/main/java/max/music_cyclon/service/ProgressUpdater.java new file mode 100644 index 0000000..74579e0 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/service/ProgressUpdater.java @@ -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); + } +} diff --git a/app/src/main/java/max/music_cyclon/FileTracker.java b/app/src/main/java/max/music_cyclon/service/db/FileTracker.java similarity index 51% rename from app/src/main/java/max/music_cyclon/FileTracker.java rename to app/src/main/java/max/music_cyclon/service/db/FileTracker.java index 9acca9b..16f427c 100644 --- a/app/src/main/java/max/music_cyclon/FileTracker.java +++ b/app/src/main/java/max/music_cyclon/service/db/FileTracker.java @@ -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 preferences = this.preferences.getAll(); + SQLiteDatabase db = helper.getReadableDatabase(); - for (Map.Entry 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(); - } } diff --git a/app/src/main/java/max/music_cyclon/service/db/LibraryDBOpenHelper.java b/app/src/main/java/max/music_cyclon/service/db/LibraryDBOpenHelper.java new file mode 100644 index 0000000..bd3aaf0 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/service/db/LibraryDBOpenHelper.java @@ -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) { + } +} \ No newline at end of file diff --git a/app/src/main/java/max/music_cyclon/slidingtab/SlidingTabLayout.java b/app/src/main/java/max/music_cyclon/slidingtab/SlidingTabLayout.java new file mode 100644 index 0000000..5e12996 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/slidingtab/SlidingTabLayout.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * 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 mContentDescriptions = new SparseArray(); + 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; + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/max/music_cyclon/slidingtab/SlidingTabStrip.java b/app/src/main/java/max/music_cyclon/slidingtab/SlidingTabStrip.java new file mode 100644 index 0000000..0683c9c --- /dev/null +++ b/app/src/main/java/max/music_cyclon/slidingtab/SlidingTabStrip.java @@ -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; + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_sync_black_48dp.png b/app/src/main/res/drawable-hdpi/ic_sync_black_48dp.png new file mode 100644 index 0000000..f799008 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_sync_black_48dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_sync_black_48dp.png b/app/src/main/res/drawable-mdpi/ic_sync_black_48dp.png new file mode 100644 index 0000000..860a5db Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_sync_black_48dp.png differ diff --git a/app/src/main/res/drawable-nodpi/example_appwidget_preview.png b/app/src/main/res/drawable-nodpi/example_appwidget_preview.png deleted file mode 100644 index 894b069..0000000 Binary files a/app/src/main/res/drawable-nodpi/example_appwidget_preview.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_sync_black_48dp.png b/app/src/main/res/drawable-xhdpi/ic_sync_black_48dp.png new file mode 100644 index 0000000..b9f56f3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_sync_black_48dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_sync_black_48dp.png b/app/src/main/res/drawable-xxhdpi/ic_sync_black_48dp.png new file mode 100644 index 0000000..5c431e0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_sync_black_48dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_sync_black_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_sync_black_48dp.png new file mode 100644 index 0000000..2c007ea Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_sync_black_48dp.png differ diff --git a/app/src/main/res/layout/activity_main_preference.xml b/app/src/main/res/layout/activity_main_preference.xml new file mode 100644 index 0000000..cd42929 --- /dev/null +++ b/app/src/main/res/layout/activity_main_preference.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_synchronize.xml b/app/src/main/res/layout/activity_synchronize.xml new file mode 100644 index 0000000..992d23f --- /dev/null +++ b/app/src/main/res/layout/activity_synchronize.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml new file mode 100644 index 0000000..034fa4c --- /dev/null +++ b/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,21 @@ + +

+ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml deleted file mode 100644 index dba3c41..0000000 --- a/app/src/main/res/values-v21/styles.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..4f448ef --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #607D8B + \ No newline at end of file diff --git a/app/src/main/res/values/default_config.xml b/app/src/main/res/values/default_config.xml new file mode 100644 index 0000000..1a15eea --- /dev/null +++ b/app/src/main/res/values/default_config.xml @@ -0,0 +1,11 @@ + + + 10 + true + true + + false + 7 + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 415b170..40aad04 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ music-cyclon + Synchronizing + Synchronize diff --git a/app/src/main/res/values/style.xml b/app/src/main/res/values/style.xml new file mode 100644 index 0000000..29bfbdb --- /dev/null +++ b/app/src/main/res/values/style.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml deleted file mode 100644 index ff6c9d2..0000000 --- a/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index fac2a9a..fe872e1 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -1,12 +1,6 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/sync_config.xml b/app/src/main/res/xml/sync_config.xml new file mode 100644 index 0000000..cc761e4 --- /dev/null +++ b/app/src/main/res/xml/sync_config.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 9405f3f..e220f0b 100644 --- a/build.gradle +++ b/build.gradle @@ -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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c71e76..6553df6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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