diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..b0d8e67f --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 27 + buildToolsVersion "27.0.3" + + defaultConfig { + applicationId "com.mkulesh.onpc" + minSdkVersion 14 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + applicationVariants.all { variant -> + variant.outputs.each { output -> + def file = output.outputFile + output.outputFile = new File(file.parent, "onpc-" + defaultConfig.versionName + ".apk") + } + } + } + } + + lintOptions { + checkReleaseBuilds false + abortOnError false + disable "RtlHardcoded", "RtlSymmetry", "RtlEnabled" + } +} + +dependencies { + compile 'com.android.support:support-v4:27.1.0' + compile 'com.android.support:appcompat-v7:27.1.0' + compile 'com.android.support:design:27.1.0' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..acfb08fe --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/andrey/Android/Sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..5a16426c --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/mkulesh/onpc/BaseFragment.java b/app/src/main/java/com/mkulesh/onpc/BaseFragment.java new file mode 100644 index 00000000..8e17e0c8 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/BaseFragment.java @@ -0,0 +1,73 @@ +package com.mkulesh.onpc; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.widget.AppCompatImageButton; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.mkulesh.onpc.utils.Utils; + +abstract public class BaseFragment extends Fragment +{ + /** + * Constants used to save/restore the instance state. + */ + public static final String FRAGMENT_NUMBER = "fragment_number"; + public static final String SERVER_NAME = "server_name"; + public static final String SERVER_PORT = "server_port"; + + protected MainActivity activity; + protected SharedPreferences preferences; + protected View rootView = null; + protected int fragmentNumber = -1; + + public BaseFragment() + { + // Empty constructor required for fragment subclasses + } + + public void initializeFragment(LayoutInflater inflater, ViewGroup container, int layoutId) + { + activity = (MainActivity) getActivity(); + preferences = PreferenceManager.getDefaultSharedPreferences(activity); + rootView = inflater.inflate(layoutId, container, false); + Bundle args = getArguments(); + fragmentNumber = args != null ? args.getInt(FRAGMENT_NUMBER) : 0; + } + + public void update(final State state) + { + if (state == null || !state.isOn()) + { + updateStandbyView(state); + } + else + { + updateActiveView(state); + } + } + + protected abstract void updateStandbyView(@Nullable final State state); + + protected abstract void updateActiveView(@NonNull final State state); + + protected void setButtonEnabled(AppCompatImageButton b, boolean isEnabled) + { + b.setEnabled(isEnabled); + Utils.setImageButtonColorAttr(activity, b, + b.isEnabled() ? R.attr.colorButtonEnabled : R.attr.colorButtonDisabled); + } + + protected void setButtonSelected(AppCompatImageButton b, boolean isSelected) + { + b.setSelected(isSelected); + Utils.setImageButtonColorAttr(activity, b, + b.isSelected() ? R.attr.colorAccent : R.attr.colorButtonEnabled); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/DeviceFragment.java b/app/src/main/java/com/mkulesh/onpc/DeviceFragment.java new file mode 100644 index 00000000..5242929b --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/DeviceFragment.java @@ -0,0 +1,97 @@ +package com.mkulesh.onpc; + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +public class DeviceFragment extends BaseFragment implements View.OnClickListener +{ + private ImageView deviceCover = null; + + public DeviceFragment() + { + // Empty constructor required for fragment subclasses + } + + @SuppressLint("SetTextI18n") + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + initializeFragment(inflater, container, R.layout.device_fragment); + + final Button buttonServerConnect = rootView.findViewById(R.id.device_connect); + buttonServerConnect.setOnClickListener(this); + + ((EditText) rootView.findViewById(R.id.device_name)).setText(preferences.getString( + DeviceFragment.SERVER_NAME, "onkyo")); + ((EditText) rootView.findViewById(R.id.device_port)).setText(Integer.toString(preferences.getInt( + DeviceFragment.SERVER_PORT, 60128))); + + deviceCover = rootView.findViewById(R.id.device_cover); + + update(null); + return rootView; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.device_connect) + { + final String serverName = ((EditText) rootView.findViewById(R.id.device_name)).getText().toString(); + final String serverPortStr = ((EditText) rootView.findViewById(R.id.device_port)).getText() + .toString(); + final int serverPort = Integer.parseInt(serverPortStr); + if (activity.connectToServer(serverName, serverPort)) + { + SharedPreferences.Editor prefEditor = preferences.edit(); + prefEditor.putString(SERVER_NAME, serverName); + prefEditor.putInt(SERVER_PORT, serverPort); + prefEditor.commit(); + } + } + } + + @Override + protected void updateStandbyView(@Nullable final State state) + { + updateDeviceCover(state); + } + + @Override + protected void updateActiveView(@NonNull final State state) + { + updateDeviceCover(state); + + if (!state.deviceProperties.isEmpty()) + { + ((TextView) rootView.findViewById(R.id.device_brand)).setText(state.deviceProperties.get("brand")); + ((TextView) rootView.findViewById(R.id.device_model)).setText(state.deviceProperties.get("model")); + ((TextView) rootView.findViewById(R.id.device_year)).setText(state.deviceProperties.get("year")); + ((TextView) rootView.findViewById(R.id.device_firmware)).setText(state.deviceProperties.get("firmwareversion")); + } + } + + private void updateDeviceCover(@Nullable final State state) + { + if (state != null && state.deviceCover != null) + { + deviceCover.setVisibility(View.VISIBLE); + deviceCover.setImageBitmap(state.deviceCover); + } + else + { + deviceCover.setVisibility(View.GONE); + deviceCover.setImageBitmap(null); + } + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/MainActivity.java b/app/src/main/java/com/mkulesh/onpc/MainActivity.java new file mode 100644 index 00000000..7fc74dd7 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/MainActivity.java @@ -0,0 +1,356 @@ +package com.mkulesh.onpc; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v4.view.ViewPager.OnPageChangeListener; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.SparseArray; +import android.view.Menu; +import android.view.MenuItem; +import android.view.ViewGroup; +import android.widget.Toast; + +import com.mkulesh.onpc.iscp.MessageChannel; +import com.mkulesh.onpc.iscp.messages.PowerStatusMsg; +import com.mkulesh.onpc.utils.AppTheme; +import com.mkulesh.onpc.utils.Utils; + +import java.util.Locale; + +public class MainActivity extends AppCompatActivity implements OnPageChangeListener +{ + private final static boolean ENABLE_MOCKUP = true; + private static final int SETTINGS_ACTIVITY_REQID = 256; + public static final String EXIT_CONFIRM = "exit_confirm"; + + private Toolbar toolbar; + private SectionsPagerAdapter pagerAdapter; + private ViewPager viewPager; + private MessageChannel messageChannel = null; + private Menu mainMenu; + private StateManager stateManager = null; + private Toast exitToast = null; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + setTheme(AppTheme.getTheme(this, AppTheme.ThemeType.MAIN_THEME)); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + toolbar = findViewById(R.id.toolbar); + toolbar.setTitle(R.string.app_toolbar_title); + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) + { + getSupportActionBar().setTitle(R.string.app_toolbar_title); + getSupportActionBar().setElevation(5.0f); + } + + // Create the adapter that will return a fragment for each of the three + // primary sections of the activity. + pagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); + + // Set up the ViewPager with the sections adapter. + viewPager = findViewById(R.id.view_pager); + viewPager.setAdapter(pagerAdapter); + viewPager.addOnPageChangeListener(this); + + final TabLayout tabLayout = findViewById(R.id.tab_layout); + tabLayout.setupWithViewPager(viewPager); + updateToolbar(null); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + mainMenu = menu; + getMenuInflater().inflate(R.menu.activity_main_actions, menu); + for (int i = 0; i < mainMenu.size(); i++) + { + Utils.updateMenuIconColor(this, mainMenu.getItem(i)); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) + { + switch (menuItem.getItemId()) + { + case R.id.action_app_power: + if (getStateManager() != null) + { + if (getStateManager().getState().isOn()) + { + getStateManager().navigateTo(new PowerStatusMsg(PowerStatusMsg.PowerStatus.STB)); + } + else + { + getStateManager().navigateTo(new PowerStatusMsg(PowerStatusMsg.PowerStatus.ON)); + } + } + return true; + case R.id.action_app_settings: + { + Intent settings = new Intent(this, SettingsActivity.class); + startActivityForResult(settings, SETTINGS_ACTIVITY_REQID); + return true; + } + default: + return super.onOptionsItemSelected(menuItem); + } + } + + @Override + public void onBackPressed() + { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + if (!preferences.getBoolean(EXIT_CONFIRM, false)) + { + finish(); + } + else if (exitToast != null && exitToast.getView().isShown()) + { + exitToast.cancel(); + finish(); + } + else + { + exitToast = Toast.makeText(this, R.string.action_exit_confirm, Toast.LENGTH_LONG); + exitToast.show(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == SETTINGS_ACTIVITY_REQID) + { + restartActivity(); + } + } + + public void restartActivity() + { + Intent intent = getIntent(); + finish(); + startActivity(intent); + } + + private class SectionsPagerAdapter extends FragmentStatePagerAdapter + { + private SparseArray registeredFragments = new SparseArray<>(); + + SectionsPagerAdapter(FragmentManager fm) + { + super(fm); + } + + @Override + public Fragment getItem(int position) + { + Fragment fragment; + switch (position) + { + case 0: + fragment = new MonitorFragment(); + break; + case 1: + fragment = new MediaFragment(); + break; + default: + fragment = new DeviceFragment(); + break; + } + Bundle args = new Bundle(); + args.putInt(BaseFragment.FRAGMENT_NUMBER, position); + fragment.setArguments(args); + return fragment; + } + + @Override + public int getCount() + { + // Show 3 total pages. + return 3; + } + + @Override + public CharSequence getPageTitle(int position) + { + Locale l = Locale.getDefault(); + switch (position) + { + case 0: + return getString(R.string.title_monitor).toUpperCase(l); + case 1: + return getString(R.string.title_media).toUpperCase(l); + case 2: + return getString(R.string.title_device).toUpperCase(l); + } + return null; + } + + // Register the fragment when the item is instantiated + @NonNull + @Override + public Object instantiateItem(ViewGroup container, int position) + { + Fragment fragment = (Fragment) super.instantiateItem(container, position); + registeredFragments.put(position, fragment); + return fragment; + } + + // Unregister when the item is inactive + @Override + public void destroyItem(ViewGroup container, int position, Object object) + { + registeredFragments.remove(position); + super.destroyItem(container, position, object); + } + + // Returns the fragment for the position (if instantiated) + Fragment getRegisteredFragment(int position) + { + return registeredFragments.get(position); + } + } + + public boolean connectToServer(String server, int port) + { + stopThreads(); + messageChannel = new MessageChannel(this); + if (messageChannel.connectToServer(server, port)) + { + messageChannel.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + stateManager = new StateManager(this, messageChannel, false); + stateManager.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + return true; + } + else if (ENABLE_MOCKUP) + { + stateManager = new StateManager(this, messageChannel, true); + return true; + } + else + { + return false; + } + } + + private void stopThreads() + { + if (stateManager != null) + { + stateManager.stop(); + } + if (messageChannel != null) + { + messageChannel.stop(); + } + messageChannel = null; + stateManager = null; + } + + public StateManager getStateManager() + { + return stateManager; + } + + @Override + protected void onResume() + { + super.onResume(); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + final String serverName = preferences.getString(DeviceFragment.SERVER_NAME, ""); + final int serverPort = preferences.getInt(DeviceFragment.SERVER_PORT, 0); + if (!serverName.isEmpty() && serverPort > 0) + { + connectToServer(serverName, serverPort); + } + } + + @Override + protected void onPause() + { + super.onPause(); + stopThreads(); + } + + public void updateCurrentFragment(State state) + { + final BaseFragment f = (BaseFragment) (pagerAdapter.getRegisteredFragment(viewPager.getCurrentItem())); + if (f != null) + { + f.update(state); + } + updateToolbar(state); + } + + public void updateToolbar(State state) + { + // Logo + Drawable icon; + if (state == null) + { + icon = Utils.getDrawable(this, R.drawable.device_disconnect); + toolbar.setSubtitle(R.string.not_connected); + } + else + { + icon = Utils.getDrawable(this, R.drawable.device_connect); + toolbar.setSubtitle(state.deviceProperties.get("model")); + } + Utils.setDrawableColorAttr(this, icon, android.R.attr.textColorTertiary); + if (getSupportActionBar() != null) + { + getSupportActionBar().setLogo(icon); + } + // Main menu + if (mainMenu != null) + { + for (int i = 0; i < mainMenu.size(); i++) + { + final MenuItem m = mainMenu.getItem(i); + if (m.getItemId() == R.id.action_app_power) + { + m.setEnabled(state != null); + Utils.updateMenuIconColor(this, m); + } + } + + } + } + + @Override + public void onPageScrollStateChanged(int arg0) + { + // empty + + } + + @Override + public void onPageScrolled(int arg0, float arg1, int arg2) + { + // empty + } + + @Override + public void onPageSelected(int p) + { + updateCurrentFragment(stateManager == null ? null : stateManager.getState()); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/MediaFragment.java b/app/src/main/java/com/mkulesh/onpc/MediaFragment.java new file mode 100644 index 00000000..11cbd1bf --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/MediaFragment.java @@ -0,0 +1,405 @@ +package com.mkulesh.onpc; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.AppCompatImageButton; +import android.util.TypedValue; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; + +import com.mkulesh.onpc.iscp.ISCPMessage; +import com.mkulesh.onpc.iscp.messages.InputSelectorMsg; +import com.mkulesh.onpc.iscp.messages.ListTitleInfoMsg; +import com.mkulesh.onpc.iscp.messages.NetworkServiceMsg; +import com.mkulesh.onpc.iscp.messages.OperationCommandMsg; +import com.mkulesh.onpc.iscp.messages.PlayQueueAddMsg; +import com.mkulesh.onpc.iscp.messages.PlayQueueRemoveMsg; +import com.mkulesh.onpc.iscp.messages.PlayQueueReorderMsg; +import com.mkulesh.onpc.iscp.messages.ReceiverInformationMsg; +import com.mkulesh.onpc.iscp.messages.XmlListItemMsg; +import com.mkulesh.onpc.utils.Logging; +import com.mkulesh.onpc.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +public class MediaFragment extends BaseFragment implements AdapterView.OnItemClickListener +{ + private TextView titleBar; + private ListView listView; + private XmListItemMsgAdapter listViewAdapter; + private LinearLayout selectorPaletteLayout = null; + private XmlListItemMsg selectedItem = null; + private int moveFrom = -1; + + public MediaFragment() + { + // Empty constructor required for fragment subclasses + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + initializeFragment(inflater, container, R.layout.media_fragment); + rootView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + titleBar = rootView.findViewById(R.id.items_list_title_bar); + listView = rootView.findViewById(R.id.items_list_view); + listView.setItemsCanFocus(false); + listView.setFocusableInTouchMode(true); + listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + listView.setOnItemClickListener(this); + + registerForContextMenu(listView); + + update(null); + return rootView; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) + { + selectedItem = null; + if (v.getId() == listView.getId() && activity.getStateManager() != null) + { + final State state = activity.getStateManager().getState(); + final ReceiverInformationMsg.Selector selector = state.getActualSelector(); + if (selector != null) + { + Logging.info(this, "Context menu for selector " + selector.toString()); + ListView lv = (ListView) v; + AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo; + final Object item = lv.getItemAtPosition(acmi.position); + if (item instanceof XmlListItemMsg) + { + selectedItem = (XmlListItemMsg) item; + MenuInflater inflater = activity.getMenuInflater(); + inflater.inflate(R.menu.playlist_context_menu, menu); + menu.findItem(R.id.playlist_menu_add).setVisible(selector.isAddToQueue()); + menu.findItem(R.id.playlist_menu_add_and_play).setVisible(selector.isAddToQueue()); + + final boolean isQueue = state.serviceType == ListTitleInfoMsg.ServiceType.PLAYQUEUE; + menu.findItem(R.id.playlist_menu_remove).setVisible(isQueue); + menu.findItem(R.id.playlist_menu_remove_all).setVisible(isQueue); + menu.findItem(R.id.playlist_menu_move_from).setVisible(isQueue); + menu.findItem(R.id.playlist_menu_move_to).setVisible( + isQueue && isMoveToValid(selectedItem.getMessageId())); + } + } + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) + { + if (selectedItem != null && activity.getStateManager() != null) + { + final State state = activity.getStateManager().getState(); + final int idx = selectedItem.getMessageId(); + Logging.info(this, "Context menu: " + item.toString() + "; " + selectedItem.toString()); + selectedItem = null; + switch (item.getItemId()) + { + case R.id.playlist_menu_add: + activity.getStateManager().sendPlayQueueMsg(new PlayQueueAddMsg(idx, 1), false); + return true; + case R.id.playlist_menu_add_and_play: + activity.getStateManager().sendPlayQueueMsg(new PlayQueueAddMsg(idx, 0), false); + return true; + case R.id.playlist_menu_remove: + activity.getStateManager().sendPlayQueueMsg(new PlayQueueRemoveMsg(0, idx), false); + return true; + case R.id.playlist_menu_remove_all: + activity.getStateManager().sendPlayQueueMsg(new PlayQueueRemoveMsg(0, 0), true); + return true; + case R.id.playlist_menu_move_from: + moveFrom = idx; + updateListView(state.mediaItems, state.serviceItems); + return true; + case R.id.playlist_menu_move_to: + if (isMoveToValid(idx)) + { + activity.getStateManager().sendPlayQueueMsg(new PlayQueueReorderMsg(moveFrom, idx), false); + moveFrom = -1; + } + return true; + } + } + return super.onContextItemSelected(item); + } + + @Override + protected void updateStandbyView(@Nullable final State state) + { + moveFrom = -1; + if (selectorPaletteLayout != null) + { + selectorPaletteLayout.removeAllViews(); + selectorPaletteLayout = null; + } + titleBar.setText(""); + listView.clearChoices(); + listView.invalidate(); + listView.setAdapter(new XmListItemMsgAdapter(activity, new ArrayList())); + } + + @Override + protected void updateActiveView(@NonNull final State state) + { + if (selectorPaletteLayout == null) + { + addSelectorButtons(state); + } + updateSelectorButtons(state); + + if (state.itemsChanged) + { + moveFrom = -1; + updateTitle(state, state.numberOfItems > 0 && state.mediaItems == null && state.serviceItems == null); + updateListView(state.mediaItems, state.serviceItems); + state.itemsChanged = false; + } + } + + private void addSelectorButtons(@NonNull final State state) + { + if (state.deviceSelectors.isEmpty()) + { + return; + } + if (selectorPaletteLayout == null) + { + selectorPaletteLayout = rootView.findViewById(R.id.selector_palette); + } + selectorPaletteLayout.removeAllViews(); + + final int buttonSize = activity.getResources().getDimensionPixelSize(R.dimen.btn_size); + final int buttonMargin = activity.getResources().getDimensionPixelSize(R.dimen.btn_margin); + final int selNumber = state.deviceSelectors.size(); + for (int i = 0; i < selNumber; i++) + { + final ReceiverInformationMsg.Selector s = state.deviceSelectors.get(i); + final InputSelectorMsg msg = new InputSelectorMsg(s.getId()); + if (msg.getInputType() == InputSelectorMsg.InputType.NONE) + { + continue; + } + final AppCompatImageButton b = new AppCompatImageButton(activity); + final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(buttonSize, buttonSize); + lp.setMargins((i == 0 ? 0 : buttonMargin), buttonMargin, (i == selNumber - 1 ? 0 : buttonMargin), buttonMargin); + b.setLayoutParams(lp); + b.setTag(msg.getInputType()); + + TypedValue outValue = new TypedValue(); + activity.getTheme().resolveAttribute(R.attr.selectableItemBackground, outValue, true); + b.setBackgroundResource(outValue.resourceId); + + b.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + if (activity.getStateManager() != null) + { + activity.getStateManager().navigateTo(msg); + } + } + }); + + b.setContentDescription(activity.getResources().getString(msg.getInputType().getDescriptionId())); + b.setLongClickable(true); + b.setOnLongClickListener(new View.OnLongClickListener() + { + @Override + public boolean onLongClick(View v) + { + return Utils.showButtonDescription(activity, v); + } + }); + + b.setImageResource(msg.getInputType().getImageId()); + + selectorPaletteLayout.addView(b); + } + } + + private void updateSelectorButtons(@NonNull final State state) + { + if (selectorPaletteLayout == null) + { + return; + } + for (int i = 0; i < selectorPaletteLayout.getChildCount(); i++) + { + if (selectorPaletteLayout.getChildAt(i) instanceof AppCompatImageButton) + { + final AppCompatImageButton b = (AppCompatImageButton) selectorPaletteLayout.getChildAt(i); + setButtonSelected(b, state.inputType == b.getTag()); + } + } + } + + private void updateListView(final List mediaItems, final List serviceItems) + { + listView.clearChoices(); + listView.invalidate(); + + ArrayList newItems = new ArrayList<>(); + newItems.add(new OperationCommandMsg(OperationCommandMsg.Command.RETURN)); + int playing = -1; + if (mediaItems != null) + { + Logging.info(this, "Updating media items list: " + mediaItems.size()); + for (XmlListItemMsg i : mediaItems) + { + newItems.add(new XmlListItemMsg(i)); + if (i.getIcon() == XmlListItemMsg.Icon.PLAY) + { + playing = newItems.size() - 1; + } + } + } + else if (serviceItems != null) + { + Logging.info(this, "Updating service items list: " + serviceItems.size()); + for (NetworkServiceMsg i : serviceItems) + { + newItems.add(new NetworkServiceMsg(i)); + } + } + listViewAdapter = new XmListItemMsgAdapter(activity, newItems); + listView.setAdapter(listViewAdapter); + if (playing >= 0) + { + setSelection(playing, listView.getHeight() / 2); + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) + { + if (activity.getStateManager() != null && listViewAdapter != null && position < listViewAdapter.getCount()) + { + final ISCPMessage selectedItem = listViewAdapter.getItem(position); + if (selectedItem != null) + { + moveFrom = -1; + updateTitle(activity.getStateManager().getState(), true); + activity.getStateManager().navigateTo(selectedItem); + } + } + } + + private boolean isMoveToValid(int messageId) + { + return moveFrom >= 0 && moveFrom != messageId; + } + + public final void setSelection(int i, int y_) + { + final ListView flv$ = listView; + final int position$ = i, y$ = y_; + flv$.post(new Runnable() + { + public void run() + { + flv$.setSelectionFromTop(position$, y$ > 0 ? y$ : flv$.getHeight() / 2); + } + }); + } + + private void updateTitle(@NonNull final State state, boolean processing) + { + final StringBuilder title = new StringBuilder(); + title.append(state.titleBar); + if (state.numberOfItems > 0) + { + title.append("/").append(state.numberOfItems).append(" ").append( + activity.getResources().getString(R.string.medialist_items)); + } + if (processing) + { + title.append(". ").append( + activity.getResources().getString(R.string.medialist_processing)); + } + titleBar.setText(title.toString()); + } + + private final class XmListItemMsgAdapter extends ArrayAdapter + { + XmListItemMsgAdapter(Context context, ArrayList list) + { + super(context, 0, list); + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) + { + // Get the data item for this position + ISCPMessage item = getItem(position); + + // Check if an existing view is being reused, otherwise inflate the view + if (convertView == null) + { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.media_item, parent, false); + } + + final ImageView icon = convertView.findViewById(R.id.media_item_icon); + final TextView tvTitle = convertView.findViewById(R.id.media_item_title); + + if (item instanceof XmlListItemMsg) + { + final XmlListItemMsg msg = (XmlListItemMsg) item; + if (msg.getIcon() != XmlListItemMsg.Icon.UNKNOWN) + { + icon.setImageResource(msg.getIcon().getImageId()); + icon.setVisibility(View.VISIBLE); + Utils.setImageViewColorAttr(activity, icon, R.attr.colorButtonDisabled); + } + else if (!msg.isSelectable()) + { + icon.setImageDrawable(null); + icon.setVisibility(View.GONE); + } + tvTitle.setText(msg.getTitle()); + tvTitle.setTextColor(Utils.getThemeColorAttr(activity, + moveFrom == msg.getMessageId() ? android.R.attr.textColorSecondary : android.R.attr.textColor)); + } + else if (item instanceof NetworkServiceMsg) + { + final NetworkServiceMsg msg = (NetworkServiceMsg) item; + if (msg.getService().isImageValid()) + { + icon.setImageResource(msg.getService().getImageId()); + Utils.setImageViewColorAttr(activity, icon, R.attr.colorButtonDisabled); + } + tvTitle.setText(msg.getService().getDescriptionId()); + } + else if (item instanceof OperationCommandMsg) + { + final OperationCommandMsg msg = (OperationCommandMsg) item; + if (msg.getCommand().isImageValid()) + { + icon.setImageResource(msg.getCommand().getImageId()); + Utils.setImageViewColorAttr(activity, icon, android.R.attr.textColor); + } + tvTitle.setText(msg.getCommand().getDescriptionId()); + } + + return convertView; + } + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/MockupState.java b/app/src/main/java/com/mkulesh/onpc/MockupState.java new file mode 100644 index 00000000..242f78e4 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/MockupState.java @@ -0,0 +1,66 @@ +package com.mkulesh.onpc; + +import android.content.Context; +import android.graphics.BitmapFactory; + +import com.mkulesh.onpc.iscp.messages.InputSelectorMsg; +import com.mkulesh.onpc.iscp.messages.ListTitleInfoMsg; +import com.mkulesh.onpc.iscp.messages.MenuStatusMsg; +import com.mkulesh.onpc.iscp.messages.NetworkServiceMsg; +import com.mkulesh.onpc.iscp.messages.PlayStatusMsg; +import com.mkulesh.onpc.iscp.messages.PowerStatusMsg; +import com.mkulesh.onpc.iscp.messages.ReceiverInformationMsg; + +import java.util.ArrayList; + +class MockupState extends State +{ + MockupState(Context context) + { + //Common + powerStatus = PowerStatusMsg.PowerStatus.ON; + deviceProperties.put("brand", "Onkyo"); + deviceProperties.put("model", "NS-6130"); + deviceProperties.put("year", "2016"); + deviceProperties.put("firmwareversion", "1234-5678-910"); + deviceCover = BitmapFactory.decodeResource(context.getResources(), R.drawable.device_connect); + deviceSelectors.add(new ReceiverInformationMsg.Selector("2B", "Network", "2B", false)); + deviceSelectors.add(new ReceiverInformationMsg.Selector("29", "Front USB", "29", true)); + deviceSelectors.add(new ReceiverInformationMsg.Selector("2A", "Rear USB", "2A", true)); + inputType = InputSelectorMsg.InputType.NET; + + // Track info + cover = null; + album = "Album"; + artist = "Artist"; + title = "Long title of song"; + currentTime = "00:00:59"; + maxTime = "00:10:15"; + trackInfo = "0001/0022"; + fileFormat = "FLAC/44hHz/16b"; + + // Playback + playStatus = PlayStatusMsg.PlayStatus.PLAY; + repeatStatus = PlayStatusMsg.RepeatStatus.ALL; + shuffleStatus = PlayStatusMsg.ShuffleStatus.ALL; + timeSeek = MenuStatusMsg.TimeSeek.ENABLE; + + // Navigation + serviceType = ListTitleInfoMsg.ServiceType.NET; + layerInfo = ListTitleInfoMsg.LayerInfo.NET_TOP; + numberOfLayers = 0; + numberOfItems = 9; + titleBar = "Net"; + serviceItems = new ArrayList<>(); + serviceItems.add(new NetworkServiceMsg("Music Server")); + serviceItems.add(new NetworkServiceMsg("SPOTIFY")); + serviceItems.add(new NetworkServiceMsg("TuneIn")); + serviceItems.add(new NetworkServiceMsg("Deezer")); + serviceItems.add(new NetworkServiceMsg("Airplay")); + serviceItems.add(new NetworkServiceMsg("Tidal")); + serviceItems.add(new NetworkServiceMsg("Chromecast built-in")); + serviceItems.add(new NetworkServiceMsg("FlareConnect")); + serviceItems.add(new NetworkServiceMsg("Play Queue")); + itemsChanged = true; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/MonitorFragment.java b/app/src/main/java/com/mkulesh/onpc/MonitorFragment.java new file mode 100644 index 00000000..f5a85220 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/MonitorFragment.java @@ -0,0 +1,273 @@ +package com.mkulesh.onpc; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.AppCompatImageButton; +import android.support.v7.widget.AppCompatSeekBar; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.mkulesh.onpc.iscp.ISCPMessage; +import com.mkulesh.onpc.iscp.messages.AmpOperationCommandMsg; +import com.mkulesh.onpc.iscp.messages.MenuStatusMsg; +import com.mkulesh.onpc.iscp.messages.OperationCommandMsg; +import com.mkulesh.onpc.iscp.messages.PlayStatusMsg; +import com.mkulesh.onpc.iscp.messages.TimeSeekMsg; +import com.mkulesh.onpc.utils.Utils; + +import java.util.ArrayList; +import java.util.List; + +public class MonitorFragment extends BaseFragment +{ + private AppCompatImageButton btnRepeat; + private AppCompatImageButton btnPrevious; + private AppCompatImageButton btnPausePlay; + private AppCompatImageButton btnNext; + private AppCompatImageButton btnRandom; + private List cmdButtons = new ArrayList<>(); + private List ampButtons = new ArrayList<>(); + private ImageView cover; + private AppCompatSeekBar seekBar; + + public MonitorFragment() + { + // Empty constructor required for fragment subclasses + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + initializeFragment(inflater, container, R.layout.monitor_fragment); + rootView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + + // Command Buttons + btnRepeat = rootView.findViewById(R.id.btn_repeat); + cmdButtons.add(btnRepeat); + + btnPrevious = rootView.findViewById(R.id.btn_previous); + cmdButtons.add(btnPrevious); + + final AppCompatImageButton btnStop = rootView.findViewById(R.id.btn_stop); + cmdButtons.add(btnStop); + + btnPausePlay = rootView.findViewById(R.id.btn_pause_play); + cmdButtons.add(btnPausePlay); + + btnNext = rootView.findViewById(R.id.btn_next); + cmdButtons.add(btnNext); + + btnRandom = rootView.findViewById(R.id.btn_random); + cmdButtons.add(btnRandom); + + for (AppCompatImageButton b : cmdButtons) + { + final OperationCommandMsg msg = new OperationCommandMsg((String) (b.getTag())); + prepareButton(b, msg, msg.getCommand().getImageId(), msg.getCommand().getDescriptionId()); + } + + // Amplifier command Buttons + { + ampButtons.add((AppCompatImageButton) rootView.findViewById(R.id.btn_volume_up)); + ampButtons.add((AppCompatImageButton) rootView.findViewById(R.id.btn_volume_down)); + ampButtons.add((AppCompatImageButton) rootView.findViewById(R.id.btn_volume_mute)); + for (AppCompatImageButton b : ampButtons) + { + final AmpOperationCommandMsg msg = new AmpOperationCommandMsg((String) (b.getTag())); + prepareButton(b, msg, msg.getCommand().getImageId(), msg.getCommand().getDescriptionId()); + } + } + + cover = rootView.findViewById(R.id.tv_cover); + seekBar = rootView.findViewById(R.id.progress_bar); + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() + { + int progressChanged = 0; + + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) + { + progressChanged = progress; + } + + public void onStartTrackingTouch(SeekBar seekBar) + { + // empty + } + + public void onStopTrackingTouch(SeekBar seekBar) + { + if (activity.getStateManager() != null) + { + seekTime(progressChanged); + } + } + }); + + update(null); + return rootView; + } + + private void prepareButton(AppCompatImageButton b, final ISCPMessage msg, final int imageId, final int descriptionId) + { + b.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + if (activity.getStateManager() != null) + { + activity.getStateManager().navigateTo(msg); + } + } + }); + + b.setContentDescription(activity.getResources().getString(descriptionId)); + b.setOnLongClickListener(new View.OnLongClickListener() + { + @Override + public boolean onLongClick(View v) + { + return Utils.showButtonDescription(activity, v); + } + }); + + b.setImageResource(imageId); + setButtonEnabled(b, false); + } + + @Override + protected void updateStandbyView(@Nullable final State state) + { + ((TextView) rootView.findViewById(R.id.tv_time_start)).setText( + activity.getResources().getString(R.string.tv_time_default)); + ((TextView) rootView.findViewById(R.id.tv_time_end)).setText( + activity.getResources().getString(R.string.tv_time_default)); + ((TextView) rootView.findViewById(R.id.tv_track)).setText(""); + ((TextView) rootView.findViewById(R.id.tv_album)).setText(""); + ((TextView) rootView.findViewById(R.id.tv_artist)).setText(""); + ((TextView) rootView.findViewById(R.id.tv_title)).setText(""); + ((TextView) rootView.findViewById(R.id.tv_file_format)).setText(""); + cover.setImageResource(R.drawable.empty_cover); + seekBar.setEnabled(false); + seekBar.setProgress(0); + for (AppCompatImageButton b : ampButtons) + { + setButtonEnabled(b, state != null); + } + for (AppCompatImageButton b : cmdButtons) + { + setButtonEnabled(b, false); + } + } + + @Override + protected void updateActiveView(@NonNull final State state) + { + // Text + ((TextView) rootView.findViewById(R.id.tv_time_start)).setText(state.currentTime); + ((TextView) rootView.findViewById(R.id.tv_time_end)).setText(state.maxTime); + ((TextView) rootView.findViewById(R.id.tv_track)).setText(state.trackInfo); + ((TextView) rootView.findViewById(R.id.tv_album)).setText(state.album); + ((TextView) rootView.findViewById(R.id.tv_artist)).setText(state.artist); + ((TextView) rootView.findViewById(R.id.tv_title)).setText(state.title); + ((TextView) rootView.findViewById(R.id.tv_file_format)).setText(state.fileFormat); + + // cover + if (state.cover == null) + { + cover.setImageResource(R.drawable.empty_cover); + } + else + { + cover.setImageBitmap(state.cover); + } + + // progress bar + final int currTime = Utils.timeToSeconds(state.currentTime); + final int maxTime = Utils.timeToSeconds(state.maxTime); + if (currTime >= 0 && maxTime >= 0 && state.timeSeek == MenuStatusMsg.TimeSeek.ENABLE) + { + seekBar.setEnabled(true); + seekBar.setMax(maxTime); + seekBar.setProgress(currTime); + } + else + { + seekBar.setEnabled(false); + seekBar.setMax(1000); + seekBar.setProgress(0); + } + + // buttons + for (AppCompatImageButton b : ampButtons) + { + setButtonEnabled(b, true); + } + for (AppCompatImageButton b : cmdButtons) + { + setButtonEnabled(b, true); + } + + if (state.repeatStatus == PlayStatusMsg.RepeatStatus.DISABLE) + { + setButtonEnabled(btnRepeat, false); + } + else + { + setButtonEnabled(btnRepeat, true); + setButtonSelected(btnRepeat, state.repeatStatus != PlayStatusMsg.RepeatStatus.OFF); + } + + if (state.shuffleStatus == PlayStatusMsg.ShuffleStatus.DISABLE) + { + setButtonEnabled(btnRandom, false); + } + else + { + setButtonEnabled(btnRandom, true); + setButtonSelected(btnRandom, state.shuffleStatus != PlayStatusMsg.ShuffleStatus.OFF); + } + + setButtonEnabled(btnPrevious, state.isPlaying()); + setButtonEnabled(btnNext, state.isPlaying()); + + switch (state.playStatus) + { + case STOP: + btnPausePlay.setImageResource(R.drawable.cmd_play); + break; + case PLAY: + btnPausePlay.setImageResource(R.drawable.cmd_pause); + break; + case PAUSE: + btnPausePlay.setImageResource(R.drawable.cmd_play); + break; + default: + break; + } + setButtonEnabled(btnPausePlay, state.isOn()); + } + + private void seekTime(int newSec) + { + final State state = activity.getStateManager().getState(); + final int currTime = Utils.timeToSeconds(state.currentTime); + final int maxTime = Utils.timeToSeconds(state.maxTime); + if (currTime >= 0 && maxTime >= 0) + { + final int hour = newSec / 3600; + final int min = (newSec - hour * 3600) / 60; + final int sec = newSec - hour * 3600 - min * 60; + activity.getStateManager().requestSkipNextTimeMsg(2); + final TimeSeekMsg msg = new TimeSeekMsg(hour, min, sec); + state.currentTime = msg.getTimeAsString(); + ((TextView) rootView.findViewById(R.id.tv_time_start)).setText(state.currentTime); + activity.getStateManager().navigateTo(msg); + } + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/SettingsActivity.java b/app/src/main/java/com/mkulesh/onpc/SettingsActivity.java new file mode 100644 index 00000000..8a017bb7 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/SettingsActivity.java @@ -0,0 +1,91 @@ +package com.mkulesh.onpc; + +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.Preference; +import android.support.v7.app.ActionBar; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import com.mkulesh.onpc.utils.AppTheme; +import com.mkulesh.onpc.widgets.AppCompatPreferenceActivity; + +public class SettingsActivity extends AppCompatPreferenceActivity +{ + @Override + @SuppressWarnings("deprecation") + protected void onCreate(Bundle savedInstanceState) + { + setTheme(AppTheme.getTheme(this, AppTheme.ThemeType.SETTINGS_THEME)); + super.onCreate(savedInstanceState); + setupActionBar(); + addPreferencesFromResource(R.xml.preferences); + prepareListPreference((ListPreference) findPreference("app_language")); + prepareListPreference((ListPreference) findPreference("app_theme")); + } + + private void setupActionBar() + { + ViewGroup rootView = findViewById(R.id.action_bar_root); //id from appcompat + if (rootView != null) + { + View view = getLayoutInflater().inflate(R.layout.settings_toolbar, rootView, false); + rootView.addView(view, 0); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + } + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) + { + // Show the Up button in the action bar. + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setTitle(R.string.action_app_settings); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + int id = item.getItemId(); + if (id == android.R.id.home) + { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void prepareListPreference(final ListPreference listPreference) + { + if (listPreference == null) + { + return; + } + + if (listPreference.getValue() == null) + { + // to ensure we don't get a null value + // set first value by default + listPreference.setValueIndex(0); + } + + if (listPreference.getEntry() != null) + { + listPreference.setSummary(listPreference.getEntry().toString()); + } + listPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() + { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) + { + listPreference.setValue(newValue.toString()); + preference.setSummary(listPreference.getEntry().toString()); + return true; + } + }); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/State.java b/app/src/main/java/com/mkulesh/onpc/State.java new file mode 100644 index 00000000..c358583e --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/State.java @@ -0,0 +1,379 @@ +package com.mkulesh.onpc; + +import android.graphics.Bitmap; + +import com.mkulesh.onpc.iscp.ISCPMessage; +import com.mkulesh.onpc.iscp.messages.AlbumNameMsg; +import com.mkulesh.onpc.iscp.messages.ArtistNameMsg; +import com.mkulesh.onpc.iscp.messages.FileFormatMsg; +import com.mkulesh.onpc.iscp.messages.InputSelectorMsg; +import com.mkulesh.onpc.iscp.messages.JacketArtMsg; +import com.mkulesh.onpc.iscp.messages.ListInfoMsg; +import com.mkulesh.onpc.iscp.messages.ListTitleInfoMsg; +import com.mkulesh.onpc.iscp.messages.MenuStatusMsg; +import com.mkulesh.onpc.iscp.messages.NetworkServiceMsg; +import com.mkulesh.onpc.iscp.messages.PlayStatusMsg; +import com.mkulesh.onpc.iscp.messages.PowerStatusMsg; +import com.mkulesh.onpc.iscp.messages.ReceiverInformationMsg; +import com.mkulesh.onpc.iscp.messages.TimeInfoMsg; +import com.mkulesh.onpc.iscp.messages.TitleNameMsg; +import com.mkulesh.onpc.iscp.messages.TrackInfoMsg; +import com.mkulesh.onpc.iscp.messages.XmlListInfoMsg; +import com.mkulesh.onpc.iscp.messages.XmlListItemMsg; +import com.mkulesh.onpc.utils.Logging; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class State +{ + //Common + PowerStatusMsg.PowerStatus powerStatus = PowerStatusMsg.PowerStatus.STB; + Map deviceProperties = new HashMap<>(); + Bitmap deviceCover = null; + List deviceSelectors; + InputSelectorMsg.InputType inputType = InputSelectorMsg.InputType.NONE; + + // Track info + Bitmap cover = null; + String album = "", artist = "", title = ""; + String currentTime = "", maxTime = ""; + String trackInfo = ""; + String fileFormat = ""; + private ByteArrayOutputStream coverBuffer = null; + + // Playback + PlayStatusMsg.PlayStatus playStatus = PlayStatusMsg.PlayStatus.STOP; + PlayStatusMsg.RepeatStatus repeatStatus = PlayStatusMsg.RepeatStatus.OFF; + PlayStatusMsg.ShuffleStatus shuffleStatus = PlayStatusMsg.ShuffleStatus.OFF; + MenuStatusMsg.TimeSeek timeSeek = MenuStatusMsg.TimeSeek.ENABLE; + + // Navigation + ListTitleInfoMsg.ServiceType serviceType = null; + ListTitleInfoMsg.LayerInfo layerInfo = null; + int numberOfLayers = 0; + int numberOfItems = 0; + String titleBar = ""; + List mediaItems = null; + List serviceItems = null; + boolean itemsChanged = false; + + State() + { + deviceSelectors = new ArrayList<>(); + } + + @Override + public String toString() + { + return powerStatus.toString() + + "; " + album + "/" + artist + "/" + title + + "; " + currentTime + "/" + maxTime + + "; " + playStatus.toString() + "/" + repeatStatus.toString() + "/" + shuffleStatus.toString() + + "; cover=" + ((cover != null) ? "YES" : "NO"); + } + + boolean isOn() + { + return powerStatus == PowerStatusMsg.PowerStatus.ON; + } + + boolean isPlaying() + { + return playStatus != PlayStatusMsg.PlayStatus.STOP; + } + + boolean update(ISCPMessage msg) + { + if (!(msg instanceof TimeInfoMsg) && !(msg instanceof JacketArtMsg)) + { + Logging.info(msg, "<< " + msg.toString()); + } + + //Common + if (msg instanceof PowerStatusMsg) + { + return process((PowerStatusMsg) msg); + } + if (msg instanceof ReceiverInformationMsg) + { + return process((ReceiverInformationMsg) msg); + } + if (msg instanceof InputSelectorMsg) + { + return process((InputSelectorMsg) msg); + } + + // Track info + if (msg instanceof JacketArtMsg) + { + return process((JacketArtMsg) msg); + } + if (msg instanceof AlbumNameMsg) + { + return process((AlbumNameMsg) msg); + } + if (msg instanceof ArtistNameMsg) + { + return process((ArtistNameMsg) msg); + } + if (msg instanceof TitleNameMsg) + { + return process((TitleNameMsg) msg); + } + if (msg instanceof FileFormatMsg) + { + return process((FileFormatMsg) msg); + } + if (msg instanceof TimeInfoMsg) + { + return process((TimeInfoMsg) msg); + } + if (msg instanceof TrackInfoMsg) + { + return process((TrackInfoMsg) msg); + } + + // Playback + if (msg instanceof PlayStatusMsg) + { + return process((PlayStatusMsg) msg); + } + if (msg instanceof MenuStatusMsg) + { + return process((MenuStatusMsg) msg); + } + + // Navigation + if (msg instanceof ListTitleInfoMsg) + { + return process((ListTitleInfoMsg) msg); + } + if (msg instanceof XmlListInfoMsg) + { + return process((XmlListInfoMsg) msg); + } + return msg instanceof ListInfoMsg && process((ListInfoMsg) msg); + } + + private boolean process(PowerStatusMsg msg) + { + final boolean changed = msg.getPowerStatus() != powerStatus; + powerStatus = msg.getPowerStatus(); + return changed; + } + + private boolean process(ReceiverInformationMsg msg) + { + try + { + msg.parseXml(); + deviceProperties = msg.getDeviceProperties(); + deviceCover = msg.getDeviceCover(); + deviceSelectors = msg.getDeviceSelectors(); + return true; + } + catch (Exception e) + { + Logging.info(msg, "Can not parse XML: " + e.getLocalizedMessage()); + } + return false; + } + + private boolean process(InputSelectorMsg msg) + { + final boolean changed = inputType != msg.getInputType(); + inputType = msg.getInputType(); + return changed; + } + + private boolean process(JacketArtMsg msg) + { + cover = null; + if (msg.getImageType() == JacketArtMsg.ImageType.URL) + { + Logging.info(msg, "<< " + msg.toString()); + cover = msg.loadFromUrl(); + return true; + } + else if (msg.getRawData() != null) + { + final byte in[] = msg.getRawData(); + if (msg.getPacketFlag() == JacketArtMsg.PacketFlag.START) + { + Logging.info(msg, "<< " + msg.toString()); + coverBuffer = new ByteArrayOutputStream(); + } + if (coverBuffer != null) + { + coverBuffer.write(in, 0, in.length); + } + if (msg.getPacketFlag() == JacketArtMsg.PacketFlag.END) + { + Logging.info(msg, "<< " + msg.toString()); + cover = msg.loadFromBuffer(coverBuffer); + coverBuffer = null; + return true; + } + } + return false; + } + + private boolean process(AlbumNameMsg msg) + { + final boolean changed = !msg.getData().equals(album); + album = msg.getData(); + return changed; + } + + private boolean process(ArtistNameMsg msg) + { + final boolean changed = !msg.getData().equals(artist); + artist = msg.getData(); + return changed; + } + + private boolean process(TitleNameMsg msg) + { + final boolean changed = !msg.getData().equals(title); + title = msg.getData(); + return changed; + } + + private boolean process(TimeInfoMsg msg) + { + final boolean changed = !msg.getCurrentTime().equals(currentTime) + || !msg.getMaxTime().equals(maxTime); + currentTime = msg.getCurrentTime(); + maxTime = msg.getMaxTime(); + return changed; + } + + private boolean process(TrackInfoMsg msg) + { + final String newInfo = msg.getCurrentTrack() + "/" + msg.getMaxTrack(); + final boolean changed = !newInfo.equals(trackInfo); + trackInfo = newInfo; + return changed; + } + + private boolean process(FileFormatMsg msg) + { + final boolean changed = !msg.getFullFormat().equals(title); + fileFormat = msg.getFullFormat(); + return changed; + } + + private boolean process(PlayStatusMsg msg) + { + final boolean changed = msg.getPlayStatus() != playStatus + || msg.getRepeatStatus() != repeatStatus + || msg.getShuffleStatus() != shuffleStatus; + playStatus = msg.getPlayStatus(); + repeatStatus = msg.getRepeatStatus(); + shuffleStatus = msg.getShuffleStatus(); + return changed; + } + + private boolean process(MenuStatusMsg msg) + { + final boolean changed = msg.getTimeSeek() != timeSeek; + timeSeek = msg.getTimeSeek(); + return changed; + } + + private boolean process(ListTitleInfoMsg msg) + { + boolean changed = false; + if (serviceType != msg.getServiceType()) + { + serviceType = msg.getServiceType(); + mediaItems = null; + serviceItems = null; + itemsChanged = true; + changed = true; + } + if (layerInfo != msg.getLayerInfo()) + { + layerInfo = msg.getLayerInfo(); + changed = true; + } + if (!titleBar.equals(msg.getTitleBar())) + { + titleBar = msg.getTitleBar(); + changed = true; + } + if (numberOfLayers != msg.getNumberOfLayers()) + { + numberOfLayers = msg.getNumberOfLayers(); + changed = true; + } + if (numberOfItems != msg.getNumberOfItems()) + { + numberOfItems = msg.getNumberOfItems(); + changed = true; + } + return changed; + } + + private boolean process(XmlListInfoMsg msg) + { + try + { + Logging.info(msg, "processig XmlListInfoMsg"); + mediaItems = msg.parseXml(numberOfLayers); + itemsChanged = true; + return true; + } + catch (Exception e) + { + Logging.info(msg, "Can not parse XML: " + e.getLocalizedMessage()); + } + return false; + } + + private boolean process(ListInfoMsg msg) + { + if (msg.getInformationType() == ListInfoMsg.InformationType.CURSOR) + { + return false; + } + if (serviceType == ListTitleInfoMsg.ServiceType.NET) + { + if (serviceItems == null) + { + serviceItems = new ArrayList<>(); + } + for (NetworkServiceMsg i : serviceItems) + { + if (i.getService().getCode().toUpperCase().equals(msg.getListedData().toUpperCase())) + { + return false; + } + } + final NetworkServiceMsg nsMsg = new NetworkServiceMsg(msg.getListedData()); + if (nsMsg.getService() != NetworkServiceMsg.Service.UNKNOWN) + { + serviceItems.add(nsMsg); + } + itemsChanged = true; + return true; + } + return false; + } + + ReceiverInformationMsg.Selector getActualSelector() + { + for (ReceiverInformationMsg.Selector s : deviceSelectors) + { + if (s.getId().equals(inputType.getCode())) + { + return s; + } + } + return null; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/StateManager.java b/app/src/main/java/com/mkulesh/onpc/StateManager.java new file mode 100644 index 00000000..f35d60f4 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/StateManager.java @@ -0,0 +1,276 @@ +package com.mkulesh.onpc; + +import android.os.AsyncTask; +import android.os.StrictMode; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; +import com.mkulesh.onpc.iscp.MessageChannel; +import com.mkulesh.onpc.iscp.messages.AlbumNameMsg; +import com.mkulesh.onpc.iscp.messages.ArtistNameMsg; +import com.mkulesh.onpc.iscp.messages.FileFormatMsg; +import com.mkulesh.onpc.iscp.messages.InputSelectorMsg; +import com.mkulesh.onpc.iscp.messages.ListTitleInfoMsg; +import com.mkulesh.onpc.iscp.messages.MenuStatusMsg; +import com.mkulesh.onpc.iscp.messages.OperationCommandMsg; +import com.mkulesh.onpc.iscp.messages.PlayStatusMsg; +import com.mkulesh.onpc.iscp.messages.PowerStatusMsg; +import com.mkulesh.onpc.iscp.messages.ReceiverInformationMsg; +import com.mkulesh.onpc.iscp.messages.TimeInfoMsg; +import com.mkulesh.onpc.iscp.messages.TitleNameMsg; +import com.mkulesh.onpc.iscp.messages.TrackInfoMsg; +import com.mkulesh.onpc.iscp.messages.XmlListInfoMsg; +import com.mkulesh.onpc.utils.Logging; + +import java.util.Timer; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +class StateManager extends AsyncTask +{ + private static final long GUI_UPDATE_DELAY = 500; + private final State state; + private final MainActivity activity; + private final AtomicBoolean active = new AtomicBoolean(); + private final AtomicInteger skipNextTimeMsg = new AtomicInteger(); + private final MessageChannel messageChannel; + private int xmlReqId = 0; + private ISCPMessage circlePlayQueueMsg = null; + + StateManager(MainActivity activity, MessageChannel messageChannel, boolean mockup) + { + this.activity = activity; + this.messageChannel = messageChannel; + state = mockup ? new MockupState(activity) : new State(); + StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.setThreadPolicy(policy); + } + + void stop() + { + synchronized (active) + { + active.set(false); + } + } + + State getState() + { + return state; + } + + @Override + protected Void doInBackground(Void... params) + { + Logging.info(this, "started"); + active.set(true); + + requestPowerState(); + final BlockingQueue timerQueue = new ArrayBlockingQueue<>(1, true); + skipNextTimeMsg.set(0); + while (true) + { + try + { + synchronized (active) + { + if (!active.get() || isCancelled()) + { + Logging.info(this, "cancelled"); + break; + } + } + + final PlayStatusMsg.PlayStatus playStatus = state.playStatus; + final ISCPMessage msg = messageChannel.getInputQueue().take(); + boolean changed = false; + if (msg != null) + { + if (msg instanceof TimeInfoMsg && skipNextTimeMsg.get() > 0) + { + // skip time message + skipNextTimeMsg.set(Math.max(0, skipNextTimeMsg.get() - 1)); + } + else + { + changed = state.update(msg); + } + } + + if (changed && state.isOn()) + { + if (msg instanceof PowerStatusMsg) + { + requestPlayState(); + requestListState(); + } + else if (msg instanceof PlayStatusMsg && playStatus != state.playStatus) + { + if (state.isPlaying()) + { + requestTrackState(); + } + else + { + requestListState(); + } + } + else if (msg instanceof TrackInfoMsg) + { + if (((TrackInfoMsg) msg).isValidTrack()) + { + requestListState(); + } + } + else if (msg instanceof ListTitleInfoMsg) + { + final ListTitleInfoMsg liMsg = (ListTitleInfoMsg) msg; + if (circlePlayQueueMsg != null && liMsg.getNumberOfItems() > 0) + { + sendPlayQueueMsg(circlePlayQueueMsg, true); + } + else + { + circlePlayQueueMsg = null; + requestXmlListState(liMsg); + } + } + } + + if (changed && timerQueue.isEmpty()) + { + final Timer t = new Timer(); + timerQueue.add(t); + t.schedule(new java.util.TimerTask() + { + @Override + public void run() + { + timerQueue.poll(); + publishProgress(); + } + }, + GUI_UPDATE_DELAY + ); + } + } + catch (Exception e) + { + Logging.info(this, "interrupted: " + e.getLocalizedMessage()); + break; + } + } + + synchronized (active) + { + active.set(false); + } + Logging.info(this, "stopped"); + return null; + } + + @Override + protected void onProgressUpdate(Void... result) + { + activity.updateCurrentFragment(state); + } + + private void requestPowerState() + { + Logging.info(this, "requesting power state..."); + messageChannel.sendMessage( + new EISCPMessage('1', PowerStatusMsg.CODE, EISCPMessage.QUERY)); + messageChannel.sendMessage( + new EISCPMessage('1', ReceiverInformationMsg.CODE, EISCPMessage.QUERY)); + messageChannel.sendMessage( + new EISCPMessage('1', InputSelectorMsg.CODE, EISCPMessage.QUERY)); + } + + private void requestPlayState() + { + Logging.info(this, "requesting play state..."); + messageChannel.sendMessage( + new EISCPMessage('1', PlayStatusMsg.CODE, EISCPMessage.QUERY)); + } + + private void requestTrackState() + { + Logging.info(this, "requesting track state..."); + messageChannel.sendMessage( + new EISCPMessage('1', ArtistNameMsg.CODE, EISCPMessage.QUERY)); + messageChannel.sendMessage( + new EISCPMessage('1', AlbumNameMsg.CODE, EISCPMessage.QUERY)); + messageChannel.sendMessage( + new EISCPMessage('1', TitleNameMsg.CODE, EISCPMessage.QUERY)); + messageChannel.sendMessage( + new EISCPMessage('1', FileFormatMsg.CODE, EISCPMessage.QUERY)); + messageChannel.sendMessage( + new EISCPMessage('1', TrackInfoMsg.CODE, EISCPMessage.QUERY)); + messageChannel.sendMessage( + new EISCPMessage('1', TimeInfoMsg.CODE, EISCPMessage.QUERY)); + messageChannel.sendMessage( + new EISCPMessage('1', MenuStatusMsg.CODE, EISCPMessage.QUERY)); + } + + private void requestListState() + { + Logging.info(this, "requesting list state..."); + state.serviceType = null; // request update of List Title Info + messageChannel.sendMessage( + new EISCPMessage('1', ListTitleInfoMsg.CODE, EISCPMessage.QUERY)); + } + + private void requestXmlListState(final ListTitleInfoMsg liMsg) + { + if (liMsg.getServiceType() == ListTitleInfoMsg.ServiceType.NET + && liMsg.getLayerInfo() == ListTitleInfoMsg.LayerInfo.NET_TOP) + { + Logging.info(this, "requesting XML list state skipped"); + return; + } + Logging.info(this, "requesting XML list state"); + if (liMsg.getUiType() == ListTitleInfoMsg.UIType.PLAYBACK) + { + navigateTo(new OperationCommandMsg(OperationCommandMsg.Command.RETURN)); + } + else if (liMsg.getNumberOfLayers() > 0) + { + messageChannel.sendMessage( + new EISCPMessage('1', XmlListInfoMsg.CODE, XmlListInfoMsg.getListedData( + xmlReqId++, liMsg.getNumberOfLayers(), 0, liMsg.getNumberOfItems()))); + } + } + + void navigateTo(final ISCPMessage msg) + { + circlePlayQueueMsg = null; + Logging.info(this, "selecting: " + msg.toString()); + final EISCPMessage cmdMsg = msg.getCmdMsg(); + if (cmdMsg != null) + { + messageChannel.sendMessage(cmdMsg); + } + } + + void sendPlayQueueMsg(ISCPMessage msg, boolean repeat) + { + if (msg == null) + { + return; + } + if (repeat) + { + Logging.info(this, "starting repeat mode: " + msg.toString()); + circlePlayQueueMsg = msg; + } + state.serviceType = null; // request update of List Title Info + messageChannel.sendMessage(msg.getCmdMsg()); + } + + void requestSkipNextTimeMsg(final int number) + { + skipNextTimeMsg.set(number); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/EISCPMessage.java b/app/src/main/java/com/mkulesh/onpc/iscp/EISCPMessage.java new file mode 100644 index 00000000..41b7199b --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/EISCPMessage.java @@ -0,0 +1,234 @@ +package com.mkulesh.onpc.iscp; + +import com.mkulesh.onpc.utils.Utils; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; + +public class EISCPMessage +{ + private final static String MSG_START = "ISCP"; + private final static String INVALID_MSG = "INVALID"; + private final static int CR = 0x0D; + private final static int LF = 0x0A; + private final static int EOF = 0x1A; + private final static Character START_CHAR = '!'; + private final static int MIN_MSG_LENGTH = 22; + public final static String QUERY = "QSTN"; + + private final int messageId; + private final int headerSize, dataSize, version; + private final Character modelCategoryId; + private final String code; + private final String parameters; + + public EISCPMessage(int messageId, byte[] bytes, int startIndex, int headerSize, int dataSize) throws Exception + { + this.messageId = messageId; + this.headerSize = headerSize; + this.dataSize = dataSize; + version = getVersion(bytes, startIndex); + final String body = getRawMessage(bytes, startIndex); + + if (body.length() < 5) + { + throw new Exception("Can not decode message body: length " + body.length() + " is invalid"); + } + if (body.charAt(0) != START_CHAR) + { + throw new Exception("Can not find start character in the raw message"); + } + + modelCategoryId = body.charAt(1); + code = body.substring(2, 5); + parameters = (body.length() > 5) ? body.substring(5) : ""; + } + + public EISCPMessage(final Character modelCategoryId, final String code, final String parameters) + { + messageId = 0; + headerSize = 16; + dataSize = 2 + code.length() + parameters.length() + 1; + version = 1; + this.modelCategoryId = modelCategoryId; + this.code = code; + this.parameters = parameters; + } + + @Override + public String toString() + { + return MSG_START + "/v" + version + "[" + headerSize + "," + dataSize + "]: " + code + "(" + parameters + ")"; + } + + int getMsgSize() + { + return headerSize + dataSize; + } + + int getMessageId() + { + return messageId; + } + + Character getModelCategoryId() + { + return modelCategoryId; + } + + public String getCode() + { + return code; + } + + String getParameters() + { + return parameters; + } + + static int getMsgStartIndex(byte[] bytes) + { + for (int i = 0; i < bytes.length; i++) + { + if (bytes[i] == MSG_START.charAt(0) && + bytes[i + 1] == MSG_START.charAt(1) && + bytes[i + 2] == MSG_START.charAt(2) && + bytes[i + 3] == MSG_START.charAt(3)) + { + return i; + } + } + return -1; + } + + static int getHeaderSize(byte[] bytes, int startIndex) throws Exception + { + // Header Size : 4 bytes after "ISCP" + try + { + if (startIndex + MSG_START.length() + 4 <= bytes.length) + { + return ByteBuffer.wrap(bytes, startIndex + MSG_START.length(), 4).getInt(); + } + } + catch (Exception e) + { + throw new Exception("Can not decode header size: " + e.getLocalizedMessage()); + } + return -1; + } + + static int getDataSize(byte[] bytes, int startIndex) throws Exception + { + // Data Size : 4 bytes after Header Size + try + { + if (startIndex + MSG_START.length() + 8 <= bytes.length) + { + return ByteBuffer.wrap(bytes, startIndex + MSG_START.length() + 4, 4).getInt(); + } + } + catch (Exception e) + { + throw new Exception("Can not decode data size: " + e.getLocalizedMessage()); + } + return -1; + } + + private int getVersion(byte[] bytes, int startIndex) throws Exception + { + // Version : 1 byte after Data Size + try + { + if (startIndex + MSG_START.length() + 9 <= bytes.length) + { + final byte[] intBytes = new byte[]{ 0, 0, 0, 0 }; + intBytes[3] = bytes[startIndex + MSG_START.length() + 8]; + return ByteBuffer.wrap(intBytes).getInt(); + } + } + catch (Exception e) + { + throw new Exception("Can not decode version: " + e.getLocalizedMessage()); + } + return -1; + } + + private boolean isSpecialCharacter(byte val) + { + return val == EOF || val == CR || val == LF; + } + + private String getRawMessage(byte[] bytes, int startIndex) throws Exception + { + try + { + if (headerSize > 0 && dataSize > 0 && startIndex + headerSize + dataSize <= bytes.length) + { + int actualLength = 0; + for (int i = 0; i < dataSize; i++) + { + byte val = bytes[startIndex + headerSize + i]; + if (isSpecialCharacter(val)) + { + break; + } + actualLength++; + } + final byte[] stringBytes = Utils.catBuffer(bytes, startIndex + headerSize, actualLength); + return new String(stringBytes, Charset.forName("UTF-8")); + } + } + catch (Exception e) + { + throw new Exception("Can not decode raw message: " + e.getLocalizedMessage()); + } + return INVALID_MSG; + } + + byte[] getBytes() + { + if (headerSize + dataSize < MIN_MSG_LENGTH) + { + return null; + } + final byte[] bytes = new byte[headerSize + dataSize]; + Arrays.fill(bytes, (byte) 0); + + // Message header + for (int i = 0; i < MSG_START.length(); i++) + { + bytes[i] = (byte) MSG_START.charAt(i); + } + + // Header size + byte[] size = ByteBuffer.allocate(4).putInt(headerSize).array(); + System.arraycopy(size, 0, bytes, 4, size.length); + + // Data size + size = ByteBuffer.allocate(4).putInt(dataSize).array(); + System.arraycopy(size, 0, bytes, 8, size.length); + + // Version + bytes[12] = (byte) version; + + // CMD + bytes[16] = (byte) START_CHAR.charValue(); + bytes[17] = (byte) '1'; + for (int i = 0; i < code.length(); i++) + { + bytes[i + 18] = (byte) code.charAt(i); + } + + // Parameters + for (int i = 0; i < parameters.length(); i++) + { + bytes[i + 21] = (byte) parameters.charAt(i); + } + + // End char + bytes[21 + parameters.length()] = (byte) LF; + return bytes; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/ISCPMessage.java b/app/src/main/java/com/mkulesh/onpc/iscp/ISCPMessage.java new file mode 100644 index 00000000..d277abf7 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/ISCPMessage.java @@ -0,0 +1,96 @@ +package com.mkulesh.onpc.iscp; + +public class ISCPMessage +{ + protected final static String PAR_SEP = "/"; + + protected final int messageId; + protected final String data; + private final Character modelCategoryId; + + protected ISCPMessage(final int messageId, final String data) + { + this.messageId = messageId; + this.data = data; + modelCategoryId = 'X'; + } + + protected ISCPMessage(EISCPMessage raw) throws Exception + { + messageId = raw.getMessageId(); + data = raw.getParameters().trim(); + modelCategoryId = raw.getModelCategoryId(); + } + + public ISCPMessage(ISCPMessage other) + { + messageId = other.messageId; + data = other.data; + modelCategoryId = other.modelCategoryId; + } + + public int getMessageId() + { + return messageId; + } + + public final String getData() + { + return data; + } + + @Override + public String toString() + { + return "ISCPMessage/" + modelCategoryId; + } + + /** + * Helper methods for enumerations based on char parameter + */ + protected interface CharParameterIf + { + Character getCode(); + } + + protected static CharParameterIf searchParameter(Character code, CharParameterIf[] values, CharParameterIf defValue) + { + for (CharParameterIf t : values) + { + if (t.getCode() == code) + { + return t; + } + } + return defValue; + } + + /** + * Helper methods for enumerations based on char parameter + */ + public interface StringParameterIf + { + String getCode(); + } + + public static StringParameterIf searchParameter(String code, StringParameterIf[] values, StringParameterIf defValue) + { + if (code == null) + { + return defValue; + } + for (StringParameterIf t : values) + { + if (t.getCode().toUpperCase().equals(code.toUpperCase())) + { + return t; + } + } + return defValue; + } + + public EISCPMessage getCmdMsg() + { + return null; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/MessageChannel.java b/app/src/main/java/com/mkulesh/onpc/iscp/MessageChannel.java new file mode 100644 index 00000000..947adccd --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/MessageChannel.java @@ -0,0 +1,236 @@ +package com.mkulesh.onpc.iscp; + +import android.os.AsyncTask; +import android.os.StrictMode; +import android.support.v4.app.FragmentActivity; +import android.widget.Toast; + +import com.mkulesh.onpc.R; +import com.mkulesh.onpc.iscp.messages.MessageFactory; +import com.mkulesh.onpc.iscp.messages.OperationCommandMsg; +import com.mkulesh.onpc.utils.Logging; +import com.mkulesh.onpc.utils.Utils; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.Calendar; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +public class MessageChannel extends AsyncTask +{ + private final static long CONNECTION_TIMEOUT = 5000; + private final static int QUEUE_SIZE = 4 * 1024; + private final static int SOCKET_BUFFER = 4 * 1024; + + private final FragmentActivity activity; + private final AtomicBoolean active = new AtomicBoolean(); + private SocketChannel socket = null; + + private final BlockingQueue outputQueue = new ArrayBlockingQueue<>(QUEUE_SIZE, true); + private final BlockingQueue inputQueue = new ArrayBlockingQueue<>(QUEUE_SIZE, true); + + private byte[] packetJoinBuffer = null; + private int messageId = 0; + + public MessageChannel(FragmentActivity activity) + { + this.activity = activity; + StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.setThreadPolicy(policy); + } + + public void stop() + { + synchronized (active) + { + active.set(false); + } + inputQueue.add(new OperationCommandMsg(OperationCommandMsg.Command.DOWN)); + } + + public BlockingQueue getInputQueue() + { + return inputQueue; + } + + @Override + protected Void doInBackground(Void... params) + { + Logging.info(this, "started"); + + ByteBuffer buffer = ByteBuffer.allocate(SOCKET_BUFFER); + while (true) + { + try + { + synchronized (active) + { + if (!active.get() || isCancelled()) + { + Logging.info(this, "cancelled"); + break; + } + } + + // process input messages + buffer.clear(); + int readedSize = socket.read(buffer); + if (readedSize < 0) + { + Logging.info(this, "server disconnected"); + break; + } + else if (readedSize > 0) + { + processInputData(buffer); + } + + // process output messages + EISCPMessage m = outputQueue.poll(); + if (m != null) + { + final byte[] bytes = m.getBytes(); + if (bytes != null) + { + final ByteBuffer messageBuffer = ByteBuffer.wrap(m.getBytes()); + Logging.info(this, ">> sending: " + m.toString()); + socket.write(messageBuffer); + } + } + } + catch (Exception e) + { + Logging.info(this, "interrupted: " + e.getLocalizedMessage()); + break; + } + } + + try + { + socket.close(); + } + catch (IOException e) + { + // nothing to do + } + synchronized (active) + { + active.set(false); + } + Logging.info(this, "stopped"); + return null; + } + + public boolean connectToServer(String server, int port) + { + final String addr = server + ":" + Integer.toString(port); + try + { + socket = SocketChannel.open(); + socket.configureBlocking(false); + socket.connect(new InetSocketAddress(server, port)); + final long startTime = Calendar.getInstance().getTimeInMillis(); + while (!socket.finishConnect()) + { + final long currTime = Calendar.getInstance().getTimeInMillis(); + if (currTime > startTime + CONNECTION_TIMEOUT) + { + throw new Exception(activity.getResources().getString(R.string.error_connection_timeout)); + } + } + Logging.info(this, "connected to " + addr); + active.set(true); + } + catch (Exception e) + { + String message = String.format(activity.getResources().getString(R.string.error_connection_failed), addr); + Logging.info(this, message + ": " + e.getLocalizedMessage()); + Toast.makeText(activity, message, Toast.LENGTH_LONG).show(); + for (StackTraceElement t : e.getStackTrace()) + { + Logging.info(this, t.toString()); + } + active.set(false); + } + return active.get(); + } + + private void processInputData(ByteBuffer buffer) + { + buffer.flip(); + byte[] bytes; + + if (packetJoinBuffer == null) + { + // A new buffer - just copy it + bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + } + else + { + // Remaining part of existing buffer - join it + final int s1 = packetJoinBuffer.length; + final int s2 = buffer.remaining(); + bytes = new byte[s1 + s2]; + System.arraycopy(packetJoinBuffer, 0, bytes, 0, s1); + buffer.get(bytes, s1, s2); + packetJoinBuffer = null; + } + + int remaining = bytes.length; + while (remaining > 0) + { + EISCPMessage raw = null; + try + { + final int startIndex = EISCPMessage.getMsgStartIndex(bytes); + if (startIndex != 0) + { + Logging.info(this, "unexpected position of start index: " + startIndex); + } + + final int hSize = EISCPMessage.getHeaderSize(bytes, startIndex); + final int dSize = EISCPMessage.getDataSize(bytes, startIndex); + final int expectedSize = hSize + dSize; + if (expectedSize <= bytes.length) + { + messageId++; + raw = new EISCPMessage(messageId, bytes, startIndex, hSize, dSize); + remaining = Math.max(0, bytes.length - raw.getMsgSize()); + inputQueue.add(MessageFactory.create(raw)); + } + else + { + packetJoinBuffer = bytes; + return; + } + } + catch (Exception e) + { + Logging.info(this, "Error: " + e.getLocalizedMessage() + ", message: " + + (raw != null ? raw.toString() : "null")); + break; + } + + if (remaining > 0) + { + bytes = Utils.catBuffer(bytes, bytes.length - remaining, remaining); + } + } + } + + @Override + protected void onProgressUpdate(Void... result) + { + // empty + } + + public void sendMessage(EISCPMessage eiscpMessage) + { + outputQueue.add(eiscpMessage); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/AlbumNameMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/AlbumNameMsg.java new file mode 100644 index 00000000..b9f0db90 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/AlbumNameMsg.java @@ -0,0 +1,23 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB Album Name (variable-length, 64 ASCII letters max) + */ +public class AlbumNameMsg extends ISCPMessage +{ + public final static String CODE = "NAL"; + + AlbumNameMsg(EISCPMessage raw) throws Exception + { + super(raw); + } + + @Override + public String toString() + { + return CODE + "[" + data + "]"; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/AmpOperationCommandMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/AmpOperationCommandMsg.java new file mode 100644 index 00000000..ac773475 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/AmpOperationCommandMsg.java @@ -0,0 +1,82 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.R; +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * Amplifier Operation Command + */ +public class AmpOperationCommandMsg extends ISCPMessage +{ + public final static String CODE = "CAP"; + + public enum Command implements StringParameterIf + { + MVLUP(R.string.amp_cmd_volume_up, R.drawable.volume_up), + MVLDOWN(R.string.amp_cmd_volume_down, R.drawable.volume_down), + SLIUP(R.string.amp_cmd_selector_up), + SLIDOWN(R.string.amp_cmd_selector_down), + AMTON(R.string.amp_cmd_audio_mut_off), + AMTOFF(R.string.amp_cmd_audio_mut_on), + AMTTG(R.string.amp_cmd_audio_mut_toggle, R.drawable.volume_mute), + PWRON(R.string.amp_cmd_system_on), + PWROFF(R.string.amp_cmd_system_standby), + PWRTG(R.string.amp_cmd_system_on_toggle); + + final int descriptionId; + final int imageId; + + Command(final int descriptionId) + { + this.descriptionId = descriptionId; + this.imageId = -1; + } + + Command(final int descriptionId, final int imageId) + { + this.descriptionId = descriptionId; + this.imageId = imageId; + } + + public String getCode() + { + return toString(); + } + + public int getDescriptionId() + { + return descriptionId; + } + + public int getImageId() + { + return imageId; + } + } + + private final Command command; + + public AmpOperationCommandMsg(final String command) + { + super(0, null); + this.command = (Command) OperationCommandMsg.searchParameter(command, Command.values(), null); + } + + public Command getCommand() + { + return command; + } + + @Override + public String toString() + { + return CODE + "[" + (command == null ? "null" : command.toString()) + "]"; + } + + @Override + public EISCPMessage getCmdMsg() + { + return command == null ? null : new EISCPMessage('1', CODE, command.getCode()); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/ArtistNameMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/ArtistNameMsg.java new file mode 100644 index 00000000..3951321a --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/ArtistNameMsg.java @@ -0,0 +1,23 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB Artist Name (variable-length, 64 ASCII letters max) + */ +public class ArtistNameMsg extends ISCPMessage +{ + public final static String CODE = "NAT"; + + ArtistNameMsg(EISCPMessage raw) throws Exception + { + super(raw); + } + + @Override + public String toString() + { + return CODE + "[" + data + "]"; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/DeviceNameMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/DeviceNameMsg.java new file mode 100644 index 00000000..84a59524 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/DeviceNameMsg.java @@ -0,0 +1,23 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB Device Name + */ +class DeviceNameMsg extends ISCPMessage +{ + public final static String CODE = "NDN"; + + DeviceNameMsg(EISCPMessage raw) throws Exception + { + super(raw); + } + + @Override + public String toString() + { + return CODE + "[" + data + "]"; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/FileFormatMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/FileFormatMsg.java new file mode 100644 index 00000000..5d60001c --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/FileFormatMsg.java @@ -0,0 +1,39 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB File Info (variable-length, 64 ASCII letters max) + */ +public class FileFormatMsg extends ISCPMessage +{ + public final static String CODE = "NFI"; + + private final String format, sampleFrequency, bitRate; + + FileFormatMsg(EISCPMessage raw) throws Exception + { + super(raw); + final String[] pars = data.split(PAR_SEP); + format = pars.length > 0 ? pars[0] : ""; + sampleFrequency = pars.length > 1 ? pars[1] : ""; + bitRate = pars.length > 2 ? pars[2] : ""; + } + + public String getFullFormat() + { + return format + "/" + sampleFrequency + "/" + bitRate; + } + + @Override + public String toString() + { + return CODE + "[" + data + + "; FORMAT=" + format + + "; FREQUENCY=" + sampleFrequency + + "; BITRATE=" + bitRate + + "]"; + } + +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/InputSelectorMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/InputSelectorMsg.java new file mode 100644 index 00000000..5bcd3256 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/InputSelectorMsg.java @@ -0,0 +1,116 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.R; +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * Input Selector Command + */ +public class InputSelectorMsg extends ISCPMessage +{ + public final static String CODE = "SLI"; + + public enum InputType implements StringParameterIf + { + PC("05"), + VIDEO7("06"), + BD_DVD("10"), + STRM_BOX("11"), + TV("12"), + TV_TAPE("20"), + TAPE2("21"), + PHONO("22"), + TV_CD("23"), + FM("24"), + AM("25"), + TUNER("26"), + MUSIC_SERVER("27"), + INTERNET_RADIO("28"), + USB_FRONT("29", R.string.selector_usb_front, R.drawable.selector_usb_front), + USB_REAR("2A", R.string.selector_usb_rear, R.drawable.selector_usb_rear), + NET("2B", R.string.selector_net, R.drawable.selector_net), + USB_TOGGLE("2C"), + AIRPLAY("2D"), + BLUETOOTH("2E"), + USB_DAC_IN("2F"), + LINE("41"), + LINE2("42"), + OPTICAL("44"), + COAXIAL("45"), + UNIVERSAL_PORT("40"), + MULTI_CH("30"), + XM_1("31"), + SIRIUS_1("32"), + DAB_5("33"), + HDMI_5("55"), + HDMI_6("56"), + HDMI_7("57"), + NONE("XX"); + + final String code; + final int descriptionId; + final int imageId; + + InputType(String code) + { + this.code = code; + this.descriptionId = -1; + this.imageId = R.drawable.media_item_unknown; + } + + InputType(String code, final int descriptionId, int imageId) + { + this.code = code; + this.descriptionId = descriptionId; + this.imageId = imageId; + } + + public String getCode() + { + return code; + } + + public int getDescriptionId() + { + return descriptionId; + } + + public int getImageId() + { + return imageId; + } + } + + private final InputType inputType; + + InputSelectorMsg(EISCPMessage raw) throws Exception + { + super(raw); + inputType = (InputType) searchParameter(data, InputType.values(), InputType.NONE); + } + + public InputSelectorMsg(final String cmd) + { + super(0, null); + inputType = (InputType) searchParameter(cmd, InputType.values(), InputType.NONE); + } + + public InputType getInputType() + { + return inputType; + } + + @Override + public String toString() + { + return CODE + "[" + inputType.toString() + "; CODE=" + inputType.getCode() + "]"; + } + + @Override + public EISCPMessage getCmdMsg() + { + return new EISCPMessage('1', CODE, inputType.getCode()); + } + +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/JacketArtMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/JacketArtMsg.java new file mode 100644 index 00000000..e5d474b0 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/JacketArtMsg.java @@ -0,0 +1,198 @@ +package com.mkulesh.onpc.iscp.messages; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; +import com.mkulesh.onpc.utils.Logging; +import com.mkulesh.onpc.utils.Utils; + +import java.io.ByteArrayOutputStream; +import java.net.URL; + +/* + * NET/USB Jacket Art (When Jacket Art is available and Output for Network Control Only) + */ +public class JacketArtMsg extends ISCPMessage +{ + public final static String CODE = "NJA"; + + /* + * Image type 0:BMP, 1:JPEG, 2:URL, n:No Image + */ + public enum ImageType implements CharParameterIf + { + BMP('0'), JPEG('1'), URL('2'), NO_IMAGE('n'); + final Character code; + + ImageType(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private ImageType imageType = ImageType.NO_IMAGE; + + /* + * Packet flag 0:Start, 1:Next, 2:End, -:not used + */ + public enum PacketFlag implements CharParameterIf + { + START('0'), NEXT('1'), END('2'), NOT_USED('-'); + final Character code; + + PacketFlag(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private PacketFlag packetFlag = PacketFlag.NOT_USED; + + private URL url = null; + private byte[] rawData = null; + + JacketArtMsg(EISCPMessage raw) throws Exception + { + super(raw); + if (data.length() > 0) + { + imageType = (ImageType) searchParameter(data.charAt(0), ImageType.values(), imageType); + } + if (data.length() > 1) + { + packetFlag = (PacketFlag) searchParameter(data.charAt(1), PacketFlag.values(), packetFlag); + } + if (data.length() > 2) + { + switch (imageType) + { + case URL: + url = new URL(data.substring(2)); + break; + case BMP: + case JPEG: + rawData = convertRaw(data.substring(2)); + break; + case NO_IMAGE: + // nothing to do; + break; + } + + } + } + + public ImageType getImageType() + { + return imageType; + } + + public PacketFlag getPacketFlag() + { + return packetFlag; + } + + public byte[] getRawData() + { + return rawData; + } + + @Override + public String toString() + { + return CODE + "/" + Integer.toString(messageId) + "[" + data.substring(0, 2) + "..." + + "; TYPE=" + imageType.toString() + + "; PACKET=" + packetFlag.toString() + + "; URL=" + url + + "; RAW(" + (rawData == null ? "null" : rawData.length) + ")" + + "]"; + } + + private byte[] convertRaw(String str) + { + byte[] bytes = new byte[str.length() / 2]; + for (int i = 0; i < bytes.length; i++) + { + final int j1 = 2 * i; + final int j2 = 2 * i + 1; + if (j1 < str.length() && j2 < str.length()) + { + bytes[i] = (byte) Integer.parseInt(str.substring(j1, j2 + 1), 16); + } + } + return bytes; + } + + public Bitmap loadFromUrl() + { + Bitmap cover = null; + try + { + Logging.info(this, "loading image from URL: " + url.toString()); + + byte[] bytes = Utils.streamToByteArray(url.openConnection().getInputStream()); + int offset = 1; + for (; offset < bytes.length; offset++) + { + if (bytes[offset] == 0x0A && bytes[offset - 1] == 0x0A) + { + break; + } + } + offset++; + final int length = bytes.length - offset; + if (length > 0) + { + Logging.info(this, "Cover image size length=" + length); + cover = BitmapFactory.decodeByteArray(bytes, offset, length); + } + } + catch (Exception e) + { + Logging.info(this, "can not open image: " + e.getLocalizedMessage()); + } + if (cover == null) + { + Logging.info(this, "can not open image"); + } + return cover; + } + + public Bitmap loadFromBuffer(ByteArrayOutputStream coverBuffer) + { + if (coverBuffer == null) + { + Logging.info(this, "can not open image: empty stream"); + return null; + } + Bitmap cover = null; + try + { + Logging.info(this, "loading image from stream"); + coverBuffer.flush(); + coverBuffer.close(); + final byte out[] = coverBuffer.toByteArray(); + cover = BitmapFactory.decodeByteArray(out, 0, out.length); + } + catch (Exception e) + { + Logging.info(this, "can not open image: " + e.getLocalizedMessage()); + } + if (cover == null) + { + Logging.info(this, "can not open image"); + } + return cover; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/ListInfoMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/ListInfoMsg.java new file mode 100644 index 00000000..40b53307 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/ListInfoMsg.java @@ -0,0 +1,136 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB List Info + */ +public class ListInfoMsg extends ISCPMessage +{ + public final static String CODE = "NLS"; + + /* + * Information Type (A : ASCII letter, C : Cursor Info, U : Unicode letter) + */ + public enum InformationType implements CharParameterIf + { + ASCII('A'), CURSOR('C'), UNICODE('U'); + final Character code; + + InformationType(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private InformationType informationType = InformationType.CURSOR; + + /* Line Info (0-9 : 1st to 10th Line) */ + private int lineInfo; + + /* + * Property + * - : no + * 0 : Playing, A : Artist, B : Album, F : Folder, M : Music, P : Playlist, S : Search + * a : Account, b : Playlist-C, c : Starred, d : Unstarred, e : What's New + */ + private enum Property implements CharParameterIf + { + NO('-'), + PLAYING('0'), + ARTIST('A'), + ALBUM('B'), + FOLDER('F'), + MUSIC('M'), + PLAYLIST('P'), + SEARCH('S'), + ACCOUNT('A'), + PLAYLIST_C('B'), + STARRED('C'), + UNSTARRED('D'), + WHATS_NEW('E'); + final Character code; + + Property(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private Property property = Property.NO; + + /* + * Update Type (P : Page Infomation Update ( Page Clear or Disable List Info) , C : Cursor Position Update) + */ + private enum UpdateType implements CharParameterIf + { + NO('-'), PAGE('P'), CURSOR('C'); + final Character code; + + UpdateType(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private UpdateType updateType = UpdateType.NO; + + private String listedData = null; + + ListInfoMsg(EISCPMessage raw) throws Exception + { + super(raw); + informationType = (InformationType) searchParameter(data.charAt(0), InformationType.values(), informationType); + final char lineInfoChar = data.charAt(1); + lineInfo = Character.isDigit(lineInfoChar) ? Integer.parseInt(String.valueOf(lineInfoChar)) : -1; + switch (informationType) + { + case ASCII: + case UNICODE: + property = (Property) searchParameter(data.charAt(2), Property.values(), property); + listedData = data.substring(3); + break; + case CURSOR: + updateType = (UpdateType) searchParameter(data.charAt(2), UpdateType.values(), updateType); + break; + } + } + + public InformationType getInformationType() + { + return informationType; + } + + public String getListedData() + { + return listedData; + } + + @Override + public String toString() + { + return CODE + "[" + data + + "; INF_TYPE=" + informationType.toString() + + "; LINE_INFO=" + Integer.toString(lineInfo) + + "; PROPERTY=" + property.toString() + + "; UPD_TYPE=" + updateType.toString() + + "; LIST_DATA=" + listedData + + "]"; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/ListItemInfoMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/ListItemInfoMsg.java new file mode 100644 index 00000000..f5d4de99 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/ListItemInfoMsg.java @@ -0,0 +1,27 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB List Info (Update item, need processing XML data, for Network Control Only) + */ +public class ListItemInfoMsg extends ISCPMessage +{ + public final static String CODE = "NLU"; + + private final int index, number; + + ListItemInfoMsg(EISCPMessage raw) throws Exception + { + super(raw); + index = Integer.parseInt(data.substring(0, 4), 16); + number = Integer.parseInt(data.substring(4, 8), 16); + } + + @Override + public String toString() + { + return CODE + "[" + data + "; " + index + "/" + number + "]"; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/ListTitleInfoMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/ListTitleInfoMsg.java new file mode 100644 index 00000000..e2a32113 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/ListTitleInfoMsg.java @@ -0,0 +1,361 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB List Title Info + */ +public class ListTitleInfoMsg extends ISCPMessage +{ + public final static String CODE = "NLT"; + + /* + * Service Type + * 00 : Music Server (DLNA), 01 : Favorite, 02 : vTuner, 03 : SiriusXM, 04 : Pandora, 05 : Rhapsody, 06 : Last.fm, + * 07 : Napster, 08 : Slacker, 09 : Mediafly, 0A : Spotify, 0B : AUPEO!, 0C : radiko, 0D : e-onkyo, + * 0E : TuneIn Radio, 0F : MP3tunes, 10 : Simfy, 11:Home Media, 12:Deezer, 13:iHeartRadio, 18:Airplay, 1A:onkyo music, 1B:TIDAL, 41:FireConnect, + * F0 : USB/USB(Front) F1 : USB(Rear), F2 : Internet Radio, F3 : NET, FF : None + */ + public enum ServiceType implements StringParameterIf + { + MUSIC_SERVER("00"), + FAVORITE("01"), + VTUNER("02"), + SIRIUSXM("03"), + PANDORA("04"), + RHAPSODY("05"), + LAST_FM("06"), + NAPSTER("07"), + SLACKER("08"), + MEDIAFLY("09"), + SPOTIFY("0A"), + AUPEO("0B"), + RADIKO("0C"), + E_ONKYO("0D"), + TUNEIN_RADIO("0E"), + MP3TUNES("0F"), + SIMFY("10"), + HOME_MEDIA("11"), + DEEZER("12"), + IHEARTRADIO("13"), + AIRPLAY("18"), + ONKYO_MUSIC("1A"), + TIDAL("1B"), + PLAYQUEUE("1D"), + FIRECONNECT("41"), + USB_REAR("F1"), + INTERNET_RADIO("F2"), + USB_FRONT("F0"), + NET("F3"), + NONE("FF"); + final String code; + + ServiceType(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } + } + + private ServiceType serviceType = ServiceType.NONE; + + /* + * UI Type 0 : List, 1 : Menu, 2 : Playback, 3 : Popup, 4 : Keyboard, "5" : Menu List + */ + public enum UIType implements CharParameterIf + { + LIST('0'), MENU('1'), PLAYBACK('2'), POPUP('3'), KEYBOARD('4'), MENU_LIST('5'); + final Character code; + + UIType(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private UIType uiType = UIType.LIST; + + /* + * Layer Info : 0 : NET TOP, 1 : Service Top,DLNA/USB/iPod Top, 2 : under 2nd Layer + */ + public enum LayerInfo implements CharParameterIf + { + NET_TOP('0'), SERVICE_TOP('1'), UNDER_2ND_LAYER('2'); + final Character code; + + LayerInfo(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private LayerInfo layerInfo = LayerInfo.NET_TOP; + + /* Current Cursor Position (HEX 4 letters) */ + private int currentCursorPosition = 0; + + /* Number of List Items (HEX 4 letters) */ + private int numberOfItems = 0; + + /* Number of Layer(HEX 2 letters) */ + private int numberOfLayers = 0; + + /* + * Start Flag : 0 : Not First, 1 : First + */ + private enum StartFlag implements CharParameterIf + { + NOT_FIRST('0'), FIRST('1'); + final Character code; + + StartFlag(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private StartFlag startFlag = StartFlag.FIRST; + + /* + * Icon on Left of Title Bar + * 00 : Internet Radio, 01 : Server, 02 : USB, 03 : iPod, 04 : DLNA, 05 : WiFi, 06 : Favorite + * 10 : Account(Spotify), 11 : Album(Spotify), 12 : Playlist(Spotify), 13 : Playlist-C(Spotify) + * 14 : Starred(Spotify), 15 : What's New(Spotify), 16 : Track(Spotify), 17 : Artist(Spotify) + * 18 : Play(Spotify), 19 : Search(Spotify), 1A : Folder(Spotify) + * FF : None + */ + private enum LeftIcon implements StringParameterIf + { + INTERNET_RADIO("00"), + SERVER("01"), + USB("02"), + IPOD("03"), + DLNA("04"), + WIFI("05"), + FAVORITE("06"), + ACCOUNT_SPOTIFY("10"), + ALBUM_SPOTIFY("11"), + PLAYLIST_SPOTIFY("12"), + PLAYLIST_C_SPOTIFY("13"), + STARRED_SPOTIFY("14"), + WHATS_NEW_SPOTIFY("15"), + TRACK_SPOTIFY("16"), + ARTIST_SPOTIFY("17"), + PLAY_SPOTIFY("18"), + SEARCH_SPOTIFY("19"), + FOLDER_SPOTIFY("1A"), + NONE("FF"); + final String code; + + LeftIcon(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } + } + + private LeftIcon leftIcon = LeftIcon.NONE; + + /* + * Icon on Right of Title Bar + * 00 : Music Server (DLNA), 01 : Favorite, 02 : vTuner, 03 : SiriusXM, 04 : Pandora, 05 : Rhapsody, 06 : Last.fm, + * 07 : Napster, 08 : Slacker, 09 : Mediafly, 0A : Spotify, 0B : AUPEO!, 0C : radiko, 0D : e-onkyo, + * 0E : TuneIn Radio, 0F : MP3tunes, 10 : Simfy, 11:Home Media, 12:Deezer, 13:iHeartRadio, + * 18 : Airplay, 1A:onkyo music, 1B:TIDAL, 41:FireConnect, + * F0 : USB/USB(Front), F1:USB(Rear), + * FF : None + */ + private enum RightIcon implements StringParameterIf + { + MUSIC_SERVER("00"), + FAVORITE("01"), + VTUNER("02"), + SIRIUSXM("03"), + PANDORA("04"), + RHAPSODY("05"), + LAST_FM("06"), + NAPSTER("07"), + SLACKER("08"), + MEDIAFLY("09"), + SPOTIFY("0A"), + AUPEO("0B"), + RADIKO("0C"), + E_ONKYO("0D"), + TUNEIN_RADIO("0E"), + MP3TUNES("0F"), + SIMFY("10"), + HOME_MEDIA("11"), + DEEZER("12"), + IHEARTRADIO("13"), + AIRPLAY("18"), + ONKYO_MUSIC("1A"), + TIDAL("1B"), + FIRECONNECT("41"), + USB_FRONT("F0"), + USB_REAR("F1"), + NONE("FF"); + final String code; + + RightIcon(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } + } + + private RightIcon rightIcon = RightIcon.NONE; + + /* + * ss : Status Info + * 00 : None, 01 : Connecting, 02 : Acquiring License, 03 : Buffering + * 04 : Cannot Play, 05 : Searching, 06 : Profile update, 07 : Operation disabled + * 08 : Server Start-up, 09 : Song rated as Favorite, 0A : Song banned from station, + * 0B : Authentication Failed, 0C : Spotify Paused(max 1 device), 0D : Track Not Available, 0E : Cannot Skip + */ + private enum StatusInfo implements StringParameterIf + { + NONE("00"), + CONNECTING("01"), + ACQUIRING_LICENSE("02"), + BUFFERING("03"), + CANNOT_PLAY("04"), + SEARCHING("05"), + PROFILE_UPDATE("06"), + OPERATION_DISABLED("07"), + SERVER_START_UP("08"), + SONG_RATED_AS_FAVORITE("09"), + SONG_BANNED_FROM_STATION("0A"), + AUTHENTICATION_FAILED("0B"), + SPOTIFY_PAUSED("0C"), + TRACK_NOT_AVAILABLE("0D"), + CANNOT_SKIP("0E"); + final String code; + + StatusInfo(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } + } + + private StatusInfo statusInfo = StatusInfo.NONE; + + /* Character of Title Bar (variable-length, 64 Unicode letters [UTF-8 encoded] max) */ + private String titleBar; + + ListTitleInfoMsg(EISCPMessage raw) throws Exception + { + super(raw); + + /* NET/USB List Title Info + xx : Service Type + u : UI Type + y : Layer Info + cccc : Current Cursor Position (HEX 4 letters) + iiii : Number of List Items (HEX 4 letters) + ll : Number of Layer(HEX 2 letters) + s : Start Flag + r : Reserved (1 leters, don't care) + aa : Icon on Left of Title Bar + bb : Icon on Right of Title Bar + ss : Status Info + nnn...nnn : Character of Title Bar (variable-length, 64 Unicode letters [UTF-8 encoded] max) + */ + final String format = "xxuycccciiiillsraabbss"; + + if (data.length() >= format.length()) + { + serviceType = (ServiceType) searchParameter(data.substring(0, 2), ServiceType.values(), serviceType); + uiType = (UIType) searchParameter(data.charAt(2), UIType.values(), uiType); + layerInfo = (LayerInfo) searchParameter(data.charAt(3), LayerInfo.values(), layerInfo); + currentCursorPosition = Integer.parseInt(data.substring(4, 8), 16); + numberOfItems = Integer.parseInt(data.substring(8, 12), 16); + numberOfLayers = Integer.parseInt(data.substring(12, 14), 16); + startFlag = (StartFlag) searchParameter(data.charAt(14), StartFlag.values(), startFlag); + leftIcon = (LeftIcon) searchParameter(data.substring(16, 18), LeftIcon.values(), leftIcon); + rightIcon = (RightIcon) searchParameter(data.substring(18, 20), RightIcon.values(), rightIcon); + statusInfo = (StatusInfo) searchParameter(data.substring(20, 22), StatusInfo.values(), statusInfo); + titleBar = data.substring(22); + } + } + + public ServiceType getServiceType() + { + return serviceType; + } + + public UIType getUiType() + { + return uiType; + } + + public LayerInfo getLayerInfo() + { + return layerInfo; + } + + public int getNumberOfItems() + { + return numberOfItems; + } + + public int getNumberOfLayers() + { + return numberOfLayers; + } + + public String getTitleBar() + { + return titleBar; + } + + @Override + public String toString() + { + return CODE + "[" + data + + "; SERVICE=" + serviceType.toString() + + "; UI=" + uiType.toString() + + "; LAYER=" + layerInfo + + "; CURSOR=" + currentCursorPosition + + "; ITEMS=" + numberOfItems + + "; LAYERS=" + numberOfLayers + + "; START=" + startFlag.toString() + + "; LEFT_ICON=" + leftIcon.toString() + + "; RIGHT_ICON=" + rightIcon.toString() + + "; STATUS=" + statusInfo.toString() + + "; title=" + titleBar + + "]"; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/MenuStatusMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/MenuStatusMsg.java new file mode 100644 index 00000000..29b3770c --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/MenuStatusMsg.java @@ -0,0 +1,211 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB Menu Status + */ +public class MenuStatusMsg extends ISCPMessage +{ + public final static String CODE = "NMS"; + + /* + * Track Menu: "M": Menu is enable, "x": Menu is disable + */ + private enum TrackMenu implements CharParameterIf + { + ENABLE('M'), DISABLE('x'); + final Character code; + + TrackMenu(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private TrackMenu trackMenu = TrackMenu.DISABLE; + + /* + * Feed: "xx":disable, "01":Like, "02":don't like, "03":Love, "04":Ban, + * "05":episode, "06":ratings, "07":Ban(black), "08":Ban(white), + * "09":Favorite(black), "0A":Favorite(white), "0B":Favorite(yellow) + */ + private enum Feed implements StringParameterIf + { + DISABLE("XX"), + LIKE("01"), + DONT_LIKE("02"), + LOVE("03"), + BAN("04"), + EPISODE("05"), + RATINGS("06"), + BAN_BLACK("07"), + BAN_WHITE("08"), + FAVORITE_BLACK("09"), + FAVORITE_WHITE("0A"), + FAVORITE_YELLOW("0B"); + final String code; + + Feed(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } + } + + private Feed positiveFeed = Feed.DISABLE; + private Feed negativeFeed = Feed.DISABLE; + + /* + * Time Seek "S": Time Seek is enable "x": Time Seek is disable + */ + public enum TimeSeek implements CharParameterIf + { + ENABLE('S'), DISABLE('x'); + final Character code; + + TimeSeek(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private TimeSeek timeSeek = TimeSeek.DISABLE; + + /* + * Time Display "1": Elapsed Time/Total Time, "2": Elapsed Time, "x": disable + */ + private enum TimeDisplay implements CharParameterIf + { + ELAPSED_TOTAL('1'), ELAPSED('2'), DISABLE('x'); + final Character code; + + TimeDisplay(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private TimeDisplay timeDisplay = TimeDisplay.DISABLE; + + /* + * Service icon + * "00":Music Server (DLNA), "01":My Favorite, "02":vTuner, + * "03":SiriusXM, "04":Pandora, + * "05":Rhapsody, "06":Last.fm, "07":Napster, "08":Slacker, "09":Mediafly, + * "0A":Spotify, "0B":AUPEO!, + * "0C":radiko, "0D":e-onkyo, "0E":TuneIn, "0F":MP3tunes, "10":Simfy, + * "11":Home Media, "12":Deezer, "13":iHeartRadio, "18":Airplay, + * “1A”: onkyo Music, “1B”:TIDAL, “41”:FireConnect, + * "F0": USB/USB(Front), "F1: USB(Rear), "F2":Internet Radio + * "F3":NET, "F4":Bluetooth + */ + private enum ServiceIcon implements StringParameterIf + { + MUSIC_SERVER("00"), + MY_FAVORITE("01"), + VTUNER("02"), + SIRIUSXM("03"), + PANDORA("04"), + RHAPSODY("05"), + LAST_FM("06"), + NAPSTER("07"), + SLACKER("08"), + MEDIAFLY("09"), + SPOTIFY("0A"), + AUPEO("0B"), + RADIKO("0C"), + E_ONKYO("0D"), + TUNEIN("0E"), + MP3TUNES("0F"), + SIMFY("10"), + HOME_MEDIA("11"), + DEEZER("12"), + IHEARTRADIO("13"), + AIRPLAY("18"), + ONKYO_MUSIC("1A"), + TIDAL("1B"), + FIRECONNECT("41"), + USB_FRONT("F0"), + USB_REAR("F1"), + INTERNET_RADIO("F2"), + NET("F3"), + BLUETOOTH("F4"), + NONE("XX"); + final String code; + + ServiceIcon(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } + } + + private ServiceIcon serviceIcon = ServiceIcon.NONE; + + MenuStatusMsg(EISCPMessage raw) throws Exception + { + super(raw); + + /* NET/USB Menu Status (9 letters) + m -> Track Menu: "M": Menu is enable, "x": Menu is disable + aa -> F1 button icon (Positive Feed or Mark/Unmark) + bb -> F2 button icon (Negative Feed) + s -> Time Seek "S": Time Seek is enable "x": Time Seek is disable + t -> Time Display "1": Elapsed Time/Total Time, "2": Elapsed Time, "x": disable + ii-> Service icon + */ + final String format = "maabbstii"; + if (data.length() >= format.length()) + { + trackMenu = (TrackMenu) searchParameter(data.charAt(0), TrackMenu.values(), trackMenu); + positiveFeed = (Feed) searchParameter(data.substring(1, 3), Feed.values(), positiveFeed); + negativeFeed = (Feed) searchParameter(data.substring(3, 5), Feed.values(), negativeFeed); + timeSeek = (TimeSeek) searchParameter(data.charAt(5), TimeSeek.values(), timeSeek); + timeDisplay = (TimeDisplay) searchParameter(data.charAt(6), TimeDisplay.values(), timeDisplay); + serviceIcon = (ServiceIcon) searchParameter(data.substring(7, 9), ServiceIcon.values(), serviceIcon); + } + } + + public TimeSeek getTimeSeek() + { + return timeSeek; + } + + @Override + public String toString() + { + return CODE + "[" + data + + "; TRACK_MENU=" + trackMenu.toString() + + "; POS_FEED=" + positiveFeed.toString() + + "; NEG_FEED=" + negativeFeed.toString() + + "; TIME_SEEK=" + timeSeek.toString() + + "; TIME_DISPLAY=" + timeDisplay.toString() + + "; ICON=" + serviceIcon.toString() + + "]"; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/MessageFactory.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/MessageFactory.java new file mode 100644 index 00000000..77ced869 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/MessageFactory.java @@ -0,0 +1,53 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/** + * A static helper class used to create messages + */ +public class MessageFactory +{ + public static ISCPMessage create(EISCPMessage raw) throws Exception + { + switch (raw.getCode().toUpperCase()) + { + case PowerStatusMsg.CODE: + return new PowerStatusMsg(raw); + case ReceiverInformationMsg.CODE: + return new ReceiverInformationMsg(raw); + case DeviceNameMsg.CODE: + return new DeviceNameMsg(raw); + case InputSelectorMsg.CODE: + return new InputSelectorMsg(raw); + case TimeInfoMsg.CODE: + return new TimeInfoMsg(raw); + case JacketArtMsg.CODE: + return new JacketArtMsg(raw); + case TitleNameMsg.CODE: + return new TitleNameMsg(raw); + case AlbumNameMsg.CODE: + return new AlbumNameMsg(raw); + case ArtistNameMsg.CODE: + return new ArtistNameMsg(raw); + case FileFormatMsg.CODE: + return new FileFormatMsg(raw); + case TrackInfoMsg.CODE: + return new TrackInfoMsg(raw); + case PlayStatusMsg.CODE: + return new PlayStatusMsg(raw); + case ListTitleInfoMsg.CODE: + return new ListTitleInfoMsg(raw); + case ListInfoMsg.CODE: + return new ListInfoMsg(raw); + case ListItemInfoMsg.CODE: + return new ListItemInfoMsg(raw); + case MenuStatusMsg.CODE: + return new MenuStatusMsg(raw); + case XmlListInfoMsg.CODE: + return new XmlListInfoMsg(raw); + default: + throw new Exception("No factory method for message " + raw.getCode()); + } + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/NetworkServiceMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/NetworkServiceMsg.java new file mode 100644 index 00000000..78c25e79 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/NetworkServiceMsg.java @@ -0,0 +1,117 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.R; +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * Select Network Service directly only when NET selector is selected. + */ +public class NetworkServiceMsg extends ISCPMessage +{ + public final static String CODE = "NSV"; + + public enum Service implements StringParameterIf + { + UNKNOWN("", "", -1), + MUSIC_SERVER("Music Server", "00", R.string.net_service_music_server), + FAVORITE("Favorite", "01", R.string.net_service_favorite), + VTUNER("vTuner", "02", R.string.net_service_vtuner), + SIRIUSXM("SiriusXM", "03", R.string.net_service_siriusxm), + PANDORA("Pandora", "04", R.string.net_service_pandora), + RHAPSODY("Rhapsody", "05", R.string.net_service_rhapsody), + LAST_FM("Last.fm", "06", R.string.net_service_last), + NAPSTER("Napster", "07", R.string.net_service_napster), + SLACKER("Slacker", "08", R.string.net_service_slacker), + MEDIAFLY("Mediafly", "09", R.string.net_service_mediafly), + SPOTIFY("Spotify", "0A", R.string.net_service_spotify), + AUPEO("AUPEO!", "0B", R.string.net_service_aupeo), + RADIKO("Radiko", "0C", R.string.net_service_radiko), + E_ONKYO("e-onkyo", "0D", R.string.net_service_e_onkyo), + TUNEIN_RADIO("TuneIn", "0E", R.string.net_service_tunein_radio), + MP3TUNES("mp3tunes", "0F", R.string.net_service_mp3tunes), + SIMFY("Simfy", "10", R.string.net_service_simfy), + HOME_MEDIA("Home Media", "11", R.string.net_service_home_media), + DEEZER("Deezer", "12", R.string.net_service_deezer), + IHEARTRADIO("iHeartRadio", "13", R.string.net_service_iheartradio), + AIRPLAY("Airplay", "18", R.string.net_service_airplay), + ONKYO_MUSIC("onkyo music", "1A", R.string.net_service_onkyo_music), + TIDAL("Tidal", "1B", R.string.net_service_tidal), + PLAYQUEUE("Play Queue", "1D", R.string.net_service_playqueue), + CHROMECAST("Chromecast built-in", "40", R.string.net_service_chromecast), + FLARECONNECT("FlareConnect", "43", R.string.net_service_flareconnect), + PLAY_FI("Play-Fi", "42", R.string.net_service_play_fi); + + final String code; + final String id; + final int descriptionId; + final int imageId; + + Service(final String code, final String id, final int descriptionId) + { + this.code = code; + this.id = id; + this.descriptionId = descriptionId; + this.imageId = R.drawable.media_item_unknown; + } + + public String getCode() + { + return code; + } + + public String getId() + { + return id; + } + + public int getDescriptionId() + { + return descriptionId; + } + + public int getImageId() + { + return imageId; + } + + public boolean isImageValid() + { + return imageId != -1; + } + } + + private final Service service; + + public NetworkServiceMsg(final String code) + { + super(0, null); + this.service = (Service) searchParameter(code, Service.values(), Service.UNKNOWN); + } + + public NetworkServiceMsg(NetworkServiceMsg other) + { + super(other); + service = other.service; + } + + public Service getService() + { + return service; + } + + @Override + public String toString() + { + return CODE + "[" + service.toString() + "/" + service.getId() + "]"; + } + + @Override + public EISCPMessage getCmdMsg() + { + final String param = service.getId() + "0"; + return new EISCPMessage('1', CODE, param); + } +} + + diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/OperationCommandMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/OperationCommandMsg.java new file mode 100644 index 00000000..c2e528a8 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/OperationCommandMsg.java @@ -0,0 +1,132 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.R; +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * Network/USB Operation Command (Network Model Only after TX-NR905) + */ +public class OperationCommandMsg extends ISCPMessage +{ + public final static String CODE = "NTC"; + + public enum Command implements StringParameterIf + { + PLAY("PLAY", R.string.cmd_description_play, R.drawable.cmd_play), + STOP("STOP", R.string.cmd_description_stop, R.drawable.cmd_stop), + PAUSE("PAUSE", R.string.cmd_description_pause, R.drawable.cmd_pause), + P_P("P/P", R.string.cmd_description_p_p), + TRUP("TRUP", R.string.cmd_description_trup, R.drawable.cmd_next), + TRDN("TRDN", R.string.cmd_description_trdn, R.drawable.cmd_previous), + FF("FF", R.string.cmd_description_ff), + REW("REW", R.string.cmd_description_rew), + REPEAT("REPEAT", R.string.cmd_description_repeat, R.drawable.cmd_repeat), + RANDOM("RANDOM", R.string.cmd_description_random, R.drawable.cmd_random), + REP_SHF("REP/SHF", R.string.cmd_description_rep_shf), + DISPLAY("DISPLAY", R.string.cmd_description_display), + ALBUM("ALBUM", R.string.cmd_description_album), + ARTIST("ARTIST", R.string.cmd_description_artist), + GENRE("GENRE", R.string.cmd_description_genre), + PLAYLIST("PLAYLIST", R.string.cmd_description_playlist), + RIGHT("RIGHT", R.string.cmd_description_right), + LEFT("LEFT", R.string.cmd_description_left), + UP("UP", R.string.cmd_description_up), + DOWN("DOWN", R.string.cmd_description_down), + SELECT("SELECT", R.string.cmd_description_select), + KEY_0("0", R.string.cmd_description_key_0), + KEY_1("1", R.string.cmd_description_key_1), + KEY_2("2", R.string.cmd_description_key_2), + KEY_3("3", R.string.cmd_description_key_3), + KEY_4("4", R.string.cmd_description_key_4), + KEY_5("5", R.string.cmd_description_key_5), + KEY_6("6", R.string.cmd_description_key_6), + KEY_7("7", R.string.cmd_description_key_7), + KEY_8("8", R.string.cmd_description_key_8), + KEY_9("9", R.string.cmd_description_key_9), + DELETE("DELETE", R.string.cmd_description_delete), + CAPS("CAPS", R.string.cmd_description_caps), + LOCATION("LOCATION", R.string.cmd_description_location), + LANGUAGE("LANGUAGE", R.string.cmd_description_language), + SETUP("SETUP", R.string.cmd_description_setup), + RETURN("RETURN", R.string.cmd_description_return, R.drawable.cmd_return), + CHUP("CHUP", R.string.cmd_description_chup), + CHDN("CHDN", R.string.cmd_description_chdn), + MENU("MENU", R.string.cmd_description_menu), + TOP("TOP", R.string.cmd_description_top), + MODE("MODE", R.string.cmd_description_mode), + LIST("LIST", R.string.cmd_description_list), + MEMORY("MEMORY", R.string.cmd_description_memory), + F1("F1", R.string.cmd_description_f1), + F2("F2", R.string.cmd_description_f2); + + final String code; + final int descriptionId; + final int imageId; + + Command(final String code, final int descriptionId) + { + this.code = code; + this.descriptionId = descriptionId; + this.imageId = -1; + } + + Command(final String code, final int descriptionId, final int imageId) + { + this.code = code; + this.descriptionId = descriptionId; + this.imageId = imageId; + } + + public String getCode() + { + return code; + } + + public int getDescriptionId() + { + return descriptionId; + } + + public int getImageId() + { + return imageId; + } + + public boolean isImageValid() + { + return imageId != -1; + } + } + + private final Command command; + + public OperationCommandMsg(final String command) + { + super(0, null); + this.command = (Command) OperationCommandMsg.searchParameter(command, Command.values(), null); + } + + public OperationCommandMsg(final Command command) + { + super(0, null); + this.command = command; + } + + public Command getCommand() + { + return command; + } + + @Override + public String toString() + { + return CODE + "[" + (command == null ? "null" : command.toString()) + "]"; + } + + @Override + public EISCPMessage getCmdMsg() + { + return command == null ? null : new EISCPMessage('1', CODE, command.getCode()); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayQueueAddMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayQueueAddMsg.java new file mode 100644 index 00000000..cfcffec1 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayQueueAddMsg.java @@ -0,0 +1,46 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * Add PlayQueue List in List View (from Network Control Only) + */ +public class PlayQueueAddMsg extends ISCPMessage +{ + public final static String CODE = "PQA"; + + // The Index number of the item to be added in the content list + // (0000-FFFF : 1st to 65536th Item [4 HEX digits] ) + // It is also possible to set folder. + private final int itemIndex; + + // Add Type: 0:Now, 1:Next, 2:Last + private final int type; + + // The Index number in the PlayQueue to be added(0000-FFFF : 1st to 65536th Item [4 HEX digits] ) + private final int targetIndex; + + public PlayQueueAddMsg(final int itemIndex, final int type) + { + super(0, null); + this.itemIndex = itemIndex; + this.type = type; + this.targetIndex = 0; + } + + @Override + public String toString() + { + return CODE + "[INDEX=" + Integer.toString(itemIndex) + "; TYPE=" + Integer.toString(type) + "]"; + } + + @Override + public EISCPMessage getCmdMsg() + { + final String param = String.format("%04x", itemIndex) + + Integer.toString(type) + + String.format("%04x", targetIndex); + return new EISCPMessage('1', CODE, param); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayQueueRemoveMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayQueueRemoveMsg.java new file mode 100644 index 00000000..5bbc5cee --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayQueueRemoveMsg.java @@ -0,0 +1,39 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * Remove from PlayQueue List (from Network Control Only) + */ +public class PlayQueueRemoveMsg extends ISCPMessage +{ + public final static String CODE = "PQR"; + + // Remove Type: 0:Specify Line, (1:ALL) + private final int type; + + // The Index number in the PlayQueue of the item to delete(0000-FFFF : 1st to 65536th Item [4 HEX digits] ) + private final int itemIndex; + + public PlayQueueRemoveMsg(final int type, final int itemIndex) + { + super(0, null); + this.type = type; + this.itemIndex = itemIndex; + } + + @Override + public String toString() + { + return CODE + "[TYPE=" + Integer.toString(type) + "; INDEX=" + Integer.toString(itemIndex) + "]"; + } + + @Override + public EISCPMessage getCmdMsg() + { + final String param = Integer.toString(type) + + String.format("%04x", itemIndex); + return new EISCPMessage('1', CODE, param); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayQueueReorderMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayQueueReorderMsg.java new file mode 100644 index 00000000..d182259e --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayQueueReorderMsg.java @@ -0,0 +1,41 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * Reorder PlayQueue List (from Network Control Only) + */ +public class PlayQueueReorderMsg extends ISCPMessage +{ + public final static String CODE = "PQO"; + + // The Index number in the PlayQueue of the item to be moved + // (0000-FFFF : 1st to 65536th Item [4 HEX digits] ) . + private final int itemIndex; + + // The Index number in the PlayQueue of destination. + // (0000-FFFF : 1st to 65536th Item [4 HEX digits] ) + private final int targetIndex; + + public PlayQueueReorderMsg(final int itemIndex, final int targetIndex) + { + super(0, null); + this.itemIndex = itemIndex; + this.targetIndex = targetIndex; + } + + @Override + public String toString() + { + return CODE + "[INDEX=" + Integer.toString(itemIndex) + "; TARGET=" + Integer.toString(targetIndex) + "]"; + } + + @Override + public EISCPMessage getCmdMsg() + { + final String param = String.format("%04x", itemIndex) + + String.format("%04x", targetIndex); + return new EISCPMessage('1', CODE, param); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayStatusMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayStatusMsg.java new file mode 100644 index 00000000..d5de6e28 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/PlayStatusMsg.java @@ -0,0 +1,117 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB Play Status (3 letters) + */ +public class PlayStatusMsg extends ISCPMessage +{ + public final static String CODE = "NST"; + + /* + * Play Status: "S": STOP, "P": Play, "p": Pause, "F": FF, "R": FR, "E": EOF + */ + public enum PlayStatus implements CharParameterIf + { + STOP('S'), PLAY('P'), PAUSE('p'), FF('F'), FR('R'), EOF('E'); + final Character code; + + PlayStatus(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private PlayStatus playStatus = PlayStatus.EOF; + + /* + * Repeat Status: "-": Off, "R": All, "F": Folder, "1": Repeat 1, "x": disable + */ + public enum RepeatStatus implements CharParameterIf + { + OFF('-'), ALL('R'), FOLDER('F'), REPEAT_1('1'), DISABLE('x'); + final Character code; + + RepeatStatus(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private RepeatStatus repeatStatus = RepeatStatus.DISABLE; + + /* + * Shuffle Status: "-": Off, "S": All , "A": Album, "F": Folder, "x": disable + */ + public enum ShuffleStatus implements CharParameterIf + { + OFF('-'), ALL('S'), ALBUM('A'), FOLDER('F'), DISABLE('x'); + final Character code; + + ShuffleStatus(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private ShuffleStatus shuffleStatus = ShuffleStatus.DISABLE; + + PlayStatusMsg(EISCPMessage raw) throws Exception + { + super(raw); + if (data.length() > 0) + { + playStatus = (PlayStatus) searchParameter(data.charAt(0), PlayStatus.values(), playStatus); + } + if (data.length() > 1) + { + repeatStatus = (RepeatStatus) searchParameter(data.charAt(1), RepeatStatus.values(), repeatStatus); + } + if (data.length() > 2) + { + shuffleStatus = (ShuffleStatus) searchParameter(data.charAt(2), ShuffleStatus.values(), shuffleStatus); + } + } + + public PlayStatus getPlayStatus() + { + return playStatus; + } + + public RepeatStatus getRepeatStatus() + { + return repeatStatus; + } + + public ShuffleStatus getShuffleStatus() + { + return shuffleStatus; + } + + @Override + public String toString() + { + return CODE + "[" + data + + "; PLAY=" + playStatus.toString() + + "; REPEAT=" + repeatStatus.toString() + + "; SHUFFLE=" + shuffleStatus.toString() + + "]"; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/PowerStatusMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/PowerStatusMsg.java new file mode 100644 index 00000000..c1d9b3ef --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/PowerStatusMsg.java @@ -0,0 +1,62 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * System Power Command + */ +public class PowerStatusMsg extends ISCPMessage +{ + public final static String CODE = "PWR"; + + /* + * Play Status: "00": System Standby, "01": System On, "ALL": All Zone(including Main Zone) Standby + */ + public enum PowerStatus implements StringParameterIf + { + STB("00"), ON("01"), ALL_STB("ALL"); + final String code; + + PowerStatus(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } + } + + private PowerStatus powerStatus = PowerStatus.STB; + + PowerStatusMsg(EISCPMessage raw) throws Exception + { + super(raw); + powerStatus = (PowerStatus) searchParameter(data, PowerStatus.values(), powerStatus); + } + + public PowerStatusMsg(PowerStatus powerStatus) + { + super(0, null); + this.powerStatus = powerStatus; + } + + public PowerStatus getPowerStatus() + { + return powerStatus; + } + + @Override + public String toString() + { + return CODE + "[" + data + "; PWR=" + powerStatus.toString() + "]"; + } + + @Override + public EISCPMessage getCmdMsg() + { + return new EISCPMessage('1', CODE, powerStatus.getCode()); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/ReceiverInformationMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/ReceiverInformationMsg.java new file mode 100644 index 00000000..fcfd4095 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/ReceiverInformationMsg.java @@ -0,0 +1,186 @@ +package com.mkulesh.onpc.iscp.messages; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; +import com.mkulesh.onpc.utils.Logging; +import com.mkulesh.onpc.utils.Utils; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +/* + * Gets the Receiver Information Status + */ +public class ReceiverInformationMsg extends ISCPMessage +{ + public final static String CODE = "NRI"; + + public static class Selector + { + final String id; + final String name; + final String iconId; + final boolean addToQueue; + + Selector(Element e) + { + id = e.getAttribute("id").toUpperCase(); + name = e.getAttribute("name"); + iconId = e.getAttribute("iconid"); + addToQueue = e.hasAttribute("addqueue") && (Integer.parseInt(e.getAttribute("addqueue")) == 1); + } + + public Selector(final String id, final String name, final String iconId, final boolean addToQueue) + { + this.id = id; + this.name = name; + this.iconId = iconId; + this.addToQueue = addToQueue; + } + + public String getId() + { + return id; + } + + public boolean isAddToQueue() + { + return addToQueue; + } + + @Override + public String toString() + { + return id + "(" + name + "): icon=" + iconId + ", addToQueue=" + addToQueue; + } + } + + + private String deviceId; + private final HashMap deviceProperties = new HashMap<>(); + private Bitmap deviceCover; + private final List deviceSelectors = new ArrayList<>(); + + ReceiverInformationMsg(EISCPMessage raw) throws Exception + { + super(raw); + deviceId = ""; + deviceCover = null; + } + + public Map getDeviceProperties() + { + return deviceProperties; + } + + public Bitmap getDeviceCover() + { + return deviceCover; + } + + public List getDeviceSelectors() + { + return deviceSelectors; + } + + @Override + public String toString() + { + return CODE + "[XML<" + Integer.toString(data.length()) + ">]"; + } + + public void parseXml() throws Exception + { + deviceProperties.clear(); + deviceSelectors.clear(); + InputStream stream = new ByteArrayInputStream(data.getBytes(Charset.forName("UTF-8"))); + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + final DocumentBuilder builder = factory.newDocumentBuilder(); + final Document doc = builder.parse(stream); + for (Node object = doc.getDocumentElement(); object != null; object = object.getNextSibling()) + { + if (object instanceof Element) + { + final Element response = (Element) object; + if (!response.getTagName().equals("response") || !Utils.ensureAttribute(response, "status", "ok")) + { + continue; + } + + final List device = Utils.getElements(response, "device"); + if (device.isEmpty()) + { + continue; + } + + // Only process the first "items" element + Element deviceInfo = device.get(0); + if (deviceInfo == null) + { + continue; + } + + deviceId = deviceInfo.getAttribute("id"); + Logging.info(this, " deviceId=" + deviceId); + + for (Node prop = deviceInfo.getFirstChild(); prop != null; prop = prop.getNextSibling()) + { + if (prop instanceof Element) + { + final Element en = (Element) prop; + if (en.getChildNodes().getLength() == 1) + { + deviceProperties.put(en.getTagName(), en.getChildNodes().item(0).getNodeValue()); + } + else if ("selectorlist".equals(en.getTagName())) + { + final List elSelectors = Utils.getElements(en, "selector"); + for (Element Element : elSelectors) + { + deviceSelectors.add(new Selector(Element)); + } + } + } + } + } + } + + for (Map.Entry p : deviceProperties.entrySet()) + { + Logging.info(this, " Property: " + p.getKey() + "=" + p.getValue()); + } + for (Selector s : deviceSelectors) + { + Logging.info(this, " Selector: " + s.toString()); + } + + if (deviceProperties.containsKey("modeliconurl")) + { + final URL url = new URL(deviceProperties.get("modeliconurl")); + Logging.info(this, "loading image from URL: " + url.toString()); + byte[] bytes = Utils.streamToByteArray(url.openConnection().getInputStream()); + deviceCover = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + if (deviceCover == null) + { + Logging.info(this, "can not decode image"); + } + } + stream.close(); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/TimeInfoMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/TimeInfoMsg.java new file mode 100644 index 00000000..66aafa2c --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/TimeInfoMsg.java @@ -0,0 +1,45 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB Time Info + */ +public class TimeInfoMsg extends ISCPMessage +{ + public final static String CODE = "NTM"; + + /* + * (Elapsed time/Track Time Max 99:59:59. If time is unknown, this response is --:--) + */ + private String currentTime, maxTime; + + TimeInfoMsg(EISCPMessage raw) throws Exception + { + super(raw); + final String[] pars = data.split(PAR_SEP); + if (pars.length != 2) + { + throw new Exception("Can not find parameter split character in message " + raw.toString()); + } + currentTime = pars[0]; + maxTime = pars[1]; + } + + public String getCurrentTime() + { + return currentTime; + } + + public String getMaxTime() + { + return maxTime; + } + + @Override + public String toString() + { + return CODE + "[" + currentTime + "; " + maxTime + "]"; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/TimeSeekMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/TimeSeekMsg.java new file mode 100644 index 00000000..d235d20d --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/TimeSeekMsg.java @@ -0,0 +1,46 @@ +package com.mkulesh.onpc.iscp.messages; + +import android.annotation.SuppressLint; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB Time Seek + */ +public class TimeSeekMsg extends ISCPMessage +{ + public final static String CODE = "NTS"; + + private final int hours, minutes, seconds; + + public TimeSeekMsg(final int hours, final int minutes, final int seconds) + { + super(0, null); + this.hours = hours; + this.minutes = minutes; + this.seconds = seconds; + } + + @Override + public String toString() + { + return CODE + "[" + Integer.toString(hours) + + ":" + Integer.toString(minutes) + + ":" + Integer.toString(seconds) + "]"; + } + + @SuppressLint("DefaultLocale") + public String getTimeAsString() + { + return String.format("%02d", hours) + + ":" + String.format("%02d", minutes) + + ":" + String.format("%02d", seconds); + } + + @Override + public EISCPMessage getCmdMsg() + { + return new EISCPMessage('1', CODE, getTimeAsString()); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/TitleNameMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/TitleNameMsg.java new file mode 100644 index 00000000..29bfb08d --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/TitleNameMsg.java @@ -0,0 +1,23 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB Title Name (variable-length, 64 ASCII letters max) + */ +public class TitleNameMsg extends ISCPMessage +{ + public final static String CODE = "NTI"; + + TitleNameMsg(EISCPMessage raw) throws Exception + { + super(raw); + } + + @Override + public String toString() + { + return CODE + "[" + data + "]"; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/TrackInfoMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/TrackInfoMsg.java new file mode 100644 index 00000000..127d0ebe --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/TrackInfoMsg.java @@ -0,0 +1,51 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; + +/* + * NET/USB Track Info + */ +public class TrackInfoMsg extends ISCPMessage +{ + public final static String CODE = "NTR"; + + /* + * (Current Track/Toral Track Max 9999. If Track is unknown, this response is ----) + */ + private String currentTrack, maxTrack; + + TrackInfoMsg(EISCPMessage raw) throws Exception + { + super(raw); + final String[] pars = data.split(PAR_SEP); + if (pars.length != 2) + { + throw new Exception("Can not find parameter split character in message " + raw.toString()); + } + currentTrack = pars[0]; + maxTrack = pars[1]; + } + + public String getCurrentTrack() + { + return currentTrack; + } + + public String getMaxTrack() + { + return maxTrack; + } + + public boolean isValidTrack() + { + return currentTrack != null && !currentTrack.equals("----"); + } + + @Override + public String toString() + { + return CODE + "[" + currentTrack + "; " + maxTrack + "]"; + } + +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/XmlListInfoMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/XmlListInfoMsg.java new file mode 100644 index 00000000..10f475b3 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/XmlListInfoMsg.java @@ -0,0 +1,129 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; +import com.mkulesh.onpc.utils.Utils; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +/* + * NET/USB List Info(All item, need processing XML data, for Network Control Only) + */ +public class XmlListInfoMsg extends ISCPMessage +{ + public final static String CODE = "NLA"; + + private final Character responceType; + private final int sequenceNumber; + private final Character status; + + /* + * UI type '0' : List, '1' : Menu, '2' : Playback, '3' : Popup, '4' : Keyboard, "5" : Menu List + */ + private enum UiType implements CharParameterIf + { + LIST('0'), MENU('1'), PLAYBACK('2'), POPUP('3'), KEYBOARD('4'), MENU_LIST('5'); + + final Character code; + + UiType(Character code) + { + this.code = code; + } + + public Character getCode() + { + return code; + } + } + + private final UiType uiType; + private final String rawXml; + + private final List items = new ArrayList<>(); + + XmlListInfoMsg(EISCPMessage raw) throws Exception + { + super(raw); + // Format: "tzzzzsurr<.....>" + responceType = data.charAt(0); + sequenceNumber = Integer.parseInt(data.substring(1, 5), 16); + status = data.charAt(5); + uiType = (UiType) searchParameter(data.charAt(6), UiType.values(), UiType.LIST); + rawXml = data.substring(9); + } + + @Override + public String toString() + { + return CODE + "[" + data.substring(0, 9) + "..." + + "; RESP=" + responceType + + "; SEQ_NR=" + sequenceNumber + + "; STATUS=" + status + + "; UI=" + uiType.toString() + + "; XML=" + rawXml + + "]"; + } + + public static String getListedData(int seqNumber, int layer, int startItem, int endItem) + { + return "L" + String.format("%04x", seqNumber) + + String.format("%02x", layer) + + String.format("%04x", startItem) + + String.format("%04x", endItem); + } + + public List parseXml(final int numberOfLayers) throws Exception + { + items.clear(); + InputStream stream = new ByteArrayInputStream(rawXml.getBytes(Charset.forName("UTF-8"))); + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + final DocumentBuilder builder = factory.newDocumentBuilder(); + final Document doc = builder.parse(stream); + for (Node object = doc.getDocumentElement(); object != null; object = object.getNextSibling()) + { + if (object instanceof Element) + { + final Element response = (Element) object; + if (!response.getTagName().equals("response") || !Utils.ensureAttribute(response, "status", "ok")) + { + continue; + } + + final List itemsTop = Utils.getElements(response, "items"); + if (itemsTop.isEmpty()) + { + continue; + } + + // Only process the first "items" element + Element itemsInfo = itemsTop.get(0); + if (itemsInfo == null || itemsInfo.getAttribute("offset") == null || itemsInfo.getAttribute("totalitems") == null) + { + continue; + } + int offset = Integer.parseInt(itemsInfo.getAttribute("offset")); + final List elements = Utils.getElements(itemsInfo, "item"); + int id = 0; + for (Element element : elements) + { + items.add(new XmlListItemMsg(offset + id, numberOfLayers, element)); + id++; + } + } + } + stream.close(); + return items; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/iscp/messages/XmlListItemMsg.java b/app/src/main/java/com/mkulesh/onpc/iscp/messages/XmlListItemMsg.java new file mode 100644 index 00000000..39168fdf --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/iscp/messages/XmlListItemMsg.java @@ -0,0 +1,106 @@ +package com.mkulesh.onpc.iscp.messages; + +import com.mkulesh.onpc.R; +import com.mkulesh.onpc.iscp.EISCPMessage; +import com.mkulesh.onpc.iscp.ISCPMessage; +import com.mkulesh.onpc.utils.Utils; + +import org.w3c.dom.Element; + +public class XmlListItemMsg extends ISCPMessage +{ + public enum Icon implements StringParameterIf + { + UNKNOWN("--", R.drawable.media_item_unknown), + USB("31", R.drawable.media_item_usb), + FOLDER("29", R.drawable.media_item_folder), + MUSIC("2d", R.drawable.media_item_music), + PLAY("36", R.drawable.media_item_play); + final String code; + final int imageId; + + Icon(String code, int imageId) + { + this.code = code; + this.imageId = imageId; + } + + public String getCode() + { + return code; + } + + public int getImageId() + { + return imageId; + } + } + + private final int numberOfLayers; + private final String title; + private final String iconType; + private final String iconId; + private final Icon icon; + private final boolean selectable; + + XmlListItemMsg(final int id, final int numberOfLayers, final Element src) + { + super(id, null); + this.numberOfLayers = numberOfLayers; + title = src.getAttribute("title") == null ? "" : src.getAttribute("title"); + iconType = src.getAttribute("icontype") == null ? "" : src.getAttribute("icontype"); + iconId = src.getAttribute("iconid") == null ? Icon.UNKNOWN.getCode() : src.getAttribute("iconid"); + icon = (Icon) searchParameter(iconId, Icon.values(), Icon.UNKNOWN); + selectable = Utils.ensureAttribute(src, "selectable", "1"); + } + + public XmlListItemMsg(XmlListItemMsg other) + { + super(other); + numberOfLayers = other.numberOfLayers; + title = other.title; + iconType = other.iconType; + iconId = other.iconId; + icon = other.icon; + selectable = other.selectable; + } + + private int getNumberOfLayers() + { + return numberOfLayers; + } + + public Icon getIcon() + { + return icon; + } + + public String getTitle() + { + return title; + } + + public boolean isSelectable() + { + return selectable; + } + + @Override + public String toString() + { + return "ITEM[" + Integer.toString(messageId) + ": " + title + + "; ICON_TYPE=" + iconType + + "; ICON_ID=" + iconId + + "; ICON=" + icon.toString() + + "; SELECTABLE=" + selectable + + "]"; + } + + @Override + public EISCPMessage getCmdMsg() + { + final String param = "I" + String.format("%02x", getNumberOfLayers()) + + String.format("%04x", getMessageId()) + "----"; + return new EISCPMessage('1', "NLA", param); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/utils/AppTheme.java b/app/src/main/java/com/mkulesh/onpc/utils/AppTheme.java new file mode 100644 index 00000000..e79904f4 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/utils/AppTheme.java @@ -0,0 +1,60 @@ +package com.mkulesh.onpc.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.preference.PreferenceManager; +import android.support.annotation.StyleRes; + +import com.mkulesh.onpc.R; + + +/********************************************************* + * Handling of themes + *********************************************************/ +public final class AppTheme +{ + private static final String PREF_APP_THEME = "app_theme"; + + public enum ThemeType + { + MAIN_THEME, + SETTINGS_THEME + } + + @StyleRes + public static int getTheme(Context context, ThemeType type) + { + final Resources res = context.getResources(); + final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); + final String themeCode = pref.getString(PREF_APP_THEME, + res.getString(R.string.pref_default_theme_code)); + + final CharSequence[] allThemes = context.getResources().getStringArray(R.array.pref_theme_codes); + int themeIndex = 0; + for (int i = 0; i < allThemes.length; i++) + { + if (allThemes[i].toString().equals(themeCode)) + { + themeIndex = i; + break; + } + } + + if (type == ThemeType.MAIN_THEME) + { + TypedArray mainThemes = res.obtainTypedArray(R.array.main_themes); + final int resId = mainThemes.getResourceId(themeIndex, R.style.BaseThemeIndigoOrange); + mainThemes.recycle(); + return resId; + } + else + { + TypedArray settingsThemes = res.obtainTypedArray(R.array.settings_themes); + final int resId = settingsThemes.getResourceId(themeIndex, R.style.SettingsThemeIndigoOrange); + settingsThemes.recycle(); + return resId; + } + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/utils/Logging.java b/app/src/main/java/com/mkulesh/onpc/utils/Logging.java new file mode 100644 index 00000000..69235a0d --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/utils/Logging.java @@ -0,0 +1,11 @@ +package com.mkulesh.onpc.utils; + +import android.util.Log; + +public final class Logging +{ + public static void info(Object o, String text) + { + Log.d("onpc", o.getClass().getSimpleName() + ": " + text); + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/utils/Utils.java b/app/src/main/java/com/mkulesh/onpc/utils/Utils.java new file mode 100755 index 00000000..ef71a645 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/utils/Utils.java @@ -0,0 +1,196 @@ +package com.mkulesh.onpc.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.AttrRes; +import android.support.annotation.ColorInt; +import android.support.v7.widget.AppCompatImageButton; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MenuItem; +import android.view.View; +import android.widget.ImageView; +import android.widget.Toast; + +import com.mkulesh.onpc.R; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class Utils +{ + public static byte[] catBuffer(byte[] bytes, int offset, int length) + { + final byte[] newBytes = new byte[length]; + System.arraycopy(bytes, offset, newBytes, 0, length); + return newBytes; + } + + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + public static Drawable getDrawable(Context context, int icon) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + { + return context.getResources().getDrawable(icon, context.getTheme()); + } + else + { + return context.getResources().getDrawable(icon); + } + } + + public static String byteArrayToHex(byte[] a) + { + if (a == null) + { + return "null"; + } + StringBuilder sb = new StringBuilder(a.length * 3); + for (byte b : a) + { + sb.append(String.format("%02x", b)).append(" "); + } + return sb.toString(); + } + + public static byte[] streamToByteArray(InputStream stream) throws IOException + { + byte[] buffer = new byte[1024]; + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + int line; + // read bytes from stream, and store them in buffer + while ((line = stream.read(buffer)) != -1) + { + // Writes bytes from byte array (buffer) into output stream. + os.write(buffer, 0, line); + } + stream.close(); + os.flush(); + os.close(); + return os.toByteArray(); + } + + public static boolean ensureAttribute(Element e, String type, String s) + { + return e.getAttribute(type) != null && e.getAttribute(type).equals(s); + } + + public static List getElements(final Element e, final String name) + { + List retValue = new ArrayList<>(); + for (Node object = e.getFirstChild(); object != null; object = object.getNextSibling()) + { + if (object instanceof Element) + { + final Element en = (Element) object; + if (name == null || name.equals(en.getTagName())) + { + retValue.add(en); + } + } + } + return retValue; + } + + /** + * Procedure returns theme color + */ + @ColorInt + public static int getThemeColorAttr(final Context context, @AttrRes int resId) + { + final TypedValue value = new TypedValue(); + context.getTheme().resolveAttribute(resId, value, true); + return value.data; + } + + /** + * Procedure updates menu item color depends its enabled state + */ + public static void updateMenuIconColor(Context context, MenuItem m) + { + setDrawableColorAttr(context, m.getIcon(), + m.isEnabled() ? android.R.attr.textColorTertiary : R.attr.colorPrimaryDark); + } + + /** + * Procedure sets AppCompatImageButton color given by attribute ID + */ + public static void setImageButtonColorAttr(Context context, AppCompatImageButton b, @AttrRes int resId) + { + final int c = getThemeColorAttr(context, resId); + b.clearColorFilter(); + b.setColorFilter(c, PorterDuff.Mode.SRC_ATOP); + } + + /** + * Procedure sets ImageView background color given by attribute ID + */ + public static void setImageViewColorAttr(Context context, ImageView b, @AttrRes int resId) + { + final int c = getThemeColorAttr(context, resId); + b.clearColorFilter(); + b.setColorFilter(c, PorterDuff.Mode.SRC_ATOP); + } + + public static void setDrawableColorAttr(Context c, Drawable drawable, @AttrRes int resId) + { + if (drawable != null) + { + drawable.clearColorFilter(); + drawable.setColorFilter(getThemeColorAttr(c, resId), PorterDuff.Mode.SRC_ATOP); + } + } + + /** + * Procedure hows toast that contains description of the given button + */ + @SuppressLint("RtlHardcoded") + public static boolean showButtonDescription(Context context, View button) + { + CharSequence contentDesc = button.getContentDescription(); + if (contentDesc != null && contentDesc.length() > 0) + { + int[] pos = new int[2]; + button.getLocationOnScreen(pos); + + Toast t = Toast.makeText(context, contentDesc, Toast.LENGTH_SHORT); + t.setGravity(Gravity.TOP | Gravity.LEFT, 0, 0); + t.getView().measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); + final int x = pos[0] + button.getMeasuredWidth() / 2 - (t.getView().getMeasuredWidth() / 2); + final int y = pos[1] - button.getMeasuredHeight() / 2 - t.getView().getMeasuredHeight() + - context.getResources().getDimensionPixelSize(R.dimen.activity_vertical_margin); + t.setGravity(Gravity.TOP | Gravity.LEFT, x, y); + t.show(); + return true; + } + return false; + } + + public static int timeToSeconds(final String timestampStr) + { + try + { + String[] tokens = timestampStr.split(":"); + int hours = Integer.parseInt(tokens[0]); + int minutes = Integer.parseInt(tokens[1]); + int seconds = Integer.parseInt(tokens[2]); + return 3600 * hours + 60 * minutes + seconds; + } + catch (Exception ex) + { + return -1; + } + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/widgets/AppCompatPreferenceActivity.java b/app/src/main/java/com/mkulesh/onpc/widgets/AppCompatPreferenceActivity.java new file mode 100644 index 00000000..1afd282c --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/widgets/AppCompatPreferenceActivity.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * 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 com.mkulesh.onpc.widgets; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatDelegate; +import android.support.v7.widget.Toolbar; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * A {@link PreferenceActivity} which implements and proxies the necessary calls + * to be used with AppCompat. + *

+ * This technique can be used with an {@link android.app.Activity} class, not just + * {@link PreferenceActivity}. + */ +public abstract class AppCompatPreferenceActivity extends PreferenceActivity +{ + private AppCompatDelegate mDelegate; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + getDelegate().installViewFactory(); + getDelegate().onCreate(savedInstanceState); + super.onCreate(savedInstanceState); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) + { + super.onPostCreate(savedInstanceState); + getDelegate().onPostCreate(savedInstanceState); + } + + public ActionBar getSupportActionBar() + { + return getDelegate().getSupportActionBar(); + } + + public void setSupportActionBar(@Nullable Toolbar toolbar) + { + getDelegate().setSupportActionBar(toolbar); + } + + @NonNull + @Override + public MenuInflater getMenuInflater() + { + return getDelegate().getMenuInflater(); + } + + @Override + public void setContentView(@LayoutRes int layoutResID) + { + getDelegate().setContentView(layoutResID); + } + + @Override + public void setContentView(View view) + { + getDelegate().setContentView(view); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) + { + getDelegate().setContentView(view, params); + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) + { + getDelegate().addContentView(view, params); + } + + @Override + protected void onPostResume() + { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override + protected void onTitleChanged(CharSequence title, int color) + { + super.onTitleChanged(title, color); + getDelegate().setTitle(title); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) + { + super.onConfigurationChanged(newConfig); + getDelegate().onConfigurationChanged(newConfig); + } + + @Override + protected void onStop() + { + super.onStop(); + getDelegate().onStop(); + } + + @Override + protected void onDestroy() + { + super.onDestroy(); + getDelegate().onDestroy(); + } + + public void invalidateOptionsMenu() + { + getDelegate().invalidateOptionsMenu(); + } + + private AppCompatDelegate getDelegate() + { + if (mDelegate == null) + { + mDelegate = AppCompatDelegate.create(this, null); + } + return mDelegate; + } +} diff --git a/app/src/main/java/com/mkulesh/onpc/widgets/MultilineCheckBoxPreference.java b/app/src/main/java/com/mkulesh/onpc/widgets/MultilineCheckBoxPreference.java new file mode 100644 index 00000000..c1175f74 --- /dev/null +++ b/app/src/main/java/com/mkulesh/onpc/widgets/MultilineCheckBoxPreference.java @@ -0,0 +1,40 @@ +package com.mkulesh.onpc.widgets; + +import android.content.Context; +import android.preference.CheckBoxPreference; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +public class MultilineCheckBoxPreference extends CheckBoxPreference +{ + public MultilineCheckBoxPreference(Context context, AttributeSet attrs) + { + super(context, attrs); + } + + protected void onBindView(View view) + { + super.onBindView(view); + makeMultiline(view); + } + + private void makeMultiline(View view) + { + if (view instanceof ViewGroup) + { + ViewGroup grp = (ViewGroup) view; + for (int index = 0; index < grp.getChildCount(); index++) + { + makeMultiline(grp.getChildAt(index)); + } + } + else if (view instanceof TextView) + { + TextView t = (TextView) view; + t.setSingleLine(false); + t.setEllipsize(null); + } + } +} diff --git a/app/src/main/res/drawable-hdpi/cmd_next.png b/app/src/main/res/drawable-hdpi/cmd_next.png new file mode 100644 index 00000000..db3ff93d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cmd_next.png differ diff --git a/app/src/main/res/drawable-hdpi/cmd_pause.png b/app/src/main/res/drawable-hdpi/cmd_pause.png new file mode 100644 index 00000000..b53e8a87 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cmd_pause.png differ diff --git a/app/src/main/res/drawable-hdpi/cmd_play.png b/app/src/main/res/drawable-hdpi/cmd_play.png new file mode 100644 index 00000000..d2d4ce02 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cmd_play.png differ diff --git a/app/src/main/res/drawable-hdpi/cmd_previous.png b/app/src/main/res/drawable-hdpi/cmd_previous.png new file mode 100644 index 00000000..fc812d4a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cmd_previous.png differ diff --git a/app/src/main/res/drawable-hdpi/cmd_random.png b/app/src/main/res/drawable-hdpi/cmd_random.png new file mode 100644 index 00000000..28981fde Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cmd_random.png differ diff --git a/app/src/main/res/drawable-hdpi/cmd_repeat.png b/app/src/main/res/drawable-hdpi/cmd_repeat.png new file mode 100644 index 00000000..a9b3422b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cmd_repeat.png differ diff --git a/app/src/main/res/drawable-hdpi/cmd_return.png b/app/src/main/res/drawable-hdpi/cmd_return.png new file mode 100644 index 00000000..c75fa122 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cmd_return.png differ diff --git a/app/src/main/res/drawable-hdpi/cmd_stop.png b/app/src/main/res/drawable-hdpi/cmd_stop.png new file mode 100644 index 00000000..90d2809d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cmd_stop.png differ diff --git a/app/src/main/res/drawable-hdpi/device_connect.png b/app/src/main/res/drawable-hdpi/device_connect.png new file mode 100644 index 00000000..9128643a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/device_connect.png differ diff --git a/app/src/main/res/drawable-hdpi/device_disconnect.png b/app/src/main/res/drawable-hdpi/device_disconnect.png new file mode 100644 index 00000000..03dea9df Binary files /dev/null and b/app/src/main/res/drawable-hdpi/device_disconnect.png differ diff --git a/app/src/main/res/drawable-hdpi/media_item_folder.png b/app/src/main/res/drawable-hdpi/media_item_folder.png new file mode 100644 index 00000000..6867dca0 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_item_folder.png differ diff --git a/app/src/main/res/drawable-hdpi/media_item_music.png b/app/src/main/res/drawable-hdpi/media_item_music.png new file mode 100644 index 00000000..2e8f09f8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_item_music.png differ diff --git a/app/src/main/res/drawable-hdpi/media_item_play.png b/app/src/main/res/drawable-hdpi/media_item_play.png new file mode 100644 index 00000000..6adb0678 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_item_play.png differ diff --git a/app/src/main/res/drawable-hdpi/media_item_unknown.png b/app/src/main/res/drawable-hdpi/media_item_unknown.png new file mode 100644 index 00000000..813b7102 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_item_unknown.png differ diff --git a/app/src/main/res/drawable-hdpi/media_item_usb.png b/app/src/main/res/drawable-hdpi/media_item_usb.png new file mode 100644 index 00000000..0488742d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/media_item_usb.png differ diff --git a/app/src/main/res/drawable-hdpi/playlist_add.png b/app/src/main/res/drawable-hdpi/playlist_add.png new file mode 100644 index 00000000..8f3a099c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/playlist_add.png differ diff --git a/app/src/main/res/drawable-hdpi/playlist_move_from.png b/app/src/main/res/drawable-hdpi/playlist_move_from.png new file mode 100644 index 00000000..42a51bff Binary files /dev/null and b/app/src/main/res/drawable-hdpi/playlist_move_from.png differ diff --git a/app/src/main/res/drawable-hdpi/playlist_move_to.png b/app/src/main/res/drawable-hdpi/playlist_move_to.png new file mode 100644 index 00000000..21ee8d3f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/playlist_move_to.png differ diff --git a/app/src/main/res/drawable-hdpi/playlist_remove.png b/app/src/main/res/drawable-hdpi/playlist_remove.png new file mode 100644 index 00000000..71bfd174 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/playlist_remove.png differ diff --git a/app/src/main/res/drawable-hdpi/playlist_remove_all.png b/app/src/main/res/drawable-hdpi/playlist_remove_all.png new file mode 100644 index 00000000..eb1be9eb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/playlist_remove_all.png differ diff --git a/app/src/main/res/drawable-hdpi/power_standby.png b/app/src/main/res/drawable-hdpi/power_standby.png new file mode 100644 index 00000000..439bac83 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/power_standby.png differ diff --git a/app/src/main/res/drawable-hdpi/selector_net.png b/app/src/main/res/drawable-hdpi/selector_net.png new file mode 100644 index 00000000..c12df52e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/selector_net.png differ diff --git a/app/src/main/res/drawable-hdpi/selector_usb_front.png b/app/src/main/res/drawable-hdpi/selector_usb_front.png new file mode 100644 index 00000000..c5fcd805 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/selector_usb_front.png differ diff --git a/app/src/main/res/drawable-hdpi/selector_usb_rear.png b/app/src/main/res/drawable-hdpi/selector_usb_rear.png new file mode 100644 index 00000000..6aede29c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/selector_usb_rear.png differ diff --git a/app/src/main/res/drawable-hdpi/volume_down.png b/app/src/main/res/drawable-hdpi/volume_down.png new file mode 100644 index 00000000..aca47d8f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/volume_down.png differ diff --git a/app/src/main/res/drawable-hdpi/volume_mute.png b/app/src/main/res/drawable-hdpi/volume_mute.png new file mode 100644 index 00000000..624abdb8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/volume_mute.png differ diff --git a/app/src/main/res/drawable-hdpi/volume_up.png b/app/src/main/res/drawable-hdpi/volume_up.png new file mode 100644 index 00000000..3c12c895 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/volume_up.png differ diff --git a/app/src/main/res/drawable-mdpi/cmd_next.png b/app/src/main/res/drawable-mdpi/cmd_next.png new file mode 100644 index 00000000..4a5c9c61 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cmd_next.png differ diff --git a/app/src/main/res/drawable-mdpi/cmd_pause.png b/app/src/main/res/drawable-mdpi/cmd_pause.png new file mode 100644 index 00000000..988247b9 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cmd_pause.png differ diff --git a/app/src/main/res/drawable-mdpi/cmd_play.png b/app/src/main/res/drawable-mdpi/cmd_play.png new file mode 100644 index 00000000..1c2836c8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cmd_play.png differ diff --git a/app/src/main/res/drawable-mdpi/cmd_previous.png b/app/src/main/res/drawable-mdpi/cmd_previous.png new file mode 100644 index 00000000..f7e763fd Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cmd_previous.png differ diff --git a/app/src/main/res/drawable-mdpi/cmd_random.png b/app/src/main/res/drawable-mdpi/cmd_random.png new file mode 100644 index 00000000..e42e56be Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cmd_random.png differ diff --git a/app/src/main/res/drawable-mdpi/cmd_repeat.png b/app/src/main/res/drawable-mdpi/cmd_repeat.png new file mode 100644 index 00000000..5d67144e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cmd_repeat.png differ diff --git a/app/src/main/res/drawable-mdpi/cmd_return.png b/app/src/main/res/drawable-mdpi/cmd_return.png new file mode 100644 index 00000000..1ec19660 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cmd_return.png differ diff --git a/app/src/main/res/drawable-mdpi/cmd_stop.png b/app/src/main/res/drawable-mdpi/cmd_stop.png new file mode 100644 index 00000000..bddd65c6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cmd_stop.png differ diff --git a/app/src/main/res/drawable-mdpi/device_connect.png b/app/src/main/res/drawable-mdpi/device_connect.png new file mode 100644 index 00000000..97f7b67a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/device_connect.png differ diff --git a/app/src/main/res/drawable-mdpi/device_disconnect.png b/app/src/main/res/drawable-mdpi/device_disconnect.png new file mode 100644 index 00000000..8bc9c01a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/device_disconnect.png differ diff --git a/app/src/main/res/drawable-mdpi/media_item_folder.png b/app/src/main/res/drawable-mdpi/media_item_folder.png new file mode 100644 index 00000000..07066ba0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_item_folder.png differ diff --git a/app/src/main/res/drawable-mdpi/media_item_music.png b/app/src/main/res/drawable-mdpi/media_item_music.png new file mode 100644 index 00000000..9ebce69a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_item_music.png differ diff --git a/app/src/main/res/drawable-mdpi/media_item_play.png b/app/src/main/res/drawable-mdpi/media_item_play.png new file mode 100644 index 00000000..717c6ec5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_item_play.png differ diff --git a/app/src/main/res/drawable-mdpi/media_item_unknown.png b/app/src/main/res/drawable-mdpi/media_item_unknown.png new file mode 100644 index 00000000..23a2fd93 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_item_unknown.png differ diff --git a/app/src/main/res/drawable-mdpi/media_item_usb.png b/app/src/main/res/drawable-mdpi/media_item_usb.png new file mode 100644 index 00000000..166878dc Binary files /dev/null and b/app/src/main/res/drawable-mdpi/media_item_usb.png differ diff --git a/app/src/main/res/drawable-mdpi/playlist_add.png b/app/src/main/res/drawable-mdpi/playlist_add.png new file mode 100644 index 00000000..f092deb8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/playlist_add.png differ diff --git a/app/src/main/res/drawable-mdpi/playlist_move_from.png b/app/src/main/res/drawable-mdpi/playlist_move_from.png new file mode 100644 index 00000000..722eec40 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/playlist_move_from.png differ diff --git a/app/src/main/res/drawable-mdpi/playlist_move_to.png b/app/src/main/res/drawable-mdpi/playlist_move_to.png new file mode 100644 index 00000000..2ff859ea Binary files /dev/null and b/app/src/main/res/drawable-mdpi/playlist_move_to.png differ diff --git a/app/src/main/res/drawable-mdpi/playlist_remove.png b/app/src/main/res/drawable-mdpi/playlist_remove.png new file mode 100644 index 00000000..5d8f51aa Binary files /dev/null and b/app/src/main/res/drawable-mdpi/playlist_remove.png differ diff --git a/app/src/main/res/drawable-mdpi/playlist_remove_all.png b/app/src/main/res/drawable-mdpi/playlist_remove_all.png new file mode 100644 index 00000000..7c3b985d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/playlist_remove_all.png differ diff --git a/app/src/main/res/drawable-mdpi/power_standby.png b/app/src/main/res/drawable-mdpi/power_standby.png new file mode 100644 index 00000000..54bf51d0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/power_standby.png differ diff --git a/app/src/main/res/drawable-mdpi/selector_net.png b/app/src/main/res/drawable-mdpi/selector_net.png new file mode 100644 index 00000000..a4e43d0b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/selector_net.png differ diff --git a/app/src/main/res/drawable-mdpi/selector_usb_front.png b/app/src/main/res/drawable-mdpi/selector_usb_front.png new file mode 100644 index 00000000..993b2f90 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/selector_usb_front.png differ diff --git a/app/src/main/res/drawable-mdpi/selector_usb_rear.png b/app/src/main/res/drawable-mdpi/selector_usb_rear.png new file mode 100644 index 00000000..4228af1d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/selector_usb_rear.png differ diff --git a/app/src/main/res/drawable-mdpi/volume_down.png b/app/src/main/res/drawable-mdpi/volume_down.png new file mode 100644 index 00000000..2dcf408f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/volume_down.png differ diff --git a/app/src/main/res/drawable-mdpi/volume_mute.png b/app/src/main/res/drawable-mdpi/volume_mute.png new file mode 100644 index 00000000..cf6f958e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/volume_mute.png differ diff --git a/app/src/main/res/drawable-mdpi/volume_up.png b/app/src/main/res/drawable-mdpi/volume_up.png new file mode 100644 index 00000000..286ff283 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/volume_up.png differ diff --git a/app/src/main/res/drawable-nodpi/empty_cover.jpg b/app/src/main/res/drawable-nodpi/empty_cover.jpg new file mode 100644 index 00000000..570302d2 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/empty_cover.jpg differ diff --git a/app/src/main/res/drawable-xhdpi/cmd_next.png b/app/src/main/res/drawable-xhdpi/cmd_next.png new file mode 100644 index 00000000..3cb1d19d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cmd_next.png differ diff --git a/app/src/main/res/drawable-xhdpi/cmd_pause.png b/app/src/main/res/drawable-xhdpi/cmd_pause.png new file mode 100644 index 00000000..82c023f7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cmd_pause.png differ diff --git a/app/src/main/res/drawable-xhdpi/cmd_play.png b/app/src/main/res/drawable-xhdpi/cmd_play.png new file mode 100644 index 00000000..729712aa Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cmd_play.png differ diff --git a/app/src/main/res/drawable-xhdpi/cmd_previous.png b/app/src/main/res/drawable-xhdpi/cmd_previous.png new file mode 100644 index 00000000..4e18ac29 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cmd_previous.png differ diff --git a/app/src/main/res/drawable-xhdpi/cmd_random.png b/app/src/main/res/drawable-xhdpi/cmd_random.png new file mode 100644 index 00000000..a3e63dba Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cmd_random.png differ diff --git a/app/src/main/res/drawable-xhdpi/cmd_repeat.png b/app/src/main/res/drawable-xhdpi/cmd_repeat.png new file mode 100644 index 00000000..14d8855c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cmd_repeat.png differ diff --git a/app/src/main/res/drawable-xhdpi/cmd_return.png b/app/src/main/res/drawable-xhdpi/cmd_return.png new file mode 100644 index 00000000..d3fa882f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cmd_return.png differ diff --git a/app/src/main/res/drawable-xhdpi/cmd_stop.png b/app/src/main/res/drawable-xhdpi/cmd_stop.png new file mode 100644 index 00000000..f3811f3d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cmd_stop.png differ diff --git a/app/src/main/res/drawable-xhdpi/device_connect.png b/app/src/main/res/drawable-xhdpi/device_connect.png new file mode 100644 index 00000000..d61b2d53 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/device_connect.png differ diff --git a/app/src/main/res/drawable-xhdpi/device_disconnect.png b/app/src/main/res/drawable-xhdpi/device_disconnect.png new file mode 100644 index 00000000..700faa1b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/device_disconnect.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_item_folder.png b/app/src/main/res/drawable-xhdpi/media_item_folder.png new file mode 100644 index 00000000..bdc8267f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_item_folder.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_item_music.png b/app/src/main/res/drawable-xhdpi/media_item_music.png new file mode 100644 index 00000000..ad0e8e28 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_item_music.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_item_play.png b/app/src/main/res/drawable-xhdpi/media_item_play.png new file mode 100644 index 00000000..3592804c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_item_play.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_item_unknown.png b/app/src/main/res/drawable-xhdpi/media_item_unknown.png new file mode 100644 index 00000000..31dd232a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_item_unknown.png differ diff --git a/app/src/main/res/drawable-xhdpi/media_item_usb.png b/app/src/main/res/drawable-xhdpi/media_item_usb.png new file mode 100644 index 00000000..336bd38d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/media_item_usb.png differ diff --git a/app/src/main/res/drawable-xhdpi/playlist_add.png b/app/src/main/res/drawable-xhdpi/playlist_add.png new file mode 100644 index 00000000..896e294f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/playlist_add.png differ diff --git a/app/src/main/res/drawable-xhdpi/playlist_move_from.png b/app/src/main/res/drawable-xhdpi/playlist_move_from.png new file mode 100644 index 00000000..25cd3837 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/playlist_move_from.png differ diff --git a/app/src/main/res/drawable-xhdpi/playlist_move_to.png b/app/src/main/res/drawable-xhdpi/playlist_move_to.png new file mode 100644 index 00000000..495c02a7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/playlist_move_to.png differ diff --git a/app/src/main/res/drawable-xhdpi/playlist_remove.png b/app/src/main/res/drawable-xhdpi/playlist_remove.png new file mode 100644 index 00000000..8d2ad7d3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/playlist_remove.png differ diff --git a/app/src/main/res/drawable-xhdpi/playlist_remove_all.png b/app/src/main/res/drawable-xhdpi/playlist_remove_all.png new file mode 100644 index 00000000..c7fceab4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/playlist_remove_all.png differ diff --git a/app/src/main/res/drawable-xhdpi/power_standby.png b/app/src/main/res/drawable-xhdpi/power_standby.png new file mode 100644 index 00000000..c2248bd6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/power_standby.png differ diff --git a/app/src/main/res/drawable-xhdpi/selector_net.png b/app/src/main/res/drawable-xhdpi/selector_net.png new file mode 100644 index 00000000..6d6d2dc7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/selector_net.png differ diff --git a/app/src/main/res/drawable-xhdpi/selector_usb_front.png b/app/src/main/res/drawable-xhdpi/selector_usb_front.png new file mode 100644 index 00000000..04e84a78 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/selector_usb_front.png differ diff --git a/app/src/main/res/drawable-xhdpi/selector_usb_rear.png b/app/src/main/res/drawable-xhdpi/selector_usb_rear.png new file mode 100644 index 00000000..b96aaf9f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/selector_usb_rear.png differ diff --git a/app/src/main/res/drawable-xhdpi/volume_down.png b/app/src/main/res/drawable-xhdpi/volume_down.png new file mode 100644 index 00000000..e58a45c9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/volume_down.png differ diff --git a/app/src/main/res/drawable-xhdpi/volume_mute.png b/app/src/main/res/drawable-xhdpi/volume_mute.png new file mode 100644 index 00000000..3b8fdd23 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/volume_mute.png differ diff --git a/app/src/main/res/drawable-xhdpi/volume_up.png b/app/src/main/res/drawable-xhdpi/volume_up.png new file mode 100644 index 00000000..bc9776f9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/volume_up.png differ diff --git a/app/src/main/res/drawable-xxhdpi/cmd_next.png b/app/src/main/res/drawable-xxhdpi/cmd_next.png new file mode 100644 index 00000000..e87bf3d3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cmd_next.png differ diff --git a/app/src/main/res/drawable-xxhdpi/cmd_pause.png b/app/src/main/res/drawable-xxhdpi/cmd_pause.png new file mode 100644 index 00000000..e7fabb2e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cmd_pause.png differ diff --git a/app/src/main/res/drawable-xxhdpi/cmd_play.png b/app/src/main/res/drawable-xxhdpi/cmd_play.png new file mode 100644 index 00000000..7275ec9c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cmd_play.png differ diff --git a/app/src/main/res/drawable-xxhdpi/cmd_previous.png b/app/src/main/res/drawable-xxhdpi/cmd_previous.png new file mode 100644 index 00000000..15877eaa Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cmd_previous.png differ diff --git a/app/src/main/res/drawable-xxhdpi/cmd_random.png b/app/src/main/res/drawable-xxhdpi/cmd_random.png new file mode 100644 index 00000000..76074440 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cmd_random.png differ diff --git a/app/src/main/res/drawable-xxhdpi/cmd_repeat.png b/app/src/main/res/drawable-xxhdpi/cmd_repeat.png new file mode 100644 index 00000000..3f02ceca Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cmd_repeat.png differ diff --git a/app/src/main/res/drawable-xxhdpi/cmd_return.png b/app/src/main/res/drawable-xxhdpi/cmd_return.png new file mode 100644 index 00000000..09c368e2 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cmd_return.png differ diff --git a/app/src/main/res/drawable-xxhdpi/cmd_stop.png b/app/src/main/res/drawable-xxhdpi/cmd_stop.png new file mode 100644 index 00000000..879b8bfb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cmd_stop.png differ diff --git a/app/src/main/res/drawable-xxhdpi/device_connect.png b/app/src/main/res/drawable-xxhdpi/device_connect.png new file mode 100644 index 00000000..05e24bab Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/device_connect.png differ diff --git a/app/src/main/res/drawable-xxhdpi/device_disconnect.png b/app/src/main/res/drawable-xxhdpi/device_disconnect.png new file mode 100644 index 00000000..2be30994 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/device_disconnect.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_item_folder.png b/app/src/main/res/drawable-xxhdpi/media_item_folder.png new file mode 100644 index 00000000..6df103ac Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_item_folder.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_item_music.png b/app/src/main/res/drawable-xxhdpi/media_item_music.png new file mode 100644 index 00000000..2da65ea4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_item_music.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_item_play.png b/app/src/main/res/drawable-xxhdpi/media_item_play.png new file mode 100644 index 00000000..fe2636eb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_item_play.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_item_unknown.png b/app/src/main/res/drawable-xxhdpi/media_item_unknown.png new file mode 100644 index 00000000..a5f3fa62 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_item_unknown.png differ diff --git a/app/src/main/res/drawable-xxhdpi/media_item_usb.png b/app/src/main/res/drawable-xxhdpi/media_item_usb.png new file mode 100644 index 00000000..84f009b5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/media_item_usb.png differ diff --git a/app/src/main/res/drawable-xxhdpi/playlist_add.png b/app/src/main/res/drawable-xxhdpi/playlist_add.png new file mode 100644 index 00000000..0b55cf18 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/playlist_add.png differ diff --git a/app/src/main/res/drawable-xxhdpi/playlist_move_from.png b/app/src/main/res/drawable-xxhdpi/playlist_move_from.png new file mode 100644 index 00000000..eda4ef6f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/playlist_move_from.png differ diff --git a/app/src/main/res/drawable-xxhdpi/playlist_move_to.png b/app/src/main/res/drawable-xxhdpi/playlist_move_to.png new file mode 100644 index 00000000..3a66c535 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/playlist_move_to.png differ diff --git a/app/src/main/res/drawable-xxhdpi/playlist_remove.png b/app/src/main/res/drawable-xxhdpi/playlist_remove.png new file mode 100644 index 00000000..99fa9ac5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/playlist_remove.png differ diff --git a/app/src/main/res/drawable-xxhdpi/playlist_remove_all.png b/app/src/main/res/drawable-xxhdpi/playlist_remove_all.png new file mode 100644 index 00000000..21002ea6 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/playlist_remove_all.png differ diff --git a/app/src/main/res/drawable-xxhdpi/power_standby.png b/app/src/main/res/drawable-xxhdpi/power_standby.png new file mode 100644 index 00000000..33d09116 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/power_standby.png differ diff --git a/app/src/main/res/drawable-xxhdpi/selector_net.png b/app/src/main/res/drawable-xxhdpi/selector_net.png new file mode 100644 index 00000000..64778cd7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/selector_net.png differ diff --git a/app/src/main/res/drawable-xxhdpi/selector_usb_front.png b/app/src/main/res/drawable-xxhdpi/selector_usb_front.png new file mode 100644 index 00000000..59f6b7be Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/selector_usb_front.png differ diff --git a/app/src/main/res/drawable-xxhdpi/selector_usb_rear.png b/app/src/main/res/drawable-xxhdpi/selector_usb_rear.png new file mode 100644 index 00000000..1b5a560c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/selector_usb_rear.png differ diff --git a/app/src/main/res/drawable-xxhdpi/volume_down.png b/app/src/main/res/drawable-xxhdpi/volume_down.png new file mode 100644 index 00000000..17af646e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/volume_down.png differ diff --git a/app/src/main/res/drawable-xxhdpi/volume_mute.png b/app/src/main/res/drawable-xxhdpi/volume_mute.png new file mode 100644 index 00000000..a5419b2f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/volume_mute.png differ diff --git a/app/src/main/res/drawable-xxhdpi/volume_up.png b/app/src/main/res/drawable-xxhdpi/volume_up.png new file mode 100644 index 00000000..478d5dda Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/volume_up.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/cmd_next.png b/app/src/main/res/drawable-xxxhdpi/cmd_next.png new file mode 100644 index 00000000..87d248a1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cmd_next.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/cmd_pause.png b/app/src/main/res/drawable-xxxhdpi/cmd_pause.png new file mode 100644 index 00000000..cd06fbb1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cmd_pause.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/cmd_play.png b/app/src/main/res/drawable-xxxhdpi/cmd_play.png new file mode 100644 index 00000000..6c627113 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cmd_play.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/cmd_previous.png b/app/src/main/res/drawable-xxxhdpi/cmd_previous.png new file mode 100644 index 00000000..5ed77297 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cmd_previous.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/cmd_random.png b/app/src/main/res/drawable-xxxhdpi/cmd_random.png new file mode 100644 index 00000000..8b188c53 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cmd_random.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/cmd_repeat.png b/app/src/main/res/drawable-xxxhdpi/cmd_repeat.png new file mode 100644 index 00000000..bf3c7dbd Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cmd_repeat.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/cmd_return.png b/app/src/main/res/drawable-xxxhdpi/cmd_return.png new file mode 100644 index 00000000..bc36ef0d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cmd_return.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/cmd_stop.png b/app/src/main/res/drawable-xxxhdpi/cmd_stop.png new file mode 100644 index 00000000..52ea4de4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cmd_stop.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/device_connect.png b/app/src/main/res/drawable-xxxhdpi/device_connect.png new file mode 100644 index 00000000..bc92f357 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/device_connect.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/device_disconnect.png b/app/src/main/res/drawable-xxxhdpi/device_disconnect.png new file mode 100644 index 00000000..49347e4a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/device_disconnect.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_item_folder.png b/app/src/main/res/drawable-xxxhdpi/media_item_folder.png new file mode 100644 index 00000000..485f5444 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_item_folder.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_item_music.png b/app/src/main/res/drawable-xxxhdpi/media_item_music.png new file mode 100644 index 00000000..777ac0dd Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_item_music.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_item_play.png b/app/src/main/res/drawable-xxxhdpi/media_item_play.png new file mode 100644 index 00000000..053bda83 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_item_play.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_item_unknown.png b/app/src/main/res/drawable-xxxhdpi/media_item_unknown.png new file mode 100644 index 00000000..aea15e9b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_item_unknown.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/media_item_usb.png b/app/src/main/res/drawable-xxxhdpi/media_item_usb.png new file mode 100644 index 00000000..ce7be3f0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/media_item_usb.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/playlist_add.png b/app/src/main/res/drawable-xxxhdpi/playlist_add.png new file mode 100644 index 00000000..8c707361 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/playlist_add.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/playlist_move_from.png b/app/src/main/res/drawable-xxxhdpi/playlist_move_from.png new file mode 100644 index 00000000..45ec7f91 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/playlist_move_from.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/playlist_move_to.png b/app/src/main/res/drawable-xxxhdpi/playlist_move_to.png new file mode 100644 index 00000000..0a76b324 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/playlist_move_to.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/playlist_remove.png b/app/src/main/res/drawable-xxxhdpi/playlist_remove.png new file mode 100644 index 00000000..3ca194f1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/playlist_remove.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/playlist_remove_all.png b/app/src/main/res/drawable-xxxhdpi/playlist_remove_all.png new file mode 100644 index 00000000..20ca4b62 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/playlist_remove_all.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/power_standby.png b/app/src/main/res/drawable-xxxhdpi/power_standby.png new file mode 100644 index 00000000..88adbd58 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/power_standby.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/selector_net.png b/app/src/main/res/drawable-xxxhdpi/selector_net.png new file mode 100644 index 00000000..3ff50de4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/selector_net.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/selector_usb_front.png b/app/src/main/res/drawable-xxxhdpi/selector_usb_front.png new file mode 100644 index 00000000..ec9b7891 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/selector_usb_front.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/selector_usb_rear.png b/app/src/main/res/drawable-xxxhdpi/selector_usb_rear.png new file mode 100644 index 00000000..5ec79916 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/selector_usb_rear.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/volume_down.png b/app/src/main/res/drawable-xxxhdpi/volume_down.png new file mode 100644 index 00000000..9f858c4e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/volume_down.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/volume_mute.png b/app/src/main/res/drawable-xxxhdpi/volume_mute.png new file mode 100644 index 00000000..2b34e394 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/volume_mute.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/volume_up.png b/app/src/main/res/drawable-xxxhdpi/volume_up.png new file mode 100644 index 00000000..f4217efd Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/volume_up.png differ diff --git a/app/src/main/res/layout-land/monitor_fragment.xml b/app/src/main/res/layout-land/monitor_fragment.xml new file mode 100644 index 00000000..3b38683e --- /dev/null +++ b/app/src/main/res/layout-land/monitor_fragment.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..80bca784 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/device_fragment.xml b/app/src/main/res/layout/device_fragment.xml new file mode 100644 index 00000000..206c4f14 --- /dev/null +++ b/app/src/main/res/layout/device_fragment.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + +