commit 04dc157f6ebdbf8f8153e4dcc2f8cf4c23afb67d Author: David Schulte Date: Fri Jan 23 18:26:34 2015 +0100 Initial commit diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000..a349533 --- /dev/null +++ b/app/app.iml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..90fbcfe --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + applicationId "de.arcus.playmusicexporter2" + minSdkVersion 8 + targetSdkVersion 21 + versionCode 65 + versionName '2.4.0' + } + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:21.0.3' + compile 'com.android.support:support-v4:21.0.3' + compile project(':framework') +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..28e4acc --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/david/android-sdks/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 *; +#} diff --git a/app/src/androidTest/java/de/arcus/playmusicexporter2/ApplicationTest.java b/app/src/androidTest/java/de/arcus/playmusicexporter2/ApplicationTest.java new file mode 100644 index 0000000..89f2893 --- /dev/null +++ b/app/src/androidTest/java/de/arcus/playmusicexporter2/ApplicationTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.playmusicexporter2; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6e53474 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/de/arcus/playmusicexporter2/TrackDetailActivity.java b/app/src/main/java/de/arcus/playmusicexporter2/TrackDetailActivity.java new file mode 100644 index 0000000..e688625 --- /dev/null +++ b/app/src/main/java/de/arcus/playmusicexporter2/TrackDetailActivity.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.playmusicexporter2; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.support.v4.app.NavUtils; +import android.view.MenuItem; + + +/** + * An activity representing a single Track detail screen. This + * activity is only used on handset devices. On tablet-size devices, + * item details are presented side-by-side with a list of items + * in a {@link TrackListActivity}. + *

+ * This activity is mostly just a 'shell' activity containing nothing + * more than a {@link TrackDetailFragment}. + */ +public class TrackDetailActivity extends ActionBarActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_track_detail); + + // Show the Up button in the action bar. + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + // savedInstanceState is non-null when there is fragment state + // saved from previous configurations of this activity + // (e.g. when rotating the screen from portrait to landscape). + // In this case, the fragment will automatically be re-added + // to its container so we don't need to manually add it. + // For more information, see the Fragments API guide at: + // + // http://developer.android.com/guide/components/fragments.html + // + if (savedInstanceState == null) { + // Create the detail fragment and add it to the activity + // using a fragment transaction. + Bundle arguments = new Bundle(); + arguments.putString(TrackDetailFragment.ARG_ITEM_ID, + getIntent().getStringExtra(TrackDetailFragment.ARG_ITEM_ID)); + TrackDetailFragment fragment = new TrackDetailFragment(); + fragment.setArguments(arguments); + getSupportFragmentManager().beginTransaction() + .add(R.id.track_detail_container, fragment) + .commit(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == android.R.id.home) { + // This ID represents the Home or Up button. In the case of this + // activity, the Up button is shown. Use NavUtils to allow users + // to navigate up one level in the application structure. For + // more details, see the Navigation pattern on Android Design: + // + // http://developer.android.com/design/patterns/navigation.html#up-vs-back + // + NavUtils.navigateUpTo(this, new Intent(this, TrackListActivity.class)); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/de/arcus/playmusicexporter2/TrackDetailFragment.java b/app/src/main/java/de/arcus/playmusicexporter2/TrackDetailFragment.java new file mode 100644 index 0000000..c04b81d --- /dev/null +++ b/app/src/main/java/de/arcus/playmusicexporter2/TrackDetailFragment.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.playmusicexporter2; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + + +import de.arcus.playmusicexporter2.dummy.DummyContent; + +/** + * A fragment representing a single Track detail screen. + * This fragment is either contained in a {@link TrackListActivity} + * in two-pane mode (on tablets) or a {@link TrackDetailActivity} + * on handsets. + */ +public class TrackDetailFragment extends Fragment { + /** + * The fragment argument representing the item ID that this fragment + * represents. + */ + public static final String ARG_ITEM_ID = "item_id"; + + /** + * The dummy content this fragment is presenting. + */ + private DummyContent.DummyItem mItem; + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public TrackDetailFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments().containsKey(ARG_ITEM_ID)) { + // Load the dummy content specified by the fragment + // arguments. In a real-world scenario, use a Loader + // to load content from a content provider. + mItem = DummyContent.ITEM_MAP.get(getArguments().getString(ARG_ITEM_ID)); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_track_detail, container, false); + + // Show the dummy content as text in a TextView. + if (mItem != null) { + ((TextView) rootView.findViewById(R.id.track_detail)).setText(mItem.content); + } + + return rootView; + } +} diff --git a/app/src/main/java/de/arcus/playmusicexporter2/TrackListActivity.java b/app/src/main/java/de/arcus/playmusicexporter2/TrackListActivity.java new file mode 100644 index 0000000..1bb7412 --- /dev/null +++ b/app/src/main/java/de/arcus/playmusicexporter2/TrackListActivity.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.playmusicexporter2; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; + +import de.arcus.framework.logger.Logger; +import de.arcus.framework.crashhandler.CrashHandler; +import de.arcus.framework.superuser.SuperUser; + +/** + * An activity representing a list of Tracks. This activity + * has different presentations for handset and tablet-size devices. On + * handsets, the activity presents a list of items, which when touched, + * lead to a {@link TrackDetailActivity} representing + * item details. On tablets, the activity presents the list of items and + * item details side-by-side using two vertical panes. + *

