From d91918002d47c4c286304a10be77439a02c41971 Mon Sep 17 00:00:00 2001 From: aNNiMON Date: Wed, 14 Feb 2024 00:02:51 +0200 Subject: [PATCH] Initial commit --- .gitignore | 7 + WebcamViewer.iml | 11 + app/.gitignore | 1 + app/app.iml | 109 +++++++++ app/build.gradle | 51 ++++ app/proguard-rules.pro | 18 ++ app/src/main/AndroidManifest.xml | 25 ++ .../webcamviewer/ExceptionHandler.java | 39 +++ .../annimon/webcamviewer/MainActivity.java | 77 ++++++ .../NavigationDrawerCallbacks.java | 5 + .../NavigationDrawerFragment.java | 231 ++++++++++++++++++ .../annimon/webcamviewer/NavigationItem.java | 33 +++ .../java/com/annimon/webcamviewer/Webcam.java | 34 +++ .../annimon/webcamviewer/WebcamAdapter.java | 101 ++++++++ .../com/annimon/webcamviewer/WebcamGroup.java | 49 ++++ .../webcamviewer/WebcamListLoader.java | 130 ++++++++++ .../webcamviewer/WebcamViewerFragment.java | 118 +++++++++ .../com/blundell/tut/LoaderImageView.java | 214 ++++++++++++++++ .../main/res/drawable-hdpi/ic_menu_check.png | Bin 0 -> 212 bytes .../drawable-hdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 840 bytes app/src/main/res/drawable-hdpi/no_image.png | Bin 0 -> 3408 bytes .../main/res/drawable-mdpi/ic_menu_check.png | Bin 0 -> 163 bytes .../drawable-mdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 578 bytes .../main/res/drawable-xhdpi/ic_menu_check.png | Bin 0 -> 232 bytes .../ic_refresh_grey600_24dp.png | Bin 0 -> 1088 bytes .../res/drawable-xxhdpi/ic_menu_check.png | Bin 0 -> 329 bytes .../ic_refresh_grey600_24dp.png | Bin 0 -> 1568 bytes .../ic_refresh_grey600_24dp.png | Bin 0 -> 2070 bytes app/src/main/res/drawable/row_selector.xml | 5 + app/src/main/res/drawable/wallpaper.png | Bin 0 -> 3796 bytes app/src/main/res/layout/activity_main.xml | 38 +++ app/src/main/res/layout/drawer_group.xml | 15 ++ app/src/main/res/layout/drawer_row.xml | 14 ++ .../res/layout/fragment_navigation_drawer.xml | 56 +++++ app/src/main/res/layout/toolbar_default.xml | 7 + app/src/main/res/menu/main.xml | 4 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1516 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 813 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2216 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4506 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6892 bytes app/src/main/res/values-v21/styles.xml | 28 +++ app/src/main/res/values/colors.xml | 21 ++ app/src/main/res/values/dimens.xml | 12 + app/src/main/res/values/strings.xml | 7 + app/src/main/res/values/styles.xml | 28 +++ build.gradle | 19 ++ gradle.properties | 18 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++++++++++++ gradlew.bat | 90 +++++++ settings.gradle | 1 + 53 files changed, 1786 insertions(+) create mode 100644 .gitignore create mode 100644 WebcamViewer.iml create mode 100644 app/.gitignore create mode 100644 app/app.iml create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/annimon/webcamviewer/ExceptionHandler.java create mode 100644 app/src/main/java/com/annimon/webcamviewer/MainActivity.java create mode 100644 app/src/main/java/com/annimon/webcamviewer/NavigationDrawerCallbacks.java create mode 100644 app/src/main/java/com/annimon/webcamviewer/NavigationDrawerFragment.java create mode 100644 app/src/main/java/com/annimon/webcamviewer/NavigationItem.java create mode 100644 app/src/main/java/com/annimon/webcamviewer/Webcam.java create mode 100644 app/src/main/java/com/annimon/webcamviewer/WebcamAdapter.java create mode 100644 app/src/main/java/com/annimon/webcamviewer/WebcamGroup.java create mode 100644 app/src/main/java/com/annimon/webcamviewer/WebcamListLoader.java create mode 100644 app/src/main/java/com/annimon/webcamviewer/WebcamViewerFragment.java create mode 100644 app/src/main/java/com/blundell/tut/LoaderImageView.java create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_check.png create mode 100644 app/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/no_image.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_check.png create mode 100644 app/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_check.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu_check.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png create mode 100644 app/src/main/res/drawable/row_selector.xml create mode 100644 app/src/main/res/drawable/wallpaper.png create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/drawer_group.xml create mode 100644 app/src/main/res/layout/drawer_row.xml create mode 100644 app/src/main/res/layout/fragment_navigation_drawer.xml create mode 100644 app/src/main/res/layout/toolbar_default.xml create mode 100644 app/src/main/res/menu/main.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/values-v21/styles.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c4de58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/WebcamViewer.iml b/WebcamViewer.iml new file mode 100644 index 0000000..f1120b1 --- /dev/null +++ b/WebcamViewer.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file 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..59dafff --- /dev/null +++ b/app/app.iml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..95162f2 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,51 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.2.3' + classpath 'me.tatarka:gradle-retrolambda:3.2.4' + } +} +apply plugin: 'com.android.application' +apply plugin: 'me.tatarka.retrolambda' + +repositories { + jcenter() +} + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + applicationId "com.annimon.webcamviewer" + minSdkVersion 14 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + 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:23.1.1' + compile 'com.android.support:support-v4:23.1.1' + compile 'com.android.support:recyclerview-v7:23.1.1' + compile 'com.android.support:design:23.1.1' + compile 'com.bignerdranch.android:expandablerecyclerview:2.0.4' + compile 'com.annimon:stream:1.0.6' + compile 'com.squareup.okhttp3:okhttp:3.1.2' + compile 'me.zhanghai.android.materialprogressbar:library:1.1.4' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..726a7fc --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,18 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in D:/Android/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 *; +#} +-dontwarn java.lang.invoke.* \ 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..aa49957 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/annimon/webcamviewer/ExceptionHandler.java b/app/src/main/java/com/annimon/webcamviewer/ExceptionHandler.java new file mode 100644 index 0000000..2614aba --- /dev/null +++ b/app/src/main/java/com/annimon/webcamviewer/ExceptionHandler.java @@ -0,0 +1,39 @@ +package com.annimon.webcamviewer; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.util.Log; + +/** + * Handling exceptions. + * + * @author aNNiMON + */ +public class ExceptionHandler { + + private static final boolean DEBUG = true; + private static final String TAG = "webcamviewer"; + + public static void log(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + public static void log(String message, Throwable ex) { + if (DEBUG) { + Log.e(TAG, message, ex); + } + } + + public static void log(Exception ex) { + if (DEBUG) { + Log.e(TAG, getErrorMessage(ex)); + } + } + + private static String getErrorMessage(Exception ex) { + return ex.getMessage(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/annimon/webcamviewer/MainActivity.java b/app/src/main/java/com/annimon/webcamviewer/MainActivity.java new file mode 100644 index 0000000..a34ccfb --- /dev/null +++ b/app/src/main/java/com/annimon/webcamviewer/MainActivity.java @@ -0,0 +1,77 @@ +package com.annimon.webcamviewer; + +import android.os.Bundle; +import android.support.design.widget.Snackbar; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +public class MainActivity extends AppCompatActivity implements NavigationDrawerCallbacks { + + private Toolbar mToolbar; + private View mRootView; + private NavigationDrawerFragment mNavigationDrawerFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mToolbar = (Toolbar) findViewById(R.id.toolbar_actionbar); + setSupportActionBar(mToolbar); + + mRootView = findViewById(R.id.root); + + mNavigationDrawerFragment = (NavigationDrawerFragment) + getSupportFragmentManager().findFragmentById(R.id.fragment_drawer); + new WebcamListLoader(this, mNavigationDrawerFragment).execute(WebcamListLoader.NORMAL); + mNavigationDrawerFragment.setup(R.id.fragment_drawer, (DrawerLayout) findViewById(R.id.drawer), mToolbar); + mNavigationDrawerFragment.configureNavHeader(); + } + + @Override + public void onNavigationDrawerItemSelected(int position, Webcam webcam) { + Snackbar.make(mRootView, "Menu item selected -> " + position + "\n" + webcam, Snackbar.LENGTH_SHORT).show(); + + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.container, WebcamViewerFragment.newInstance(webcam)) + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + .commit(); + mNavigationDrawerFragment.closeDrawer(); + } + + @Override + public void onBackPressed() { + if (mNavigationDrawerFragment.isDrawerOpen()) { + mNavigationDrawerFragment.closeDrawer(); + } else { + super.onBackPressed(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (!mNavigationDrawerFragment.isDrawerOpen()) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_settings: + return true; + + default: + return super.onOptionsItemSelected(item); + } + } +} diff --git a/app/src/main/java/com/annimon/webcamviewer/NavigationDrawerCallbacks.java b/app/src/main/java/com/annimon/webcamviewer/NavigationDrawerCallbacks.java new file mode 100644 index 0000000..c9d099b --- /dev/null +++ b/app/src/main/java/com/annimon/webcamviewer/NavigationDrawerCallbacks.java @@ -0,0 +1,5 @@ +package com.annimon.webcamviewer; + +public interface NavigationDrawerCallbacks { + void onNavigationDrawerItemSelected(int position, Webcam webcam); +} diff --git a/app/src/main/java/com/annimon/webcamviewer/NavigationDrawerFragment.java b/app/src/main/java/com/annimon/webcamviewer/NavigationDrawerFragment.java new file mode 100644 index 0000000..ff23030 --- /dev/null +++ b/app/src/main/java/com/annimon/webcamviewer/NavigationDrawerFragment.java @@ -0,0 +1,231 @@ +package com.annimon.webcamviewer; + +import android.app.Activity; +import android.content.Context; +import android.support.v4.app.Fragment; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; + +import java.util.ArrayList; +import java.util.List; + +/** + * Fragment used for managing interactions for and presentation of a navigation drawer. + * See the + * design guidelines for a complete explanation of the behaviors implemented here. + */ +public class NavigationDrawerFragment extends Fragment implements NavigationDrawerCallbacks { + + /** + * Remember the position of the selected item. + */ + private static final String STATE_SELECTED_POSITION = "selected_navigation_drawer_position"; + + /** + * Per the design guidelines, you should show the drawer on launch until the user manually + * expands it. This shared preference tracks this. + */ + private static final String PREF_USER_LEARNED_DRAWER = "navigation_drawer_learned"; + + /** + * A pointer to the current callbacks instance (the Activity). + */ + private NavigationDrawerCallbacks mCallbacks; + + /** + * Helper component that ties the action bar to the navigation drawer. + */ + private ActionBarDrawerToggle mActionBarDrawerToggle; + + private DrawerLayout mDrawerLayout; + private RecyclerView mDrawerList; + private View mFragmentContainerView; + + private int mCurrentSelectedPosition = 0; + private boolean mFromSavedInstanceState; + private boolean mUserLearnedDrawer; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Read in the flag indicating whether or not the user has demonstrated awareness of the + // drawer. See PREF_USER_LEARNED_DRAWER for details. + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity()); + mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false); + + if (savedInstanceState != null) { + mCurrentSelectedPosition = savedInstanceState.getInt(STATE_SELECTED_POSITION); + mFromSavedInstanceState = true; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_navigation_drawer, container, false); + mDrawerList = (RecyclerView) view.findViewById(R.id.drawerList); + LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity()); + layoutManager.setOrientation(LinearLayoutManager.VERTICAL); + mDrawerList.setLayoutManager(layoutManager); + mDrawerList.setHasFixedSize(true); + return view; + } + + public boolean isDrawerOpen() { + return mDrawerLayout != null && mDrawerLayout.isDrawerOpen(mFragmentContainerView); + } + + public ActionBarDrawerToggle getActionBarDrawerToggle() { + return mActionBarDrawerToggle; + } + + public DrawerLayout getDrawerLayout() { + return mDrawerLayout; + } + + @Override + public void onNavigationDrawerItemSelected(int position, Webcam webcam) { + selectItem(position, webcam); + } + + + public void updateAdapter(WebcamAdapter adapter) { + adapter.setNavigationDrawerCallbacks(this); + mDrawerList.setAdapter(adapter); +// selectItem(mCurrentSelectedPosition); + } + + /** + * Users of this fragment must call this method to set up the navigation drawer interactions. + * + * @param fragmentId The android:id of this fragment in its activity's layout. + * @param drawerLayout The DrawerLayout containing this fragment's UI. + * @param toolbar The Toolbar of the activity. + */ + public void setup(int fragmentId, DrawerLayout drawerLayout, Toolbar toolbar) { + mFragmentContainerView = getActivity().findViewById(fragmentId); + mDrawerLayout = drawerLayout; + + mDrawerLayout.setStatusBarBackgroundColor(ContextCompat.getColor(getActivity(), R.color.primary)); + + mActionBarDrawerToggle = new ActionBarDrawerToggle(getActivity(), mDrawerLayout, toolbar, R.string.drawer_open, R.string.drawer_close) { + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); + if (!isAdded()) return; + + getActivity().supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu() + } + + @Override + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + if (!isAdded()) return; + if (!mUserLearnedDrawer) { + mUserLearnedDrawer = true; + SharedPreferences sp = PreferenceManager + .getDefaultSharedPreferences(getActivity()); + sp.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).apply(); + } + getActivity().supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu() + } + }; + + // If the user hasn't 'learned' about the drawer, open it to introduce them to the drawer, + // per the navigation drawer design guidelines. + if (!mUserLearnedDrawer && !mFromSavedInstanceState) { + mDrawerLayout.openDrawer(mFragmentContainerView); + } + + // Defer code dependent on restoration of previous instance state. + mDrawerLayout.post(mActionBarDrawerToggle::syncState); + + mDrawerLayout.setDrawerListener(mActionBarDrawerToggle); + //selectItem(mCurrentSelectedPosition); + } + + private void selectItem(int position, Webcam webcam) { + mCurrentSelectedPosition = position; + if (mDrawerLayout != null) { + mDrawerLayout.closeDrawer(mFragmentContainerView); + } + if (mCallbacks != null) { + mCallbacks.onNavigationDrawerItemSelected(position, webcam); + } + ((WebcamAdapter) mDrawerList.getAdapter()).selectPosition(position); + } + + public void openDrawer() { + mDrawerLayout.openDrawer(mFragmentContainerView); + } + + public void closeDrawer() { + mDrawerLayout.closeDrawer(mFragmentContainerView); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + mCallbacks = (NavigationDrawerCallbacks) context; + } catch (ClassCastException e) { + throw new ClassCastException("Activity must implement NavigationDrawerCallbacks."); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(STATE_SELECTED_POSITION, mCurrentSelectedPosition); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // Forward the new configuration the drawer toggle component. + mActionBarDrawerToggle.onConfigurationChanged(newConfig); + } + + public void configureNavHeader() { + MaterialProgressBar progressBar = (MaterialProgressBar) mFragmentContainerView.findViewById(R.id.progressBar); + mFragmentContainerView.findViewById(R.id.updateWebcams).setOnClickListener(v -> { + new WebcamListLoader(getActivity(), this, progressBar).execute(WebcamListLoader.UPDATE); + }); + } + + public View getGoogleDrawer() { + return mFragmentContainerView.findViewById(R.id.googleDrawer); + } +} diff --git a/app/src/main/java/com/annimon/webcamviewer/NavigationItem.java b/app/src/main/java/com/annimon/webcamviewer/NavigationItem.java new file mode 100644 index 0000000..dc5be5c --- /dev/null +++ b/app/src/main/java/com/annimon/webcamviewer/NavigationItem.java @@ -0,0 +1,33 @@ +package com.annimon.webcamviewer; + + +import android.graphics.drawable.Drawable; + +/** + * Created by poliveira on 24/10/2014. + */ +public class NavigationItem { + private String mText; + private Drawable mDrawable; + + public NavigationItem(String text, Drawable drawable) { + mText = text; + mDrawable = drawable; + } + + public String getText() { + return mText; + } + + public void setText(String text) { + mText = text; + } + + public Drawable getDrawable() { + return mDrawable; + } + + public void setDrawable(Drawable drawable) { + mDrawable = drawable; + } +} diff --git a/app/src/main/java/com/annimon/webcamviewer/Webcam.java b/app/src/main/java/com/annimon/webcamviewer/Webcam.java new file mode 100644 index 0000000..1814503 --- /dev/null +++ b/app/src/main/java/com/annimon/webcamviewer/Webcam.java @@ -0,0 +1,34 @@ +package com.annimon.webcamviewer; + +public class Webcam { + + private final String name, url; + private final int updateInterval; + + public Webcam(String name, String url) { + this(name, url, 1000); + } + + public Webcam(String name, String url, int updateInterval) { + this.name = name; + this.url = url; + this.updateInterval = updateInterval; + } + + public String getName() { + return name; + } + + public String getUrl() { + return url; + } + + public int getUpdateInterval() { + return updateInterval; + } + + @Override + public String toString() { + return String.format("%s: %s", name, url); + } +} diff --git a/app/src/main/java/com/annimon/webcamviewer/WebcamAdapter.java b/app/src/main/java/com/annimon/webcamviewer/WebcamAdapter.java new file mode 100644 index 0000000..346c9e4 --- /dev/null +++ b/app/src/main/java/com/annimon/webcamviewer/WebcamAdapter.java @@ -0,0 +1,101 @@ +package com.annimon.webcamviewer; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import com.bignerdranch.expandablerecyclerview.Adapter.ExpandableRecyclerAdapter; +import com.bignerdranch.expandablerecyclerview.Model.ParentListItem; +import com.bignerdranch.expandablerecyclerview.ViewHolder.ChildViewHolder; +import com.bignerdranch.expandablerecyclerview.ViewHolder.ParentViewHolder; +import java.util.List; + +public class WebcamAdapter extends ExpandableRecyclerAdapter { + + private LayoutInflater mInflater; + private NavigationDrawerCallbacks mNavigationDrawerCallbacks; + private View mSelectedView; + private int mSelectedPosition; + + public WebcamAdapter(Context context, List data) { + super(data); + this.mInflater = LayoutInflater.from(context); + } + + public NavigationDrawerCallbacks getNavigationDrawerCallbacks() { + return mNavigationDrawerCallbacks; + } + + public void setNavigationDrawerCallbacks(NavigationDrawerCallbacks navigationDrawerCallbacks) { + mNavigationDrawerCallbacks = navigationDrawerCallbacks; + } + + public void selectPosition(int position) { + mSelectedPosition = position; + notifyItemChanged(position); + } + + @Override + public WebcamGroupViewHolder onCreateParentViewHolder(ViewGroup parent) { + final View view = mInflater.inflate(R.layout.drawer_group, parent, false); + return new WebcamGroupViewHolder(view); + } + + @Override + public WebcamViewHolder onCreateChildViewHolder(ViewGroup parent) { + final View view = mInflater.inflate(R.layout.drawer_row, parent, false); + final WebcamViewHolder viewHolder = new WebcamViewHolder(view); + + viewHolder.mNameTextView.setClickable(true); + viewHolder.mNameTextView.setOnClickListener(v -> { + if (mSelectedView != null) { + mSelectedView.setSelected(false); + } + final int adapterPosition = viewHolder.getAdapterPosition(); + mSelectedPosition = adapterPosition; + v.setSelected(true); + mSelectedView = v; + if (mNavigationDrawerCallbacks != null) { + mNavigationDrawerCallbacks.onNavigationDrawerItemSelected( + adapterPosition, + (Webcam) getListItem(adapterPosition) + ); + } + }); +// viewHolder.mNameTextView.setBackgroundResource(R.drawable.row_selector); + return viewHolder; + } + + @Override + public void onBindParentViewHolder(WebcamGroupViewHolder webcamGroupViewHolder, int i, ParentListItem parentListItem) { + final WebcamGroup webcamGroup = (WebcamGroup) parentListItem; + webcamGroupViewHolder.mGroupNameTextView.setText(webcamGroup.getName()); + } + + @Override + public void onBindChildViewHolder(WebcamViewHolder webcamViewHolder, int i, Object childListItem) { + final Webcam webcam = (Webcam) childListItem; + webcamViewHolder.mNameTextView.setText(webcam.getName()); + } + + + public static class WebcamGroupViewHolder extends ParentViewHolder { + public TextView mGroupNameTextView; + + public WebcamGroupViewHolder(View itemView) { + super(itemView); + mGroupNameTextView = (TextView) itemView.findViewById(R.id.item_name); + } + } + + public static class WebcamViewHolder extends ChildViewHolder { + public TextView mNameTextView; + + public WebcamViewHolder(View itemView) { + super(itemView); + mNameTextView = (TextView) itemView.findViewById(R.id.item_name); + } + } + +} diff --git a/app/src/main/java/com/annimon/webcamviewer/WebcamGroup.java b/app/src/main/java/com/annimon/webcamviewer/WebcamGroup.java new file mode 100644 index 0000000..a5f0f2d --- /dev/null +++ b/app/src/main/java/com/annimon/webcamviewer/WebcamGroup.java @@ -0,0 +1,49 @@ +package com.annimon.webcamviewer; + +import com.annimon.stream.Collectors; +import com.annimon.stream.Stream; +import com.bignerdranch.expandablerecyclerview.Model.ParentListItem; + +import java.util.List; + +public class WebcamGroup implements ParentListItem { + + private final String name; + private final boolean enabled; + private final List webcams; + private List mChildrenList; + + public WebcamGroup(String name, boolean enabled, List webcams) { + this.name = name; + this.enabled = enabled; + this.webcams = webcams; + mChildrenList = Stream.of(webcams).collect(Collectors.toList()); + } + + public String getName() { + return name; + } + + public boolean isEnabled() { + return enabled; + } + + public List getWebcams() { + return webcams; + } + + @Override + public String toString() { + return String.format("%s, %s", name, webcams); + } + + @Override + public List getChildItemList() { + return webcams; + } + + @Override + public boolean isInitiallyExpanded() { + return false; + } +} diff --git a/app/src/main/java/com/annimon/webcamviewer/WebcamListLoader.java b/app/src/main/java/com/annimon/webcamviewer/WebcamListLoader.java new file mode 100644 index 0000000..7893ca0 --- /dev/null +++ b/app/src/main/java/com/annimon/webcamviewer/WebcamListLoader.java @@ -0,0 +1,130 @@ +package com.annimon.webcamviewer; + +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; +import android.view.View; +import android.widget.ProgressBar; +import com.annimon.stream.Exceptional; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONArray; +import org.json.JSONObject; +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +public class WebcamListLoader extends AsyncTask> { + + private static final String WEBCAM_LIST_URL = "http://projects.annimon.com/projects/webcam-urls.json"; + private static final String WEBCAM_LIST_FILENAME = "webcam-urls.json"; + + public static final int NORMAL = 0, UPDATE = 1; + + private final Context mContext; + private final NavigationDrawerFragment mNavigationDrawerFragment; + private final ProgressBar mProgressBar; + + public WebcamListLoader(Context context, NavigationDrawerFragment fragment) { + this(context, fragment, null); + } + + public WebcamListLoader(Context context, NavigationDrawerFragment fragment, ProgressBar progressbar) { + this.mContext = context; + this.mNavigationDrawerFragment = fragment; + this.mProgressBar = progressbar; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (mProgressBar != null) { + mProgressBar.setIndeterminate(true); + mProgressBar.setVisibility(View.VISIBLE); + } + } + + @Override + protected List doInBackground(Integer... params) { + return Exceptional.of(() -> { + final File file = mContext.getFileStreamPath(WEBCAM_LIST_FILENAME); + ExceptionHandler.log(WEBCAM_LIST_FILENAME + " exists: " + file.exists()); + final String jsonRaw; + if (params[0] == NORMAL && file.exists()) { + // Load from internal storage + jsonRaw = loadFile(file); + } else { + // Retrieve from web + jsonRaw = loadUrl(WEBCAM_LIST_URL); + } + ExceptionHandler.log("JSONRAW length: " + jsonRaw.length()); + + final List webcamGroups = new ArrayList<>(); + final JSONArray groups = new JSONObject(jsonRaw).getJSONArray("groups"); + final int length = groups.length(); + for (int i = 0; i < length; i++) { + final JSONObject group = groups.getJSONObject(i); + final String groupName = group.getString("name"); + final boolean isGroupEnabled = group.optBoolean("enabled", true); + if (!isGroupEnabled) continue; + + final JSONArray urls = group.getJSONArray("urls"); + final int urlsLength = urls.length(); + final List webcams = new ArrayList<>(urlsLength); + for (int j = 0; j < urlsLength; j++) { + final JSONObject url = urls.getJSONObject(j); + webcams.add(new Webcam(url.getString("name"), url.getString("url"))); + } + + webcamGroups.add(new WebcamGroup(groupName, isGroupEnabled, webcams)); + } + + ExceptionHandler.log("Json parsed successfully"); + ExceptionHandler.log("Got " + webcamGroups.size() + " groups"); + + try (final OutputStream os = new FileOutputStream(file); + final OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8")) { + osw.write(jsonRaw); + osw.flush(); + } + + ExceptionHandler.log("Successfully wrote to internal file"); + + return webcamGroups; + }).ifException(e -> ExceptionHandler.log("WebcamListLoader: " + e.getMessage(), e)) + .getOrElse(new ArrayList<>()); + } + + private String loadUrl(String url) throws IOException { + final OkHttpClient client = new OkHttpClient(); + final Request request = new Request.Builder() + .url(url) + .build(); + final Response response = client.newCall(request).execute(); + return response.body().string(); + } + + private String loadFile(File file) throws IOException { + try (final InputStream is = new FileInputStream(file); + final InputStreamReader isr = new InputStreamReader(is, "UTF-8"); + final BufferedReader reader = new BufferedReader(isr)) { + final StringBuilder sb = new StringBuilder(); + String line; + while ( (line = reader.readLine()) != null ) { + sb.append(line); + } + return sb.toString(); + } + } + + @Override + protected void onPostExecute(List webcams) { + super.onPostExecute(webcams); + if (mProgressBar != null) { + mProgressBar.setVisibility(View.INVISIBLE); + mProgressBar.setIndeterminate(false); + } + mNavigationDrawerFragment.updateAdapter(new WebcamAdapter(mContext, webcams)); + } +} diff --git a/app/src/main/java/com/annimon/webcamviewer/WebcamViewerFragment.java b/app/src/main/java/com/annimon/webcamviewer/WebcamViewerFragment.java new file mode 100644 index 0000000..b80cf05 --- /dev/null +++ b/app/src/main/java/com/annimon/webcamviewer/WebcamViewerFragment.java @@ -0,0 +1,118 @@ +package com.annimon.webcamviewer; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.os.Bundle; +import android.os.Environment; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.blundell.tut.LoaderImageView; + +public class WebcamViewerFragment extends Fragment { + + private static final String ARG_URL = "url"; + + private String imageUrl; + private LoaderImageView contentImage; + + private boolean autoUpdate; + private Timer timer; + + public static WebcamViewerFragment newInstance(Webcam webcam) { + final WebcamViewerFragment instance = new WebcamViewerFragment(); + final Bundle args = new Bundle(); + args.putString(ARG_URL, webcam.getUrl()); + instance.setArguments(args); + return instance; + } + + public WebcamViewerFragment() { + autoUpdate = false; + timer = new Timer(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + setHasOptionsMenu(true); + contentImage = new LoaderImageView(getActivity(), (String) null); + imageUrl = getArguments().getString(ARG_URL); + update(); + return contentImage; + } + + /*@Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_auto_update: + autoUpdate = !autoUpdate; + autoUpdate(); + break; + case R.id.menu_update: + update(); + break; + case R.id.menu_save: + try { + save("image" + File.separator + "Донецк"); + Toast.makeText(getActivity(), "Сохранено", Toast.LENGTH_SHORT).show(); + } catch (IOException ex) { + ex.printStackTrace(); + } + break; + case R.id.menu_videos: + final String camimg = "/camimg/cam"; + final int start = imageUrl.indexOf(camimg) + camimg.length(); + String camId = imageUrl.substring(start, imageUrl.indexOf("-5.jpg")); + Intent intent = new Intent(getActivity(), VideoGetActivity.class); + intent.putExtra("camera_id", Integer.parseInt(camId)); + startActivity(intent); + break; + } + return true; + }*/ + + public void update() { + contentImage.post(() -> contentImage.setImageDrawable(imageUrl)); + } + + private void autoUpdate() { + TimerTask task = new TimerTask() { + + @Override + public void run() { + update(); + } + }; + if (autoUpdate) { + timer.schedule(task, 500, 100); + contentImage.setQuickMode(true); + } else { + timer.cancel(); + timer = new Timer(); + contentImage.setQuickMode(false); + } + } + + public void save(String path) throws IOException { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss"); + String fullPath = Environment.getExternalStorageDirectory() + + File.separator + path + + File.separator + sdf.format(new Date()) + ".jpg"; + + try (OutputStream out = new FileOutputStream(new File(fullPath))) { + Bitmap bitmap = ((BitmapDrawable) contentImage.getDrawable()).getBitmap(); + bitmap.compress(Bitmap.CompressFormat.JPEG, 85, out); + out.flush(); + } + } +} diff --git a/app/src/main/java/com/blundell/tut/LoaderImageView.java b/app/src/main/java/com/blundell/tut/LoaderImageView.java new file mode 100644 index 0000000..d8cd97a --- /dev/null +++ b/app/src/main/java/com/blundell/tut/LoaderImageView.java @@ -0,0 +1,214 @@ +package com.blundell.tut; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Message; +import android.util.AttributeSet; +import android.view.Gravity; +import android.widget.FrameLayout; +import android.widget.ImageView; +import com.annimon.webcamviewer.R; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; + +/** + * Free for anyone to use, just say thanks and share :-) + * @author Blundell + */ +public final class LoaderImageView extends FrameLayout { + + private static final int COMPLETE = 0; + private static final int FAILED = 1; + private static final int PROGRESS = 2; + + private Drawable mDrawable; + private MaterialProgressBar mProgressBar; + private ImageView mImage; + private boolean mQuickMode; + + /** + * This is used when creating the view in XML + * To have an image load in XML use the tag 'image="http://developer.android.com/images/dialog_buttons.png"' + * Replacing the url with your desired image + * Once you have instantiated the XML view you can call + * setImageDrawable(url) to change the image + * @param context + * @param attrSet + */ + public LoaderImageView(final Context context, final AttributeSet attrSet) { + super(context, attrSet); + final String url = attrSet.getAttributeValue(null, "image"); + instantiate(url); + } + + /** + * This is used when creating the view programmatically + * Once you have instantiated the view you can call + * setImageDrawable(url) to change the image + * @param context the Activity context + * @param imageUrl the Image URL you wish to load + */ + public LoaderImageView(final Context context, final String imageUrl) { + super(context); + instantiate(imageUrl); + } + + /** + * This is used when creating the view programmatically + * Once you have instantiated the view you can call + * setImageDrawable(url) to change the image + */ + public LoaderImageView(final Context context) { + super(context); + instantiate(null); + } + + /** + * First time loading of the LoaderImageView + * Sets up the LayoutParams of the view, you can change these to + * get the required effects you want + */ + private void instantiate(final String imageUrl) { + mQuickMode = false; + + mImage = new ImageView(getContext()); + mImage.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + + mProgressBar = new MaterialProgressBar(getContext()); + mProgressBar.setLayoutParams(new LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, + Gravity.CENTER)); + mProgressBar.setProgress(0); + mProgressBar.setVisibility(GONE); + + addView(mImage); + addView(mProgressBar); + + if (imageUrl != null) { + setImageDrawable(imageUrl); + } + } + + /** + * Set's the view's drawable, this uses the internet to retrieve the image + * don't forget to add the correct permissions to your manifest + * @param imageUrl the url of the image you wish to load + */ + public void setImageDrawable(final String imageUrl) { +// mDrawable = null; + if (!mQuickMode) { + mProgressBar.setVisibility(VISIBLE); + mProgressBar.setProgress(0); + mProgressBar.setIndeterminate(true); + } + new Thread() { + @Override + public void run() { + try { + mDrawable = getDrawableFromUrl(imageUrl); + imageLoadedHandler.sendEmptyMessage(COMPLETE); + } catch (MalformedURLException e) { + imageLoadedHandler.sendEmptyMessage(FAILED); + } catch (IOException e) { + imageLoadedHandler.sendEmptyMessage(FAILED); + } + }; + }.start(); + } + + public Drawable getDrawable() { + return mImage.getDrawable(); + } + + public boolean isQuickMode() { + return mQuickMode; + } + + public void setQuickMode(boolean quickMode) { + this.mQuickMode = quickMode; + } + + /** + * Callback that is received once the image has been downloaded + */ + private final Handler imageLoadedHandler = new Handler(new Callback() { + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case COMPLETE: + mImage.setImageDrawable(mDrawable); + mProgressBar.setVisibility(GONE); + break; + case PROGRESS: + mProgressBar.setIndeterminate(false); + mProgressBar.setProgress(msg.arg1); + break; + case FAILED: + default: + // Could change image here to a 'failed' image + // otherwise will just keep on spinning + mImage.setImageResource(R.drawable.no_image); + mProgressBar.setIndeterminate(false); + mProgressBar.setVisibility(GONE); + break; + } + return true; + } + }); + + /** + * Pass in an image url to get a drawable object + * @return a drawable object + * @throws IOException + * @throws MalformedURLException + */ + private Drawable getDrawableFromUrl(final String imageUrl) throws IOException, MalformedURLException { + final URL url = new URL(imageUrl); + final URLConnection conection = url.openConnection(); + conection.connect(); + // this will be useful so that you can show a typical 0-100% progress bar + int lenghtOfFile = conection.getContentLength(); + if (lenghtOfFile <= 0 || mQuickMode) { + // try to load by system call + return Drawable.createFromStream((InputStream) url.getContent(), "name"); + } + + // download the file + final InputStream input = new BufferedInputStream(url.openStream(), 8192); + ByteArrayOutputStream output = new ByteArrayOutputStream(lenghtOfFile); + final byte[] buffer = new byte[4096]; + long total = 0; + int count; + while ((count = input.read(buffer)) != -1) { + total += count; + // publishing the progress.... + imageLoadedHandler.obtainMessage(PROGRESS, (int) ((total * 360) / lenghtOfFile), 0) + .sendToTarget(); + // writing data + output.write(buffer, 0, count); + } + // flushing output + output.flush(); + // closing streams + output.close(); + input.close(); + + byte[] data = output.toByteArray(); + output = null; + Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + data = null; + return new BitmapDrawable(getContext().getResources(), bitmap); + } + +} diff --git a/app/src/main/res/drawable-hdpi/ic_menu_check.png b/app/src/main/res/drawable-hdpi/ic_menu_check.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca40095f19afe1ca595bec27726e0a7572301a6 GIT binary patch literal 212 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 KelF{r5}E)qi94tO literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png b/app/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f37c0781091cbdaecc1cf0ee7056c302efaa6312 GIT binary patch literal 578 zcmV-I0=@l-P)@M7=^!cBf7{Xc>~dnn+UpaBW^1Q`UHZ4h)U6oc5(x5nl5j0Q^7)M zi`AVzK|!=`i|#5_!JP`eK#~h7H1oUYEvU&&A`!aj*$fP4zQdduemK%$OXCc+TCL)} zzXQwwr?TyDz#H%VvsSD1<-qHln@^HtVWNO~y?#7R(?wtw2y$$DX13C9w;z>CrN@%$ z&1UltW-tQ20o(`nBt4aM(ab_gr633{0jmI#9+b=FuafG+SjUEKcDvmtl4gLPz_muB zvHh223)phby#O{Poz81w;0DgQvy$!syFn0KZ8n?RC4Ns@drvsK`P z_kN>Vt)3qRH?!C5f9vJn=TdG(0AUzDOw;rX`vdMn6h$k*EARbI7>3sAbSjc&&Fn_T z*en){ZWwFYVQ0W{uh*MfT3Xuur+@=1mSIm Qpfi@Ln>~)y||I{PymDL#qbD= z7Cz;s$v0l{Ejx3?Y-LkxU90%RIfCzefr=48qQIkQ(n9h76I1+(CN37QH(MxwpW)+C z_nMD_@ilc1H0sR%sqFhvpF01^E*6pf0{^!inZ>5wVV^Pm$-b^{FIhhBFR2rJf0y%- zxOzwY9EG~VrXNQ+A58~x<|+JZQ$rGCM%c-4;F%4hqC`^T-R@H$A24{j`njxgN@xNA DN=;Vg literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png b/app/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..63f616dc29a3cadce75617bcf95f0f6b6edea795 GIT binary patch literal 1088 zcmV-G1i$-3#3SqB1MY-GpJjMq?CErb)O=l#{fJ8;9dZ?w}#gMTmtYTfD1y1 zb5cq-ET?IjXR6g|c~1?Dj*cGnJns~MVF0&AiD71!h~}8NQms}m?ZnTS?1nM?-2p{=d0!$kBnfM)<4Y!kb}%;$*c zH9y(aPsp;Yg8;q{iYKD0%seiHI4h-W?v_JJnNv!=3Sb<-fv}ogGob7GEt;l%3*cB_ zXr72(vTgfDoW&R$8@r>~Y<>pdcqaxT?*UEI-fAQMeIdkiam53`#KgokAw&_tPjO>o z8?Y?v2>$Ihe#{FY-jq^qB`xl{?ihdvJ2B87ao#AlcbhBqdi`a`aT1G<+iiA2=o=`N zO1A-cF+iW0o}OOswfIN|nE3<%;iFcC5NG#;_(%qb=o4gBhV z=MrD@tnQp#?$VanxlANW_FncZwQI}s(gFUq8)rKRgbLqmB0 zLq7VxMx#+E7K`63E-rFX@ugB}ikV*t@NZWtl}JaH$SYW{*T(@|4A8GDrM_q#xnn`s z^#Ri~KV@da&olFP`Fy?-c?IGW?Z%(_k&%&Xp-^}pfD_oX0PqJ9J#5?dl|3;4#J^U_ z=kr{v)ed-`_W*$7L^KTG?l!S2MD&Dh+ds$Aljvs7FpR-$Hv2h%N0Y?2!4ID2Jv%iu zwHh}t(k2dKd3o6_7K?L@Mk5E{k)(M5Y%+6seSQ7p%*@R7Bnf(QxeMS15q;=+-g}di zlPjInzZuu-O#puZ_>+h(0$8ZkYKxs*3ZzJpB1MW6|KT4KzkB$SIhHH{0000c8^+08+-ghBm9!>Ymu9V~|1gzr~hGTHEZIxA2gPS7CvW=_MEeQ)B*O1Umo+$&X5 z>T`E7Ij*cU>#?A~BS)JK_Kym3Ky;+JLZH7wfPb#+DY-|Az3HO-K*=rE)9$`%K6?26 zN1NBo*Bu2r(}R|8x_(``+FfNH@6pTFN*{gR{CT`_El`44uUSc`zvEHI5#>b|N*_Bk z`aEoo2zIsu*#hn=aFVdQ&MBb@0CZ?>F8}}l literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..07a6372dfe82d5b9ee8e98d99252ed15a4f96f22 GIT binary patch literal 1568 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 + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/wallpaper.png b/app/src/main/res/drawable/wallpaper.png new file mode 100644 index 0000000000000000000000000000000000000000..ac23b4e80e171d656b09c9db126e34185aca4bf7 GIT binary patch literal 3796 zcmX9>dpwhC9GB);u3I#>;?c7n%vI2oqyiX`~05g_j|t2@Av(EpZ858*jtN>$cqRF2#BAs!8r;D z?70ZOS3(8B=X&-z4*>y*;uAP5@lwhBxa-BS=W@?~MCT`}^a*PYw3NLoduQYpar?aS z>%v>5Um2|@Fu@qSv3+p*x$9~Jc>PFk2X*ZSnYrY`<8y0*p9CSQA}V|4M3*+kRUW9y zF_PbI%5+uzj7muDtInP6m?@2DpO+bbeCTTHN9!Ot$inE1WOT?D_sfK#d-YeJWVZo9 z&L6KHT6k@mCmtmYokejchbQ}HXRFsko1GN21|L?z`XaZ2-tIQ7$8Mj+lx$*WtoNbL zG0Y@=gu?FxuwdD_!|q}C$6I!KO$;BCdJWxdGs7p;2;Ph44+N};)Lmc_;A+ zUcXAmmjILs7@FsN$sB@pXt4uhldeK3-g-soWHd_{4*6&DC>W5u7YP;V2>}B#EvSIT zz8)|PqXu+Vu~P?~`_|>*kgCrn;7a1{_|tXDlnR4d{zSvnRg7n|pzq`#yt$^gNwKjw zS#3g%Ja4B1{)3`$EFmeDKkyn9ZE#5n9PtBp2{5JtOvh3lFf|ZPIwzQCj024x+dSj* zB&eV^Ic%$H-lqV<(vZZ#-XRrwBxgo`!pGOr!SkwAcp#J2s ze2(i~(It*|yz^l;LktHyqpl#{(ZX~JK?5{D@QBChE`L_u%r6ol5tsCw_P`-_HWT@_ z*Mlqrd60kvD4ZRE0KsjeDhEsSMo)|qbp_+?y85Zi!qi6jy4U3 z?MhCoxKwMizr9_jioGu6uqCbqG0M!zc}iG`UHX?T>TEL5H|3NSXJM;3u{>yRuM~PH zF8rWv{AxSw+>G!*@$$Uy)aMCIV4MA@u#sG?Q(~G1g~92g+)Y+VFZ+&GHM;X!nDh)o z(*x`}9_ro(rm z{J1=e5r0c<6x(J8LNZA2;3pvl$%Yjd2WhQJ8nOaz)igry9`Ho^L*#}pG})E@#b#t{ z^&>MtQvyWJ5gL9_=UG*^5XqpThVSfyr^6>StB(pLnIrUik-W2BKxqumC_W37tpmw$ zkOlP-DkuObROV~5pWODkujjLQMzFMY2<*_AR;kV-$Bw;mbEfS?>$BM22(M;AJG+C} zBV#?MrtDjqRGJZ;<#F05TCgwtxrnabDG1&N|H7)Ndrb&LLGx!xCj-ge%6>O~ z{_0IAWyKte_^0m$@>J8}RgB`W>xL6ffUBuX5j%mqmeLu{)%Qhguo+lhv<5i&nUY$g zn5HT1AladhQ~}oW@24oX>^m<7QbZwxcgnb2I5vf-wIKwy0N!_~v%1W?R$#9xp02$2 zZ04C1fR>@zIE~reiP`B$o<#!=H_E%Nb3SK?_}jocW3x2rW4V5BoEm$)bG3cW|k7yd}d;7 zS0*3f?BtB;NVwZ<9LU|@Ht?~1y`!p1BunCxpaUfS&6aoM(KE9CwU2_8cI^{n(MP6~ zw58|k&FWXo5!~zQG>f-JEzvSQLbZ3%6fsTJ)=YaR1e@`}%0K(YejlOZOnpkmj>K5{ zmU%$wqu=i#xDlT@{K5s^gNT1$tk+4sp~z;$u}Gjc2NRXBR!h#H9NidTggU!t9Qf2_ z3y}-=G`4XGJ9-as3TShY_GoX76LzL_b*A5kh+nr&dl`mBocab-ln=-_B?oPqdwwKk z0G9i+9g$+c*i9`^JIrBU|%EUwae?%fXamE#{k(ukqUf5rB|Fb0=W^vBu+JY(&wAf$ZN`x z!<3A#HraSMsI3k?Q2np$#^?opyN(!8#9}zZo|&wGi5`y!T^8{uHXS#bZ&)&jHg^8C zX*uUBy2df4Wc*~tCuHQknDZSp>e2+s`>)hk?)GpT6%Z);#Ps4HYFY z=}erSXIZ}$L{9(pp^JHZHS|NW>DyJ_3*y??$IOO*0%F)cceVBm7sOk4cVrdm6)Bp z)N1LCPGJ9bw0e(&qoZuP$7w~yn9qSdq>YkEPWtmep;{VJ!=(*eo4kVAe0A4vlY1<|R?i>A9V3UJ7q{yOk3Oj z(y^$x-MYP9w>s6Tq?9<>|1qz^D~CGr&&j{PUYjk)dw-cGZ~u7jzI!ua{{6iFU`XBl zvpWR>xd9JQY$~R1t{3ASHh-D zN4)}Z>;HmNYT}twzrpISqGGDpgE34AuTNtoB93^jlWkaDkf51o&Z@iJ+U#` z%Lo~CO?VUqE=3>x(vTB-y18EV@Zs?*DQL=YKS=r?rQxFq^WV$yCe`C-m6{i07FF-& ziPdG}2}!N#0BQ%_sGVQr?=bagWutO!I|3qZiNJ1r(*e0ARa%IiU3*mL^xb0-TYp>{ z+fBz4ij2S<&&@Rrmxqlv2E^}1Ecd#Et7;573y~nI1%puiTT1MgI`ymljJkzhtGn@o z3UYkErq*kKWzKDYUA7hZ62!c@>aclbaoi1VAVz`+6;46*Cr!A?uQDsY7voLT$FIG4 z&(y}o05s3Qi(sd3#RTch7UNyVuh*#utxsg|W9lBl(X)Szd@sb0mpvR0bdNLnK&In$ z;W$FkyU}w(cNUn=i1Ui&ZWsQytKkSKxj8ItS`pd7!0BGgp%8sGQdSrlP;iTB;`kz2 zp6mc*4OD7lH$qa2#aPgAbBMdfsF$l|%}K=h=+h5|6;QeBe_;_%Y^W74RI+g5EH=kt zdVZld>h96C`LOHY3>St)v4f+-29VhMmZ92h`-vXBd_IBUUIj2-pEJA_^r2(z4 zZGWyX3+f2NI_Kb|CAOdCx*~)+HGbF<6xqmWO9&T^(ZU{&FiQCoaj0x_uu_M0H>1Lf z&1g-9_MP)gJzqv@${R(FEr3Zk=z(E3dRK-My%6&yQPEoMyvP9_%9Q8G#v?{-Ojx%M+9MLa_d({HwIWFPfWPdu|)&27SW^vpr$fU4lV|YUu{`+S2vHK8i61qr| zTG6YLB~{KNLBDzs_YZZZ2Affs0xc@I^n;PN^Q?|XnToYu*CU z55W zLIi7v>0ZsJOdu3aL~AO%KeX|+)RAQ)1qVrBx?2-}e}P9fq9L{7=?P?xOvjncrRcYj<{;hw}#>Yi%6B>v+bi5cqsnM(Njygdij?=1^Qqe3CI8n~X+wispi#aTF zk}F9fOCp)J!;z7(Ez-sK>duF)$Nc%F`%3~&)C0drk*CVa-TH4*SOHi5PZ + + + + + + + + + + + diff --git a/app/src/main/res/layout/drawer_group.xml b/app/src/main/res/layout/drawer_group.xml new file mode 100644 index 0000000..c6f8933 --- /dev/null +++ b/app/src/main/res/layout/drawer_group.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/layout/drawer_row.xml b/app/src/main/res/layout/drawer_row.xml new file mode 100644 index 0000000..6a3b034 --- /dev/null +++ b/app/src/main/res/layout/drawer_row.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/layout/fragment_navigation_drawer.xml b/app/src/main/res/layout/fragment_navigation_drawer.xml new file mode 100644 index 0000000..872560a --- /dev/null +++ b/app/src/main/res/layout/fragment_navigation_drawer.xml @@ -0,0 +1,56 @@ + + + + +