+ * The activity makes heavy use of fragments. The list of items is a + * {@link TrackListFragment} and the item details + * (if present) is a {@link TrackDetailFragment}. + *

+ * This activity also implements the required + * {@link TrackListFragment.Callbacks} interface + * to listen for item selections. + */ +public class TrackListActivity extends ActionBarActivity + implements TrackListFragment.Callbacks { + + /** + * Whether or not the activity is in two-pane mode, i.e. running on a tablet + * device. + */ + private boolean mTwoPane; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_track_list); + + // Adds the crash handler to this class + CrashHandler.addCrashHandler(this); + + Logger.getInstance().logVerbose("Activity", "onCreate(" + this.getLocalClassName() + ")"); + + // Setup ActionBar + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.app_name); + } + + if (findViewById(R.id.track_detail_container) != null) { + // The detail container view will be present only in the + // large-screen layouts (res/values-large and + // res/values-sw600dp). If this view is present, then the + // activity should be in two-pane mode. + mTwoPane = true; + + // In two-pane mode, list items should be given the + // 'activated' state when touched. + ((TrackListFragment) getSupportFragmentManager() + .findFragmentById(R.id.track_list)) + .setActivateOnItemClick(true); + } + + + SuperUser.askForPermissions(); + + + // TODO: If exposing deep links into your app, handle intents here. + } + + /** + * Callback method from {@link TrackListFragment.Callbacks} + * indicating that the item with the given ID was selected. + */ + @Override + public void onItemSelected(String id) { + if (mTwoPane) { + // In two-pane mode, show the detail view in this activity by + // adding or replacing the detail fragment using a + // fragment transaction. + Bundle arguments = new Bundle(); + arguments.putString(TrackDetailFragment.ARG_ITEM_ID, id); + TrackDetailFragment fragment = new TrackDetailFragment(); + fragment.setArguments(arguments); + getSupportFragmentManager().beginTransaction() + .replace(R.id.track_detail_container, fragment) + .commit(); + + } else { + // In single-pane mode, simply start the detail activity + // for the selected item ID. + Intent detailIntent = new Intent(this, TrackDetailActivity.class); + detailIntent.putExtra(TrackDetailFragment.ARG_ITEM_ID, id); + startActivity(detailIntent); + } + } +} diff --git a/app/src/main/java/de/arcus/playmusicexporter2/TrackListFragment.java b/app/src/main/java/de/arcus/playmusicexporter2/TrackListFragment.java new file mode 100644 index 0000000..ac849fc --- /dev/null +++ b/app/src/main/java/de/arcus/playmusicexporter2/TrackListFragment.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.playmusicexporter2; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; + + +import de.arcus.playmusicexporter2.dummy.DummyContent; + +/** + * A list fragment representing a list of Tracks. This fragment + * also supports tablet devices by allowing list items to be given an + * 'activated' state upon selection. This helps indicate which item is + * currently being viewed in a {@link TrackDetailFragment}. + *

+ * Activities containing this fragment MUST implement the {@link Callbacks} + * interface. + */ +public class TrackListFragment extends ListFragment { + + /** + * The serialization (saved instance state) Bundle key representing the + * activated item position. Only used on tablets. + */ + private static final String STATE_ACTIVATED_POSITION = "activated_position"; + + /** + * The fragment's current callback object, which is notified of list item + * clicks. + */ + private Callbacks mCallbacks = sDummyCallbacks; + + /** + * The current activated item position. Only used on tablets. + */ + private int mActivatedPosition = ListView.INVALID_POSITION; + + /** + * A callback interface that all activities containing this fragment must + * implement. This mechanism allows activities to be notified of item + * selections. + */ + public interface Callbacks { + /** + * Callback for when an item has been selected. + */ + public void onItemSelected(String id); + } + + /** + * A dummy implementation of the {@link Callbacks} interface that does + * nothing. Used only when this fragment is not attached to an activity. + */ + private static Callbacks sDummyCallbacks = new Callbacks() { + @Override + public void onItemSelected(String id) { + } + }; + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public TrackListFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // TODO: replace with a real list adapter. + setListAdapter(new ArrayAdapter( + getActivity(), + android.R.layout.simple_list_item_activated_1, + android.R.id.text1, + DummyContent.ITEMS)); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // Restore the previously serialized activated item position. + if (savedInstanceState != null + && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { + setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + // Activities containing this fragment must implement its callbacks. + if (!(activity instanceof Callbacks)) { + throw new IllegalStateException("Activity must implement fragment's callbacks."); + } + + mCallbacks = (Callbacks) activity; + } + + @Override + public void onDetach() { + super.onDetach(); + + // Reset the active callbacks interface to the dummy implementation. + mCallbacks = sDummyCallbacks; + } + + @Override + public void onListItemClick(ListView listView, View view, int position, long id) { + super.onListItemClick(listView, view, position, id); + + // Notify the active callbacks interface (the activity, if the + // fragment is attached to one) that an item has been selected. + mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mActivatedPosition != ListView.INVALID_POSITION) { + // Serialize and persist the activated item position. + outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition); + } + } + + /** + * Turns on activate-on-click mode. When this mode is on, list items will be + * given the 'activated' state when touched. + */ + public void setActivateOnItemClick(boolean activateOnItemClick) { + // When setting CHOICE_MODE_SINGLE, ListView will automatically + // give items the 'activated' state when touched. + getListView().setChoiceMode(activateOnItemClick + ? ListView.CHOICE_MODE_SINGLE + : ListView.CHOICE_MODE_NONE); + } + + private void setActivatedPosition(int position) { + if (position == ListView.INVALID_POSITION) { + getListView().setItemChecked(mActivatedPosition, false); + } else { + getListView().setItemChecked(position, true); + } + + mActivatedPosition = position; + } +} diff --git a/app/src/main/java/de/arcus/playmusicexporter2/dummy/DummyContent.java b/app/src/main/java/de/arcus/playmusicexporter2/dummy/DummyContent.java new file mode 100644 index 0000000..31b3eec --- /dev/null +++ b/app/src/main/java/de/arcus/playmusicexporter2/dummy/DummyContent.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.playmusicexporter2.dummy; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Helper class for providing sample content for user interfaces created by + * Android template wizards. + *

+ * TODO: Replace all uses of this class before publishing your app. + */ +public class DummyContent { + + /** + * An array of sample (dummy) items. + */ + public static List ITEMS = new ArrayList(); + + /** + * A map of sample (dummy) items, by ID. + */ + public static Map ITEM_MAP = new HashMap(); + + static { + // Add 3 sample items. + addItem(new DummyItem("1", "Item 1")); + addItem(new DummyItem("2", "Item 2")); + addItem(new DummyItem("3", "Item 3")); + } + + private static void addItem(DummyItem item) { + ITEMS.add(item); + ITEM_MAP.put(item.id, item); + } + + /** + * A dummy item representing a piece of content. + */ + public static class DummyItem { + public String id; + public String content; + + public DummyItem(String id, String content) { + this.id = id; + this.content = content; + } + + @Override + public String toString() { + return content; + } + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..96a442e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..359047d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..71c6d76 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..4df1894 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/layout/activity_track_detail.xml b/app/src/main/res/layout/activity_track_detail.xml new file mode 100644 index 0000000..a85f8a8 --- /dev/null +++ b/app/src/main/res/layout/activity_track_detail.xml @@ -0,0 +1,26 @@ + + + diff --git a/app/src/main/res/layout/activity_track_list.xml b/app/src/main/res/layout/activity_track_list.xml new file mode 100644 index 0000000..867fff7 --- /dev/null +++ b/app/src/main/res/layout/activity_track_list.xml @@ -0,0 +1,28 @@ + + + diff --git a/app/src/main/res/layout/activity_track_twopane.xml b/app/src/main/res/layout/activity_track_twopane.xml new file mode 100644 index 0000000..6ba238b --- /dev/null +++ b/app/src/main/res/layout/activity_track_twopane.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_track_detail.xml b/app/src/main/res/layout/fragment_track_detail.xml new file mode 100644 index 0000000..5407b49 --- /dev/null +++ b/app/src/main/res/layout/fragment_track_detail.xml @@ -0,0 +1,27 @@ + + + diff --git a/app/src/main/res/values-large/refs.xml b/app/src/main/res/values-large/refs.xml new file mode 100644 index 0000000..6d68c61 --- /dev/null +++ b/app/src/main/res/values-large/refs.xml @@ -0,0 +1,32 @@ + + + + + @layout/activity_track_twopane + diff --git a/app/src/main/res/values-sw600dp/refs.xml b/app/src/main/res/values-sw600dp/refs.xml new file mode 100644 index 0000000..6d68c61 --- /dev/null +++ b/app/src/main/res/values-sw600dp/refs.xml @@ -0,0 +1,32 @@ + + + + + @layout/activity_track_twopane + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..49c44b2 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,27 @@ + + + + + #ef6c00 + #e65100 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..bc13659 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,29 @@ + + + + + + Play Music Exporter + Track Detail + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..7dadf14 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..cfe9f42 --- /dev/null +++ b/build.gradle @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} diff --git a/framework/.gitignore b/framework/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/framework/.gitignore @@ -0,0 +1 @@ +/build diff --git a/framework/build.gradle b/framework/build.gradle new file mode 100644 index 0000000..f9b1aa3 --- /dev/null +++ b/framework/build.gradle @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + minSdkVersion 8 + targetSdkVersion 21 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:21.0.3' +} diff --git a/framework/framework.iml b/framework/framework.iml new file mode 100644 index 0000000..6275aa0 --- /dev/null +++ b/framework/framework.iml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/proguard-rules.pro b/framework/proguard-rules.pro new file mode 100644 index 0000000..28e4acc --- /dev/null +++ b/framework/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/david/android-sdks/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 *; +#} diff --git a/framework/src/androidTest/java/de/arcus/framework/ApplicationTest.java b/framework/src/androidTest/java/de/arcus/framework/ApplicationTest.java new file mode 100644 index 0000000..1dab1c7 --- /dev/null +++ b/framework/src/androidTest/java/de/arcus/framework/ApplicationTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.framework; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/framework/src/main/AndroidManifest.xml b/framework/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4c8936c --- /dev/null +++ b/framework/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/framework/src/main/java/de/arcus/framework/crashhandler/CrashActivity.java b/framework/src/main/java/de/arcus/framework/crashhandler/CrashActivity.java new file mode 100644 index 0000000..7921725 --- /dev/null +++ b/framework/src/main/java/de/arcus/framework/crashhandler/CrashActivity.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.framework.crashhandler; + +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.support.v4.app.ShareCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.TextView; + +import de.arcus.framework.logger.Logger; +import de.arcus.framework.R; + +public class CrashActivity extends ActionBarActivity { + // Extra flags + public static final String EXTRA_FLAG_CRASH_MESSAGE = "CRASH_TITLE"; + public static final String EXTRA_FLAG_CRASH_LOG = "CRASH_LOG"; + + /** + * StackTrace of the exception + */ + private String mCrashMessage; + + /** + * Log of the exception + */ + private String mCrashLog; + + /** + * The name of the app + */ + private String mAppName; + + /** + * The intent to start the app (for restart the app) + */ + private Intent mLaunchIntent; + /** + * Email address to send the crash log to + * Set this in AndroidManifest.xml: + */ + private String mMetaDataEmail; + + /** + * URL of a bugtracker or a support homepage + * Set this in AndroidManifest.xml: + */ + private String mMetaDataSupportURL; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_crash); + + // Reads the crash information + Bundle bundle = getIntent().getExtras(); + if (bundle != null) { + if (bundle.containsKey(EXTRA_FLAG_CRASH_MESSAGE)) + mCrashMessage = bundle.getString(EXTRA_FLAG_CRASH_MESSAGE); + if (bundle.containsKey(EXTRA_FLAG_CRASH_LOG)) + mCrashLog = bundle.getString(EXTRA_FLAG_CRASH_LOG); + } else { + // No information; close activity + finish(); + return; + } + + try { + // Get the PackageManager to load information about the app + PackageManager packageManager = getPackageManager(); + // Loads the ApplicationInfo with meta data + ApplicationInfo applicationInfo = packageManager.getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA); + + if (applicationInfo.metaData != null) { + // Reads the crash handler settings from meta data + if (applicationInfo.metaData.containsKey("crashhandler.email")) + mMetaDataEmail = applicationInfo.metaData.getString("crashhandler.email"); + if (applicationInfo.metaData.containsKey("crashhandler.supporturl")) + mMetaDataSupportURL = applicationInfo.metaData.getString("crashhandler.supporturl"); + } + + // Gets the app name + mAppName = packageManager.getApplicationLabel(applicationInfo).toString(); + // Gets the launch intent for the restart + mLaunchIntent = packageManager.getLaunchIntentForPackage(getPackageName()); + + } catch (PackageManager.NameNotFoundException ex) { + // If this occurs then god must be already dead + Logger.getInstance().logError("CrashHandler", ex.toString()); + } + + // Set the action bar title + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(getString(R.string.crashhandler_app_has_stopped_working, mAppName)); + } + + // Set the message + TextView textViewMessage = (TextView) findViewById(R.id.text_view_crash_message); + if (textViewMessage != null) { + textViewMessage.setText(mCrashLog); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_crash, menu); + + // Hide the email button if there is no crashhandler.email in AndroidManifest.xml + menu.findItem(R.id.action_email).setVisible(!TextUtils.isEmpty(mMetaDataEmail)); + + // Hide the homepage if there is no crashhandler.supporturl in AndroidManifest.xml + menu.findItem(R.id.action_support).setVisible(!TextUtils.isEmpty(mMetaDataSupportURL)); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + if (id == R.id.action_restart) { + // Close this window + finish(); + + // Restart the app + startActivity(mLaunchIntent); + } else if (id == R.id.action_email) { + // Send email + ShareCompat.IntentBuilder builder = ShareCompat.IntentBuilder.from(this); + builder.setType("message/rfc822"); + builder.addEmailTo(mMetaDataEmail); + builder.setSubject("Crash log for " + mAppName); + builder.setChooserTitle(R.string.crashhandler_choose_email_title); + builder.setText(mCrashLog); + builder.startChooser(); + } else if (id == R.id.action_support) { + // Open Homepage + Intent intentUrl = new Intent(Intent.ACTION_VIEW, Uri.parse(mMetaDataSupportURL)); + startActivity(intentUrl); + + } else if (id == R.id.action_close_dialog) { + // Close this window + finish(); + } else { // Other + return super.onOptionsItemSelected(item); + } + + + // One of our items was selected + return true; + } +} diff --git a/framework/src/main/java/de/arcus/framework/crashhandler/CrashHandler.java b/framework/src/main/java/de/arcus/framework/crashhandler/CrashHandler.java new file mode 100644 index 0000000..972e0cb --- /dev/null +++ b/framework/src/main/java/de/arcus/framework/crashhandler/CrashHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.framework.crashhandler; + +import android.app.Activity; +import android.content.Intent; + +import de.arcus.framework.logger.Logger; + +/** + * Handles crashes of activities and shows a nice dialog with + * options to send the developer an email with the crash log + * + * Use in onCreate in every activity: + * CrashHandler.addCrashHandler(this); + * + * Created by ds on 22.01.2015. + */ +public class CrashHandler implements Thread.UncaughtExceptionHandler { + /** + * Activity of the app + */ + private Activity mActivity; + + /** + * The default crash handler + */ + private Thread.UncaughtExceptionHandler mDefaultHandler; + + /** + * Addes a crash handler to the app context + * @param activity The activity of the app + */ + public static void addCrashHandler(Activity activity) + { + Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(activity)); + } + + public CrashHandler(Activity activity) + { + mActivity = activity; + mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); + } + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + // Log crash + Logger.getInstance().logError("CrashHandler", ex.toString()); + + StringBuilder logBuilder = new StringBuilder(); + + // Information + logBuilder.append("---------- Information -----------\n"); + logBuilder.append("PackageName: " + mActivity.getPackageName() + "\n"); + logBuilder.append("Crashed activity: " + mActivity.getLocalClassName() + "\n"); + + logBuilder.append("----------- Exception ------------\n"); + logBuilder.append(ex.getMessage() + "\n"); + + // Log stack trace + for (StackTraceElement stackTraceElement : ex.getStackTrace()) + { + logBuilder.append("\t" + stackTraceElement.toString() + "\n"); + } + + // Log Caused by + if (ex.getCause() != null) { + logBuilder.append("----------- Caused by ------------\n"); + logBuilder.append(ex.getCause().getMessage() + "\n"); + + // Log stack trace + for (StackTraceElement stackTraceElement : ex.getCause().getStackTrace()) + { + logBuilder.append("\t" + stackTraceElement.toString() + "\n"); + } + } + + // Opens a crash window + Intent intendCrash = new Intent(mActivity, CrashActivity.class); + intendCrash.putExtra(CrashActivity.EXTRA_FLAG_CRASH_MESSAGE, ex.getMessage()); + intendCrash.putExtra(CrashActivity.EXTRA_FLAG_CRASH_LOG, logBuilder.toString()); + intendCrash.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + mActivity.startActivity(intendCrash); + + // Close this app + System.exit(0); + } +} diff --git a/framework/src/main/java/de/arcus/framework/logger/Logger.java b/framework/src/main/java/de/arcus/framework/logger/Logger.java new file mode 100644 index 0000000..99da062 --- /dev/null +++ b/framework/src/main/java/de/arcus/framework/logger/Logger.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.framework.logger; + +import android.util.Log; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * Helper class to write into a log file and to logcat + */ +public class Logger { + /** + * Type of the log entry + */ + public enum LogEntryType { Verbose, Debug, Info, Warning, Error }; + + /** + * Instance of the logger + */ + private static Logger instance; + + /** + * Get the active instance of the Logger + * Creates an instance if not exists + * @return Gets the logger + */ + public static Logger getInstance() { + // Create new Instance + if (instance == null) + instance = new Logger(); + + return instance; + } + + /** + * List of all entries + */ + private Queue mEntryList = new LinkedList<>(); + + /** + * @return Gets the entry list + */ + public Queue getEntryList() { + return mEntryList; + } + + /** + * The minimum level to log messages + */ + private LogEntryType mLogLevel = LogEntryType.Debug; + + /** + * @return Gets the log level + */ + public LogEntryType getLogLevel() { + return mLogLevel; + } + + /** + * Sets the log level + * @param type The minimal log level to log messages + */ + public void setLogLevel(LogEntryType type) { + mLogLevel = type; + } + + /** + * Adds a verbose entry to the log + * @param tag Tag of the log entry (function or section) + * @param message Message of the log entry + */ + public void logVerbose(String tag, String message) { + log(LogEntryType.Verbose, tag, message); + } + + /** + * Adds a debug entry to the log + * @param tag Tag of the log entry (function or section) + * @param message Message of the log entry + */ + public void logDebug(String tag, String message) { + log(LogEntryType.Debug, tag, message); + } + + /** + * Adds a info entry to the log + * @param tag Tag of the log entry (function or section) + * @param message Message of the log entry + */ + public void logInfo(String tag, String message) { + log(LogEntryType.Info, tag, message); + } + + /** + * Adds a warning entry to the log + * @param tag Tag of the log entry (function or section) + * @param message Message of the log entry + */ + public void logWarning(String tag, String message) { + log(LogEntryType.Warning, tag, message); + } + + /** + * Adds a error entry to the log + * @param tag Tag of the log entry (function or section) + * @param message Message of the log entry + */ + public void logError(String tag, String message) { + log(LogEntryType.Error, tag, message); + } + + /** + * Adds a entry to the log + * @param type Entry type (Debug, Info, Warning, Error) + * @param tag Tag of the log entry (function or section) + * @param message Message of the log entry + */ + private void log(LogEntryType type, String tag, String message) { + // Entry type will be logged + if (type.ordinal() >= mLogLevel.ordinal()) + { + LogEntry logEntry = new LogEntry(type, tag, message); + + // LogCat + logEntry.writeToLogCat(); + + // Push entry to list + mEntryList.add(logEntry); + } + } + + /** + * A single log entry + */ + public class LogEntry { + /** + * Type of the log entry + */ + private LogEntryType mType; + + /** + * Tag of the log entry + */ + private String mTag; + + /** + * Message of the log entry + */ + private String mMessage; + + /** + * @return Gets the Type of the log entry + */ + public LogEntryType getType() { + return mType; + } + + /** + * @return Gets the tag of the log entry + */ + public String getTag() { + return mTag; + } + + /** + * @return Gets the message of the log entry + */ + public String getMessage() { + return mMessage; + } + + /** + * Creates a log entry + * @param type Entry type (Debug, Info, Warning, Error) + * @param tag Tag of the log entry (function or section) + * @param message Message of the log entry + */ + public LogEntry(LogEntryType type, String tag, String message) { + mType = type; + mTag = tag; + mMessage = message; + } + + /** + * Writes the entry to logcat + */ + public void writeToLogCat() { + switch (mType) { + case Verbose: + Log.v(mTag, mMessage); + break; + case Debug: + Log.d(mTag, mMessage); + break; + case Info: + Log.i(mTag, mMessage); + break; + case Warning: + Log.w(mTag, mMessage); + break; + case Error: + Log.e(mTag, mMessage); + break; + } + } + } +} diff --git a/framework/src/main/java/de/arcus/framework/settings/AppSettings.java b/framework/src/main/java/de/arcus/framework/settings/AppSettings.java new file mode 100644 index 0000000..049da97 --- /dev/null +++ b/framework/src/main/java/de/arcus/framework/settings/AppSettings.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.arcus.framework.settings; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * Helper class to read and write app settings without to care about to open and close an editor + */ +public class AppSettings { + /** + * The default settings file + */ + private static final String DEFAULT_SETTINGS_FILENAME = "app_settings"; + + /** + * The preferences + */ + private SharedPreferences mSharedPreferences; + + /** + * Creates a new instance of AppSettings that access to the default settings file + * @param context Context of the app + */ + public AppSettings(Context context) { + this(context, DEFAULT_SETTINGS_FILENAME); + } + + /** + * Creates a new instance of AppSettings that access to a specific settings file + * @param context Context of the app + */ + public AppSettings(Context context, String settingsFilename) { + mSharedPreferences = context.getSharedPreferences(settingsFilename, Context.MODE_PRIVATE); + } + + /** + * Gets a string from the settings + * @param key Key of the setting + * @param defValue Default value which is returned if the key doesn't exists + * @return Value + */ + public String getString(String key, String defValue) { + return mSharedPreferences.getString(key, defValue); + } + + /** + * Gets a boolean from the settings + * @param key Key of the setting + * @param defValue Default value which is returned if the key doesn't exists + * @return Value + */ + public boolean getBoolean(String key, boolean defValue) { + return mSharedPreferences.getBoolean(key, defValue); + } + + /** + * Gets a float from the settings + * @param key Key of the setting + * @param defValue Default value which is returned if the key doesn't exists + * @return Value + */ + public float getFloat(String key, float defValue) { + return mSharedPreferences.getFloat(key, defValue); + } + + /** + * Gets an int from the settings + * @param key Key of the setting + * @param defValue Default value which is returned if the key doesn't exists + * @return Value + */ + public int getInt(String key, int defValue) { + return mSharedPreferences.getInt(key, defValue); + } + + /** + * Gets a long from the settings + * @param key Key of the setting + * @param defValue Default value which is returned if the key doesn't exists + * @return Value + */ + public long getLong(String key, long defValue) { + return mSharedPreferences.getLong(key, defValue); + } + + /** + * Returns whether the settings contains a specific key + * @param key Key of the setting + * @return Returns whether the settings contains a specific key + */ + public boolean contains(String key) { + return mSharedPreferences.contains(key); + } + + /** + * Removes an setting from the settings + * @param key Key of the setting + */ + public void remove(String key) { + // Opens the editor + SharedPreferences.Editor editor = mSharedPreferences.edit(); + + // Removes the key + editor.remove(key); + + // Commits the change + editor.apply(); + } + + /** + * Saves a string to the settings + * @param key Key of the setting + * @param value Value + */ + public void setString(String key, String value) { + // Opens the editor + SharedPreferences.Editor editor = mSharedPreferences.edit(); + + editor.putString(key, value); + + // Commits the change + editor.apply(); + } + + /** + * Saves a boolean to the settings + * @param key Key of the setting + * @param value Value + */ + public void setBoolean(String key, boolean value) { + // Opens the editor + SharedPreferences.Editor editor = mSharedPreferences.edit(); + + editor.putBoolean(key, value); + + // Commits the change + editor.apply(); + } + + /** + * Saves a float to the settings + * @param key Key of the setting + * @param value Value + */ + public void setFloat(String key, float value) { + // Opens the editor + SharedPreferences.Editor editor = mSharedPreferences.edit(); + + editor.putFloat(key, value); + + // Commits the change + editor.apply(); + } + + /** + * Saves an int to the settings + * @param key Key of the setting + * @param value Value + */ + public void setInt(String key, int value) { + // Opens the editor + SharedPreferences.Editor editor = mSharedPreferences.edit(); + + editor.putInt(key, value); + + // Commits the change + editor.apply(); + } + + /** + * Saves a long to the settings + * @param key Key of the setting + * @param value Value + */ + public void setLong(String key, long value) { + // Opens the editor + SharedPreferences.Editor editor = mSharedPreferences.edit(); + + editor.putLong(key, value); + + // Commits the change + editor.apply(); + } + + + +} diff --git a/framework/src/main/java/de/arcus/framework/superuser/SuperUser.java b/framework/src/main/java/de/arcus/framework/superuser/SuperUser.java new file mode 100644 index 0000000..2d37544 --- /dev/null +++ b/framework/src/main/java/de/arcus/framework/superuser/SuperUser.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015. + */ + +package de.arcus.framework.superuser; + +import java.io.IOException; + +/** + * The superuser managers + * + * This static class handles the superuser session. + * Start the session with {@link #askForPermissions() askForPermissions}. + * To run a command create an instance of {@link SuperUserCommand SuperUserCommand} and {@link SuperUserCommand#execute() execute} it. + */ +public class SuperUser { + /** + * The su process + */ + private static Process mProcess; + + /** + * Gets the active su process + * @return Process + */ + static Process getProcess() { + return mProcess; + } + + /** + * Starts the superuser session + * To start the session in your app use {@link #askForPermissions()} + */ + private static boolean sessionStart() { + // Starts the su process + try { + mProcess = Runtime.getRuntime().exec("su"); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + /** + * Stops the superuser session + */ + public static void sessionStop() { + if (mProcess == null) return; + + // End the process + mProcess.destroy(); + mProcess = null; + } + + /** + * Gets whether the su session is running + * @return Return whether the su session is running + */ + public static boolean sessionIsRunning() { + if (mProcess == null) return false; + + // Hack to see if the process is running + // This is not nice, but there is no other way to check this + try { + mProcess.exitValue(); + return false; + } catch(IllegalThreadStateException ex) { + // Could not get the return value => process is running + return true; + } + } + + /** + * Checks whether superuser permissions were granted + * @return Return whether superuser permissions were granted + */ + public static boolean hasPermissions() { + // Just check whether the session is running + return sessionIsRunning(); + } + + /** + * This is like hasPermissions() but asks for the superuser permissions + * and give the user a change to grant it now. + * Use this to start you session + * @return Return whether superuser permissions were granted + */ + public static boolean askForPermissions() { + // We already have superuser permissions + if (hasPermissions()) return true; + + // Starts the process + if (sessionStart()) { + // Test for superuser + SuperUserCommand superUserCommand = new SuperUserCommand("whoami"); + if (superUserCommand.execute()) { + // Gets the whoami username + String[] output = superUserCommand.getStandardOutput(); + if (output.length >= 1 && output[0].equals("root")) { + // We are root + return true; + } + } + } + + // We don't have superuser permissions; abort session + sessionStop(); + return false; + } +} diff --git a/framework/src/main/java/de/arcus/framework/superuser/SuperUserCommand.java b/framework/src/main/java/de/arcus/framework/superuser/SuperUserCommand.java new file mode 100644 index 0000000..54b080d --- /dev/null +++ b/framework/src/main/java/de/arcus/framework/superuser/SuperUserCommand.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2015. + */ + +package de.arcus.framework.superuser; + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +/** + * This class executes superuser commands. + */ +public class SuperUserCommand { + /** + * The default timeout for each command in milliseconds + */ + private static final long DEFAULT_COMMAND_TIMEOUT = 30 * 1000; // 30 seconds + + private String[] mCommands = new String[] {}; + private String[] mOutputStandard = new String[] {}; + private String[] mOutputError = new String[] {}; + + private boolean mSuperUserFailed; + + /** + * The timeout for this command in milliseconds + */ + private long mTimeout; + + /** + * @return Gets the timeout for this command in milliseconds + */ + public long getTimeout() { + return mTimeout; + } + + /** + * Set the timeout for this command in milliseconds + * @param timeout Timeout + * @return Itself + */ + public SuperUserCommand setTimeout(long timeout) { + mTimeout = timeout; + return this; + } + + /** + * @return Gets the executed commands + */ + public String[] getCommands() { + return mCommands; + } + + /** + * @return Get the standard output + */ + public String[] getStandardOutput() { + return mOutputStandard; + } + + /** + * @return Get the error output + */ + public String[] getErrorOutput() { + return mOutputError; + } + + /** + * @return Gets whether the command was executed without errors, even without error outputs from the command. + */ + public boolean commandWasSuccessful() { + return (!mSuperUserFailed && mOutputError.length == 0); + } + + /** + * @return Gets whether the command was granted superuser permissions, but maybe has some error outputs. + */ + public boolean superuserWasSuccessful() { + return (!mSuperUserFailed); + } + + /** + * Creates a command with one command line + * @param command The command + */ + public SuperUserCommand(String command) { + this(new String[] {command}); + } + + /** + * Creates a command with multiple command lines + * @param commands The command lines + */ + public SuperUserCommand(String[] commands) { + mCommands = commands; + + // Default timeout + mTimeout = DEFAULT_COMMAND_TIMEOUT; + } + + /** + * Execute the command and return whether the command was executed. + * It will only return false if the app wasn't granted superuser permissions, like {@link #superuserWasSuccessful()}. + * It will also return true if the command itself returns error outputs. To check this case you should use {@link #commandWasSuccessful()} instead. + * @return Gets whether the execution was successful. + */ + public boolean execute() { + String tmpLine; + List tmpList = new ArrayList<>(); + + mSuperUserFailed = false; + + // Opps, we don't have superuser permissions + // Did you run SuperUser.askForPermissions()? + if (!SuperUser.hasPermissions()) { + mSuperUserFailed = true; + return false; + } + + try { + // Gets the streams + DataOutputStream dataOutputStream = new DataOutputStream(SuperUser.getProcess().getOutputStream()); + BufferedReader bufferedInputReader = new BufferedReader(new InputStreamReader(SuperUser.getProcess().getInputStream())); + BufferedReader bufferedErrorReader = new BufferedReader(new InputStreamReader(SuperUser.getProcess().getErrorStream())); + + // Sends the command + for (String command : mCommands) + dataOutputStream.writeBytes(command + "\n"); + dataOutputStream.flush(); + + // TODO: This class cannot execute commands without any output (standard and error). These commands will run until the timeout will kill them! + + // Start waiting + long timeStarted = System.currentTimeMillis(); + + // Wait for first data + while (!bufferedInputReader.ready() && !bufferedErrorReader.ready()) { + try { + // Waiting + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + long timeNow = System.currentTimeMillis(); + + // TimeOut + if (timeNow - timeStarted >= mTimeout) break; + } + + // Reads the standard output + tmpList.clear(); + while (bufferedInputReader.ready()) { + tmpLine = bufferedInputReader.readLine(); + + // End of data + if (tmpLine == null) break; + + Log.i("SuperUser", "> " + tmpLine); + tmpList.add(tmpLine); + } + // Convert list to array + mOutputStandard = tmpList.toArray(new String[tmpList.size()]); + + + // Reads the error output + tmpList.clear(); + while (bufferedErrorReader.ready()) { + tmpLine = bufferedErrorReader.readLine(); + + // End of data + if (tmpLine == null) break; + Log.e("SuperUser", "> " + tmpLine); + + tmpList.add(tmpLine); + } + // Convert list to array + mOutputError = tmpList.toArray(new String[tmpList.size()]); + + // Done + return true; + } catch (IOException e) { + e.printStackTrace(); + + mSuperUserFailed = true; + + // Command failed + return false; + } + } +} diff --git a/framework/src/main/res/layout/activity_crash.xml b/framework/src/main/res/layout/activity_crash.xml new file mode 100644 index 0000000..91ceff7 --- /dev/null +++ b/framework/src/main/res/layout/activity_crash.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/framework/src/main/res/menu/menu_crash.xml b/framework/src/main/res/menu/menu_crash.xml new file mode 100644 index 0000000..8015fe6 --- /dev/null +++ b/framework/src/main/res/menu/menu_crash.xml @@ -0,0 +1,35 @@ + + + +

+ + + + + \ No newline at end of file diff --git a/framework/src/main/res/values-de/strings_crashhandler.xml b/framework/src/main/res/values-de/strings_crashhandler.xml new file mode 100644 index 0000000..46cbe30 --- /dev/null +++ b/framework/src/main/res/values-de/strings_crashhandler.xml @@ -0,0 +1,17 @@ + + + + + %1$s funktioniert nicht mehr! + + Bericht + + Absturzbericht senden + + App neustarten + Bereicht per E-Mail senden + Support-Homepage öffnen + Schließen + \ No newline at end of file diff --git a/framework/src/main/res/values-w820dp/dimens.xml b/framework/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..62df187 --- /dev/null +++ b/framework/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/framework/src/main/res/values/dimens.xml b/framework/src/main/res/values/dimens.xml new file mode 100644 index 0000000..295b5a9 --- /dev/null +++ b/framework/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/framework/src/main/res/values/strings_crashhandler.xml b/framework/src/main/res/values/strings_crashhandler.xml new file mode 100644 index 0000000..0cbf693 --- /dev/null +++ b/framework/src/main/res/values/strings_crashhandler.xml @@ -0,0 +1,35 @@ + + + + + %1$s has stopped working! + + Log + + Send crash log + + Restart app + Send log via email + Open support homepage + Close + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..38ba071 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,40 @@ +# +# Copyright (c) 2015 David Schulte +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..dd57d81 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2015 David Schulte +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +#Wed Apr 10 15:27:10 PDT 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..7bd332e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2015 David Schulte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +include ':app', ':framework'