├── settings.gradle ├── app ├── src │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── octocat.png │ │ │ │ ├── preview.png │ │ │ │ └── widget_shape.xml │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── values │ │ │ │ ├── styles.xml │ │ │ │ ├── dimens.xml │ │ │ │ └── strings.xml │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ ├── layout │ │ │ │ ├── activity_login.xml │ │ │ │ ├── one_row.xml │ │ │ │ ├── main.xml │ │ │ │ └── small.xml │ │ │ └── xml │ │ │ │ ├── widget.xml │ │ │ │ └── preferences.xml │ │ ├── java │ │ │ └── by │ │ │ │ └── aleks │ │ │ │ └── ghcwidget │ │ │ │ ├── LoginActivity.java │ │ │ │ ├── data │ │ │ │ ├── Day.java │ │ │ │ ├── CommitsBase.java │ │ │ │ └── ColorTheme.java │ │ │ │ ├── api │ │ │ │ ├── GitHubHelper.java │ │ │ │ └── GitHubAPITask.java │ │ │ │ ├── WidgetPreferenceActivity.java │ │ │ │ └── Widget.java │ │ └── AndroidManifest.xml │ └── androidTest │ │ └── java │ │ └── by │ │ └── aleks │ │ └── ghcwidget │ │ └── ApplicationTest.java ├── build.gradle ├── proguard-rules.pro └── manifest-merger-release-report.txt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── README.md ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeusiukou/GHCWidget/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeusiukou/GHCWidget/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeusiukou/GHCWidget/HEAD/app/src/main/res/drawable/octocat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeusiukou/GHCWidget/HEAD/app/src/main/res/drawable/preview.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeusiukou/GHCWidget/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeusiukou/GHCWidget/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeusiukou/GHCWidget/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeusiukou/GHCWidget/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeusiukou/GHCWidget/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/androidTest/java/by/aleks/ghcwidget/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package by.aleks.ghcwidget; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Windows thumbnail db 19 | Thumbs.db 20 | 21 | # OSX files 22 | .DS_Store 23 | 24 | # Eclipse project files 25 | .classpath 26 | .project 27 | 28 | # Android Studio 29 | .idea 30 | *.iml 31 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 32 | .gradle 33 | build/ 34 | .settings -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/xml/widget.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion "25.0.2" 6 | useLibrary "org.apache.http.legacy" 7 | 8 | defaultConfig { 9 | applicationId "by.aleks.ghcwidget" 10 | minSdkVersion 16 11 | targetSdkVersion 29 12 | versionCode 10 13 | versionName "1.3.3" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile fileTree(dir: 'libs', include: ['*.jar']) 25 | compile 'androidx.appcompat:appcompat:1.2.0' 26 | } 27 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/Alex/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 14 | 15 | 19 | 22 | 26 | 27 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GHCWidget 3 | Log in to GitHub 4 | 5 | https://github.com/login 6 | github.com 7 | 8 | Username 9 | Set your GitHub username 10 | GitHub username 11 | 12 | Color theme 13 | Number of months to display 14 | Transparency 15 | Start week on Monday 16 | Show days of week 17 | Manual refresh 18 | 19 | Loading failed 20 | Username not found 21 | day 22 | days 23 | total 24 | M 25 | W 26 | F 27 | S 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/by/aleks/ghcwidget/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package by.aleks.ghcwidget; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import androidx.appcompat.app.AppCompatActivity; 6 | import android.webkit.CookieManager; 7 | import android.webkit.WebView; 8 | import android.webkit.WebViewClient; 9 | 10 | public class LoginActivity extends AppCompatActivity { 11 | 12 | private WebView loginView; 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_login); 18 | setTitle(getString(R.string.login_to_github)); 19 | 20 | loginView = (WebView)findViewById(R.id.login_view); 21 | loginView.loadUrl(getString(R.string.login_url)); 22 | loginView.setWebViewClient(new WebViewClient() { 23 | 24 | public void onPageFinished(WebView view, String url) { 25 | String cookies = CookieManager.getInstance().getCookie(url); 26 | if(cookies != null){ 27 | if(cookies.split(";")[0].equals("logged_in=yes")){ 28 | Intent returnIntent = new Intent(); 29 | setResult(RESULT_OK, returnIntent); 30 | finish(); 31 | } 32 | } 33 | } 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/by/aleks/ghcwidget/data/Day.java: -------------------------------------------------------------------------------- 1 | package by.aleks.ghcwidget.data; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Calendar; 5 | import java.util.Date; 6 | 7 | /** 8 | * Created by Alex on 12/8/14. 9 | */ 10 | public class Day { 11 | 12 | private int level; 13 | private int commitsNumber; 14 | private Calendar calendar = Calendar.getInstance(); 15 | 16 | public Day(Date date, int commitsNumber, int level) { 17 | this.calendar.setTime(date); 18 | this.commitsNumber = commitsNumber; 19 | this.level = level; 20 | } 21 | 22 | public String toString() { 23 | return "Date:" + calendar.toString() + " Commits:" + commitsNumber + " Level:" + level; 24 | } 25 | 26 | public int getLevel() { 27 | return level; 28 | } 29 | 30 | public int getCommitsNumber() { 31 | return commitsNumber; 32 | } 33 | 34 | public void setCommitsNumber(int commitsNumber) { 35 | this.commitsNumber = commitsNumber; 36 | } 37 | 38 | public String getMonthName(){ 39 | return new SimpleDateFormat("MMM").format(calendar.getTime()); 40 | } 41 | public int getYear(){ return Integer.parseInt(new SimpleDateFormat("yyyy").format(calendar.getTime())); } 42 | 43 | public boolean isFirst(){ 44 | //return firstDayInMonth; 45 | return calendar.get(Calendar.DAY_OF_MONTH)==1; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitHub Contributions Widget 2 | ========== 3 | 4 | ### Description 5 | This simple widget shows your contribution activity over a chosen amount of time. It has a few different color themes to better fit your homescreen style. 6 | 7 | 8 | Get it on Google Play 10 | 11 | 12 | ### Screenshots 13 | 14 |
15 | 16 | 17 | 18 |
19 | 20 | ### License 21 | The MIT License (MIT) 22 | 23 | Copyright (c) 2015 Aliaksei Yeusiukou 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining a copy 26 | of this software and associated documentation files (the "Software"), to deal 27 | in the Software without restriction, including without limitation the rights 28 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | copies of the Software, and to permit persons to whom the Software is 30 | furnished to do so, subject to the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be included in all 33 | copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 41 | SOFTWARE. 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/widget_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/java/by/aleks/ghcwidget/data/CommitsBase.java: -------------------------------------------------------------------------------- 1 | package by.aleks.ghcwidget.data; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * Created by Alex on 12/8/14. 7 | */ 8 | public class CommitsBase { 9 | 10 | private ArrayList days = new ArrayList<>(); 11 | private ArrayList< ArrayList > weeks = new ArrayList<>(); 12 | private int currentWeek = -1; 13 | private Day currentDay; 14 | 15 | public void addDay(Day day){ 16 | if (days == null) 17 | days = new ArrayList(); 18 | 19 | // Decline the current week in the case it was created because od a new year. 20 | if (currentDay!=null && currentWeek > 0 && day.getYear() > currentDay.getYear()){ 21 | if(weeks.get(currentWeek-1).size()<7){ 22 | weeks.remove(currentWeek); 23 | currentWeek--; 24 | } 25 | //Skip the previous year days after a new year. 26 | } else if (currentDay!=null && day.getYear() < currentDay.getYear()) 27 | return; 28 | 29 | weeks.get(currentWeek).add(day); 30 | currentDay = day; 31 | } 32 | 33 | public void newWeek(){ 34 | currentWeek++; 35 | weeks.add(new ArrayList()); 36 | } 37 | 38 | public int commitsNumber(){ 39 | int commitsCounter = 0; 40 | for(ArrayList days : weeks){ 41 | for(Day day : days){ 42 | commitsCounter += day.getCommitsNumber(); 43 | } 44 | } 45 | return commitsCounter; 46 | } 47 | 48 | public int currentStreak() { 49 | int streakCounter = 0; 50 | for(ArrayList days : weeks){ 51 | for(Day day : days){ 52 | if(day.getCommitsNumber() != 0) 53 | streakCounter++; 54 | else if (weeks.size()-1 != weeks.indexOf(days) || days.size()-1 != days.indexOf(day) ){ 55 | streakCounter = 0; 56 | } 57 | } 58 | } 59 | return streakCounter; 60 | } 61 | 62 | /** Returns a very first week in a month from the given range*/ 63 | public int getFirstWeekOfMonth(int weeksNum){ 64 | int firstWeekOfLast = -1; 65 | for(int i = weeks.size()-1; i > 0; i--){ 66 | for(Day day : weeks.get(i)){ 67 | if(day.isFirst()){ 68 | firstWeekOfLast = weeks.size()-1 - i; 69 | break; 70 | } 71 | } 72 | } 73 | return firstWeekOfLast%4; 74 | } 75 | 76 | public ArrayList< ArrayList > getWeeks(){ 77 | return weeks; 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/manifest-merger-release-report.txt: -------------------------------------------------------------------------------- 1 | -- Merging decision tree log --- 2 | manifest 3 | ADDED from AndroidManifest.xml:1:1 4 | package 5 | ADDED from AndroidManifest.xml:2:11 6 | INJECTED from AndroidManifest.xml:0:0 7 | INJECTED from AndroidManifest.xml:0:0 8 | android:versionName 9 | INJECTED from AndroidManifest.xml:0:0 10 | INJECTED from AndroidManifest.xml:0:0 11 | xmlns:android 12 | ADDED from AndroidManifest.xml:1:11 13 | android:versionCode 14 | INJECTED from AndroidManifest.xml:0:0 15 | INJECTED from AndroidManifest.xml:0:0 16 | application 17 | ADDED from AndroidManifest.xml:4:5 18 | MERGED from com.android.support:appcompat-v7:21.0.2:16:5 19 | MERGED from com.android.support:support-v4:21.0.2:16:5 20 | android:label 21 | ADDED from AndroidManifest.xml:5:18 22 | android:allowBackup 23 | ADDED from AndroidManifest.xml:4:18 24 | android:icon 25 | ADDED from AndroidManifest.xml:6:18 26 | android:theme 27 | ADDED from AndroidManifest.xml:7:18 28 | receiver#by.aleks.ghcwidget.Widget 29 | ADDED from AndroidManifest.xml:9:9 30 | android:name 31 | ADDED from AndroidManifest.xml:9:19 32 | intent-filter#android.appwidget.action.APPWIDGET_ENABLED+android.appwidget.action.APPWIDGET_UPDATE 33 | ADDED from AndroidManifest.xml:10:13 34 | action#android.appwidget.action.APPWIDGET_UPDATE 35 | ADDED from AndroidManifest.xml:11:17 36 | android:name 37 | ADDED from AndroidManifest.xml:11:25 38 | action#android.appwidget.action.APPWIDGET_ENABLED 39 | ADDED from AndroidManifest.xml:12:17 40 | android:name 41 | ADDED from AndroidManifest.xml:12:25 42 | meta-data#android.appwidget.provider 43 | ADDED from AndroidManifest.xml:14:13 44 | android:resource 45 | ADDED from AndroidManifest.xml:14:66 46 | android:name 47 | ADDED from AndroidManifest.xml:14:24 48 | activity#by.aleks.ghcwidget.WidgetPreferenceActivity 49 | ADDED from AndroidManifest.xml:17:9 50 | android:exported 51 | ADDED from AndroidManifest.xml:17:60 52 | android:name 53 | ADDED from AndroidManifest.xml:17:19 54 | intent-filter#android.appwidget.action.APPWIDGET_CONFIGURE 55 | ADDED from AndroidManifest.xml:18:13 56 | action#android.appwidget.action.APPWIDGET_CONFIGURE 57 | ADDED from AndroidManifest.xml:19:17 58 | android:name 59 | ADDED from AndroidManifest.xml:19:25 60 | uses-permission#android.permission.INTERNET 61 | ADDED from AndroidManifest.xml:24:5 62 | android:name 63 | ADDED from AndroidManifest.xml:24:22 64 | uses-sdk 65 | INJECTED from AndroidManifest.xml:0:0 reason: use-sdk injection requested 66 | MERGED from com.android.support:appcompat-v7:21.0.2:15:5 67 | MERGED from com.android.support:support-v4:21.0.2:15:5 68 | android:targetSdkVersion 69 | INJECTED from AndroidManifest.xml:0:0 70 | INJECTED from AndroidManifest.xml:0:0 71 | android:minSdkVersion 72 | INJECTED from AndroidManifest.xml:0:0 73 | INJECTED from AndroidManifest.xml:0:0 74 | -------------------------------------------------------------------------------- /app/src/main/java/by/aleks/ghcwidget/data/ColorTheme.java: -------------------------------------------------------------------------------- 1 | package by.aleks.ghcwidget.data; 2 | 3 | import android.graphics.Color; 4 | 5 | import java.util.HashMap; 6 | 7 | /** 8 | * Created by Alex on 12/8/14. 9 | */ 10 | public class ColorTheme { 11 | 12 | public static String GITHUB = "GitHub"; 13 | public static String MODERN = "Modern"; 14 | public static String GRAY = "Gray"; 15 | public static String RED = "Red"; 16 | public static String BLUE = "Blue"; 17 | public static String PURPLE = "Purple"; 18 | public static String YELLOW = "Yellow"; 19 | public static String ORANGE = "Orange"; 20 | public static String HALLOWEEN = "Halloween"; 21 | public static String PSYCHEDELIC = "Psychedelic"; 22 | public static String MOON = "Moon"; 23 | 24 | private String[] standard = {"#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"}; 25 | private String[] modern = {"#afaca8", "#d6e685", "#8cc665", "#44a340", "#1e6823"}; 26 | private String[] gray = {"#eeeeee", "#bdbdbd", "#9e9e9e", "#616161", "#212121"}; 27 | private String[] red = {"#eeeeee", "#ff7171", "#ff0000", "#b70000", "#830000"}; 28 | private String[] blue = {"#eeeeee", "#6bcdff", "#00a1f3", "#0079b7", "#003958"}; 29 | private String[] purple = {"#eeeeee", "#d2ace6", "#aa66cc", "#660099", "#4f2266"}; 30 | private String[] yellow = {"#eeeeee", "#d7d7a2", "#d4d462", "#e0e03f", "#ffff00"}; 31 | private String[] orange = {"#eeeeee", "#ffcc80", "#ffa726", "#fb8c00", "#e65100"}; 32 | private String[] halloween = {"#eeeeee", "#ffee4a", "#ffc501", "#fe9600", "#03001c"}; 33 | private String[] psychedelic = {"#eeeeee", "#faafe1", "#fb6dcc", "#fa3fbc", "#ff00ab"}; 34 | private String[] moon = {"#eeeeee", "#6bcdff", "#00a1f3", "#48009a", "#4f2266"}; 35 | 36 | private HashMap themeMap; 37 | 38 | public ColorTheme(){ 39 | themeMap = new HashMap<>(); 40 | themeMap.put(ColorTheme.GITHUB, standard); 41 | themeMap.put(ColorTheme.MODERN, modern); 42 | themeMap.put(ColorTheme.GRAY, gray); 43 | themeMap.put(ColorTheme.RED, red); 44 | themeMap.put(ColorTheme.BLUE, blue); 45 | themeMap.put(ColorTheme.PURPLE, purple); 46 | themeMap.put(ColorTheme.YELLOW, yellow); 47 | themeMap.put(ColorTheme.ORANGE, orange); 48 | themeMap.put(ColorTheme.HALLOWEEN, halloween); 49 | themeMap.put(ColorTheme.PSYCHEDELIC, psychedelic); 50 | themeMap.put(ColorTheme.MOON, moon); 51 | } 52 | 53 | public int getColor(String themeName, int level){ 54 | return Color.parseColor(themeMap.get(themeName)[level]); 55 | } 56 | 57 | public static CharSequence[] getThemeNames(){ 58 | return new CharSequence[]{ColorTheme.GITHUB, ColorTheme.MODERN, ColorTheme.GRAY, ColorTheme.RED, ColorTheme.BLUE, ColorTheme.PURPLE, ColorTheme.YELLOW, ColorTheme.ORANGE, ColorTheme.HALLOWEEN, ColorTheme.PSYCHEDELIC, ColorTheme.MOON}; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/res/layout/one_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 21 | 22 | 34 | 35 | 45 | 46 | 58 | 59 | 60 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 24 | 25 | 32 | 33 | 41 | 50 | 59 | 60 | 69 | 70 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/src/main/res/layout/small.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 23 | 24 | 33 | 45 | 46 | 56 | 57 | 69 | 70 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /app/src/main/java/by/aleks/ghcwidget/api/GitHubHelper.java: -------------------------------------------------------------------------------- 1 | package by.aleks.ghcwidget.api; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | import android.webkit.CookieManager; 6 | 7 | import org.apache.http.HttpEntity; 8 | import org.apache.http.HttpResponse; 9 | import org.apache.http.StatusLine; 10 | import org.apache.http.client.methods.HttpGet; 11 | import org.apache.http.client.protocol.ClientContext; 12 | import org.apache.http.impl.client.BasicCookieStore; 13 | import org.apache.http.impl.client.DefaultHttpClient; 14 | import org.apache.http.impl.cookie.BasicClientCookie; 15 | import org.apache.http.protocol.BasicHttpContext; 16 | import org.apache.http.protocol.HttpContext; 17 | 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.InputStream; 20 | 21 | import by.aleks.ghcwidget.R; 22 | 23 | public class GitHubHelper { 24 | 25 | private static final int HTTP_STATUS_OK = 200; 26 | private static byte[] buff = new byte[1024]; 27 | private static final String logTag = "GHCWidget"; 28 | 29 | public static class ApiException extends Exception { 30 | private static final long serialVersionUID = 1L; 31 | 32 | public ApiException(String msg) { 33 | super(msg); 34 | } 35 | 36 | public ApiException(String msg, Throwable thr) { 37 | super(msg, thr); 38 | } 39 | } 40 | 41 | /** 42 | * download user contribution data. 43 | * 44 | * @param username GitHub username 45 | * @return Array of html strings returned by the API. 46 | * @throws ApiException 47 | */ 48 | protected static synchronized String downloadFromServer(String username, Context context) 49 | throws ApiException { 50 | String retval = null; 51 | String url = "https://github.com/users/" + username + "/contributions"; 52 | 53 | Log.d(logTag, "Fetching " + url); 54 | 55 | // create an http client and a request object. 56 | DefaultHttpClient client = new DefaultHttpClient(); 57 | HttpGet request = new HttpGet(url); 58 | 59 | // load and attach cookies 60 | String cookies = CookieManager.getInstance().getCookie(context.getString(R.string.login_url)); 61 | if(cookies != null){ 62 | BasicCookieStore lCS = getCookieStore(cookies, context.getString(R.string.domain)); 63 | 64 | HttpContext localContext = new BasicHttpContext(); 65 | client.setCookieStore(lCS); 66 | localContext.setAttribute(ClientContext.COOKIE_STORE, lCS); 67 | 68 | } 69 | 70 | 71 | try { 72 | 73 | // execute the request 74 | HttpResponse response = client.execute(request); 75 | StatusLine status = response.getStatusLine(); 76 | if (status.getStatusCode() != HTTP_STATUS_OK) { 77 | // handle error here 78 | return "invalid_response"; 79 | } 80 | 81 | // process the content. 82 | HttpEntity entity = response.getEntity(); 83 | InputStream ist = entity.getContent(); 84 | ByteArrayOutputStream content = new ByteArrayOutputStream(); 85 | 86 | int readCount = 0; 87 | while ((readCount = ist.read(buff)) != -1) { 88 | content.write(buff, 0, readCount); 89 | } 90 | retval = new String(content.toByteArray()); 91 | 92 | } catch (Exception e) { 93 | throw new ApiException("Problem connecting to the server " + 94 | e.getMessage(), e); 95 | } 96 | 97 | return retval; 98 | } 99 | 100 | // parse cookie string 101 | private static BasicCookieStore getCookieStore(String cookies, String domain) { 102 | String[] cookieValues = cookies.split(";"); 103 | BasicCookieStore cs = new BasicCookieStore(); 104 | 105 | BasicClientCookie cookie; 106 | for (int i = 0; i < cookieValues.length; i++) { 107 | String[] split = cookieValues[i].split("="); 108 | if (split.length == 2) 109 | cookie = new BasicClientCookie(split[0], split[1]); 110 | else 111 | cookie = new BasicClientCookie(split[0], null); 112 | 113 | cookie.setDomain(domain); 114 | cs.addCookie(cookie); 115 | } 116 | return cs; 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/by/aleks/ghcwidget/WidgetPreferenceActivity.java: -------------------------------------------------------------------------------- 1 | package by.aleks.ghcwidget; 2 | 3 | /** 4 | * Created by Alex on 12/7/14. 5 | */ 6 | 7 | import android.appwidget.AppWidgetManager; 8 | import android.content.Intent; 9 | import android.net.Uri; 10 | import android.os.Bundle; 11 | import android.preference.ListPreference; 12 | import android.preference.Preference; 13 | import android.preference.Preference.OnPreferenceChangeListener; 14 | import android.preference.PreferenceActivity; 15 | import android.util.Log; 16 | import android.widget.Toast; 17 | 18 | import by.aleks.ghcwidget.data.ColorTheme; 19 | 20 | public class WidgetPreferenceActivity extends PreferenceActivity { 21 | private static final String TAG = "GHCW"; 22 | private static final String CONFIGURE_ACTION = "android.appwidget.action.APPWIDGET_CONFIGURE"; 23 | 24 | @SuppressWarnings("deprecation") 25 | @Override 26 | public void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | addPreferencesFromResource(R.xml.preferences); 29 | 30 | ListPreference themePref = (ListPreference)findPreference("color_theme"); 31 | themePref.setEntries(ColorTheme.getThemeNames()); 32 | themePref.setEntryValues(ColorTheme.getThemeNames()); 33 | themePref.setDefaultValue(ColorTheme.BLUE); 34 | 35 | //Set up the Listener. 36 | findPreference("username").setOnPreferenceChangeListener(onPreferenceChange); 37 | findPreference("color_theme").setOnPreferenceChangeListener(onPreferenceChange); 38 | findPreference("months").setOnPreferenceChangeListener(onPreferenceChange); 39 | findPreference("start_on_monday").setOnPreferenceChangeListener(onPreferenceChange); 40 | findPreference("days_labels").setOnPreferenceChangeListener(onPreferenceChange); 41 | 42 | findPreference("refresh").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 43 | @Override 44 | public boolean onPreferenceClick(Preference preference) { 45 | updateWidget(true); 46 | finish(); 47 | return true; 48 | } 49 | }); 50 | } 51 | 52 | @Override 53 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 54 | // On successful login update widget 55 | if (resultCode == RESULT_OK) 56 | updateWidget(true); 57 | } 58 | 59 | /** 60 | * When the user changes the preferences, update the widget. 61 | */ 62 | private OnPreferenceChangeListener onPreferenceChange = new Preference.OnPreferenceChangeListener() { 63 | @Override 64 | public boolean onPreferenceChange(Preference preference, Object newValue) { 65 | 66 | String key = preference.getKey(); 67 | 68 | // Exit if the username is invalid 69 | if(preference.getKey().equals("username")){ 70 | if(!isUsernameValid((String)newValue)){ 71 | alert("Invalid username"); 72 | return false; 73 | } 74 | } 75 | 76 | Intent activityIntent = getIntent(); 77 | if (CONFIGURE_ACTION.equals(activityIntent.getAction())) { 78 | Bundle extras = activityIntent.getExtras(); 79 | 80 | if (extras != null) { 81 | int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, 82 | AppWidgetManager.INVALID_APPWIDGET_ID); 83 | 84 | //Put the widget ID into the extras and let the activity caller know the result is ok. 85 | Intent result = new Intent(); 86 | result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); 87 | 88 | setResult(RESULT_OK, result); 89 | Log.d(TAG, preference.getTitle().toString()); 90 | //Update the widget. 91 | updateWidget(preference.getKey().equals("username")); 92 | 93 | finish(); 94 | } 95 | else { 96 | Log.d(TAG, "Intent Extras is null"); 97 | 98 | return false; 99 | } 100 | } 101 | else { 102 | Log.d(TAG, "Intent Action is: {" + activityIntent.getAction() + "} and not: " + CONFIGURE_ACTION); 103 | 104 | return false; 105 | } 106 | 107 | return true; 108 | } 109 | }; 110 | 111 | /** 112 | * Send an intent to update the widget. 113 | */ 114 | private void updateWidget(boolean online) { 115 | Intent updateIntent = new Intent(android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE, 116 | Uri.EMPTY, this, Widget.class); 117 | updateIntent.putExtra(Widget.LOAD_DATA_KEY, online); 118 | sendBroadcast(updateIntent); 119 | } 120 | 121 | private boolean isUsernameValid(String value){ 122 | return value.matches("[a-zA-Z0-9-]+"); 123 | } 124 | 125 | 126 | public void alert (String msg) 127 | { 128 | Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/src/main/java/by/aleks/ghcwidget/api/GitHubAPITask.java: -------------------------------------------------------------------------------- 1 | package by.aleks.ghcwidget.api; 2 | 3 | import android.content.Context; 4 | import android.os.AsyncTask; 5 | import android.util.Log; 6 | 7 | import org.xmlpull.v1.XmlPullParser; 8 | import org.xmlpull.v1.XmlPullParserFactory; 9 | import org.xmlpull.v1.XmlPullParserException; 10 | 11 | import java.io.IOException; 12 | import java.io.StringReader; 13 | import java.text.ParseException; 14 | import java.text.SimpleDateFormat; 15 | import java.util.Date; 16 | import java.util.concurrent.ExecutionException; 17 | 18 | import by.aleks.ghcwidget.Widget; 19 | import by.aleks.ghcwidget.data.CommitsBase; 20 | import by.aleks.ghcwidget.data.Day; 21 | 22 | public class GitHubAPITask extends AsyncTask // Username to the input, Progress, Output 23 | { 24 | 25 | private static final String debugTag = "GHCWiget"; 26 | private Widget widget; 27 | private Context context; 28 | private static CommitsBase base = null; 29 | 30 | public GitHubAPITask(Widget widget, Context context) { 31 | this.widget = widget; 32 | this.context = context; 33 | } 34 | 35 | 36 | // Call the downloading method in background and load data 37 | @Override 38 | protected String doInBackground(String... params) { 39 | String result = null; 40 | try { 41 | Log.d(debugTag, "Background:" + Thread.currentThread().getName()); 42 | result = GitHubHelper.downloadFromServer(params[0], context); 43 | } catch (GitHubHelper.ApiException e) { 44 | Log.d(debugTag, "Loading failed >>> " + e.getMessage()); 45 | widget.setStatus(Widget.STATUS_OFFLINE); 46 | return null; 47 | } 48 | if (result.equals("invalid_response")) { 49 | widget.setStatus(Widget.STATUS_NOTFOUND); 50 | return null; 51 | } 52 | 53 | return result; 54 | } 55 | 56 | public static CommitsBase parseResult(final String result) throws ExecutionException, InterruptedException { 57 | 58 | AsyncTask task = new AsyncTask(){ 59 | 60 | @Override 61 | protected CommitsBase doInBackground(Void... params) { 62 | 63 | CommitsBase base = new CommitsBase(); 64 | try { 65 | XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 66 | factory.setNamespaceAware(true); 67 | XmlPullParser xpp = factory.newPullParser(); 68 | 69 | xpp.setInput(new StringReader(result)); 70 | int eventType = xpp.getEventType(); 71 | 72 | boolean firstTagSkipped = false; 73 | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); 74 | 75 | while (eventType != XmlPullParser.END_DOCUMENT) { 76 | switch (eventType) { 77 | case XmlPullParser.START_DOCUMENT: { 78 | break; 79 | } 80 | case XmlPullParser.START_TAG: { 81 | if (xpp.getName().equals("g")) { 82 | if (!firstTagSkipped) { 83 | firstTagSkipped = true; 84 | eventType = xpp.next(); 85 | break; 86 | } else { 87 | base.newWeek(); 88 | eventType = xpp.next(); 89 | break; 90 | } 91 | } 92 | if (xpp.getName().equals("rect")) { 93 | String dateStr = xpp.getAttributeValue(null, "data-date"); 94 | if (dateStr != null) { 95 | Date date = dateFormat.parse(dateStr); 96 | int commits = Integer.valueOf(xpp.getAttributeValue(null, "data-count")); 97 | int level = Integer.valueOf(xpp.getAttributeValue(null, "data-level")); 98 | Day day = new Day(date, commits, level); 99 | base.addDay(day); 100 | } 101 | eventType = xpp.next(); 102 | break; 103 | } 104 | } 105 | } 106 | 107 | eventType = xpp.next(); 108 | } 109 | 110 | } catch (XmlPullParserException e) { 111 | // Ignore parsing exceptions as html may contain attributes without values (which is invalid in XML) 112 | } catch (ParseException | IOException e) { 113 | Log.d(debugTag, "Error in parsing"); 114 | e.printStackTrace(); 115 | return null; 116 | } 117 | return base; 118 | } 119 | }; 120 | 121 | return task.execute().get(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /app/src/main/java/by/aleks/ghcwidget/Widget.java: -------------------------------------------------------------------------------- 1 | package by.aleks.ghcwidget; 2 | 3 | import android.app.PendingIntent; 4 | import android.appwidget.AppWidgetManager; 5 | import android.appwidget.AppWidgetProvider; 6 | import android.content.ComponentName; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.SharedPreferences; 10 | import android.graphics.Bitmap; 11 | import android.graphics.Canvas; 12 | import android.graphics.Color; 13 | import android.graphics.Paint; 14 | import android.graphics.Point; 15 | import android.graphics.Typeface; 16 | import android.os.Bundle; 17 | import android.preference.PreferenceManager; 18 | import android.util.Log; 19 | import android.view.Display; 20 | import android.view.WindowManager; 21 | import android.widget.RemoteViews; 22 | 23 | import java.util.ArrayList; 24 | 25 | import by.aleks.ghcwidget.api.GitHubAPITask; 26 | import by.aleks.ghcwidget.data.ColorTheme; 27 | import by.aleks.ghcwidget.data.CommitsBase; 28 | import by.aleks.ghcwidget.data.Day; 29 | 30 | public class Widget extends AppWidgetProvider { 31 | 32 | public static final int STATUS_OFFLINE = 0; 33 | public static final int STATUS_NOTFOUND = 1; 34 | public static final int STATUS_ONLINE = 2; 35 | 36 | private static final String TAG = "GHCWidget"; 37 | private RemoteViews remoteViews; 38 | private CommitsBase base; 39 | private int status = STATUS_ONLINE; 40 | private int[] appWidgetIds; 41 | private boolean resized = false; 42 | private boolean online; 43 | private Context context; 44 | public static final String LOAD_DATA_KEY = "load_data"; 45 | 46 | //Parameters 47 | private String username; 48 | private int months; 49 | private String theme; 50 | private boolean startOnMonday; 51 | private boolean showDaysLabel; 52 | 53 | 54 | @Override 55 | public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { 56 | if(this.appWidgetIds==null) 57 | this.appWidgetIds = appWidgetIds; 58 | updateWidget(context); 59 | super.onUpdate(context, appWidgetManager, appWidgetIds); 60 | } 61 | 62 | @Override 63 | public void onReceive(Context context, Intent intent) { 64 | final String action = intent.getAction(); 65 | 66 | if (action != null) { 67 | if (action.equals(android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE) || 68 | action.equals(android.appwidget.AppWidgetManager.ACTION_APPWIDGET_ENABLED)) { 69 | 70 | online = intent.getBooleanExtra(LOAD_DATA_KEY, true); //Set the flag of online/caching mode 71 | AppWidgetManager appWM = AppWidgetManager.getInstance(context); 72 | if(this.appWidgetIds==null) 73 | this.appWidgetIds = appWM.getAppWidgetIds(intent.getComponent()); 74 | 75 | updateWidget(context); 76 | } 77 | 78 | super.onReceive(context, intent); 79 | } 80 | } 81 | 82 | @Override 83 | public void onAppWidgetOptionsChanged(Context context, 84 | AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { 85 | 86 | resized = true; 87 | updateWidget(context); 88 | setClickIntent(context, appWidgetId); 89 | super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); 90 | } 91 | 92 | /** 93 | * Determine appropriate view based on width provided. 94 | * 95 | * @param minWidth 96 | * @param minHeight 97 | * @return 98 | */ 99 | private RemoteViews getRemoteViews(Context context, int minWidth, 100 | int minHeight) { 101 | // First find out rows and columns based on width provided. 102 | int rows = getCellsForSize(minHeight); 103 | int columns = getCellsForSize(minWidth); 104 | if(resized){ 105 | adjustMonthsNum(context, columns, rows); 106 | resized = false; 107 | } 108 | if (rows == 1) 109 | return new RemoteViews(context.getPackageName(), R.layout.one_row); 110 | if (columns > 2) { 111 | return new RemoteViews(context.getPackageName(), R.layout.main); 112 | } else { 113 | return new RemoteViews(context.getPackageName(), R.layout.small); 114 | } 115 | } 116 | 117 | /** 118 | * Returns number of cells needed for given size of the widget. 119 | * 120 | * @param size Widget size in dp. 121 | * @return Size in number of cells. 122 | */ 123 | private static int getCellsForSize(int size) { 124 | int n = 2; 125 | while (70 * n - 30 < size) { 126 | ++n; 127 | } 128 | return n - 1; 129 | } 130 | 131 | private void updateWidget(Context context){ 132 | 133 | if(this.context == null) 134 | this.context = context; 135 | 136 | AppWidgetManager mgr = AppWidgetManager.getInstance(context); 137 | int[] appWidgetIds = mgr.getAppWidgetIds(new ComponentName(context, Widget.class)); 138 | // See the dimensions and 139 | Bundle options = mgr.getAppWidgetOptions(appWidgetIds[0]); 140 | 141 | // Get min width and height. 142 | int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH); 143 | int minHeight = options 144 | .getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT); 145 | 146 | // Obtain appropriate widget and update it. 147 | remoteViews = getRemoteViews(context, minWidth, minHeight); 148 | 149 | setPreferences(context); 150 | Bitmap bitmap = processImage(context); 151 | if(bitmap!=null) 152 | remoteViews.setImageViewBitmap(R.id.commitsView, bitmap); 153 | 154 | switch (status){ 155 | case STATUS_OFFLINE: printMessage(context.getResources().getString(R.string.loading_error)); 156 | break; 157 | case STATUS_NOTFOUND: printMessage(context.getResources().getString(R.string.not_found)); 158 | break; 159 | } 160 | 161 | if(appWidgetIds != null){ 162 | for (int appWidgetId : appWidgetIds){ 163 | setClickIntent(context, appWidgetId); 164 | } 165 | } 166 | } 167 | 168 | 169 | private void setPreferences(Context context){ 170 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 171 | username = prefs.getString("username", "xRoker"); 172 | try{ 173 | months = Integer.parseInt(prefs.getString("months", "5")); 174 | if(months>12 || months<1) 175 | months = 12; 176 | } catch (Exception e){ 177 | months = 5; 178 | } 179 | theme = prefs.getString("color_theme", ColorTheme.GITHUB); 180 | startOnMonday = prefs.getBoolean("start_on_monday", false); 181 | showDaysLabel = prefs.getBoolean("days_labels", true); 182 | Log.d(TAG, "Preferences updated: " + username + " " + months + " " + theme); 183 | 184 | } 185 | 186 | //On click open the preferences activity 187 | private void setClickIntent(Context context, int appWidgetId) { 188 | 189 | Intent launchActivity = new Intent(context, WidgetPreferenceActivity.class); 190 | launchActivity.setAction("android.appwidget.action.APPWIDGET_CONFIGURE"); 191 | launchActivity.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); 192 | PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, launchActivity, 0); 193 | 194 | remoteViews.setOnClickPendingIntent(R.id.widget, pendingIntent); 195 | 196 | ComponentName thisWidget = new ComponentName(context, Widget.class); 197 | AppWidgetManager manager = AppWidgetManager.getInstance(context); 198 | manager.updateAppWidget(thisWidget, remoteViews); 199 | 200 | } 201 | 202 | private void updateInfoBar(CommitsBase base){ 203 | remoteViews.setTextViewText(R.id.total, String.valueOf(base.commitsNumber())); 204 | remoteViews.setTextViewText(R.id.totalTextView, context.getString(R.string.total)); 205 | int streak = base.currentStreak(); 206 | remoteViews.setTextViewText(R.id.days, String.valueOf(streak)); 207 | if(streak == 1){ 208 | remoteViews.setTextViewText(R.id.daysTextView, context.getString(R.string.day)); 209 | } else remoteViews.setTextViewText(R.id.daysTextView, context.getString(R.string.days)); 210 | } 211 | 212 | // Load data from GitHub and generate a bitmap with commits. 213 | private Bitmap processImage(Context context){ 214 | 215 | if(base == null || online){ 216 | CommitsBase refreshedBase = loadData(context, username); 217 | if (refreshedBase != null){ 218 | base = refreshedBase; 219 | updateInfoBar(base); 220 | } else return null; 221 | } 222 | 223 | Point size = getScreenSize(context); 224 | int weeks = 4*months+1; 225 | return createBitmap(base, weeks, size, theme); 226 | } 227 | 228 | 229 | //Load data from the api using AsyncTask. 230 | private CommitsBase loadData(Context context, String username){ 231 | String prefDataKey = "offline_data"; 232 | GitHubAPITask task = new GitHubAPITask(this, context); 233 | 234 | try { 235 | status = STATUS_ONLINE; 236 | String data; 237 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 238 | SharedPreferences.Editor editor = prefs.edit(); 239 | // If the widget have to be updated online, load data and save it to SharedPreferences 240 | if(online || !prefs.contains(prefDataKey)){ 241 | data = task.execute(username).get(); 242 | if(data!=null){ 243 | editor.putString(prefDataKey, data); 244 | editor.commit(); 245 | } 246 | } else data = prefs.getString(prefDataKey, null); 247 | return GitHubAPITask.parseResult(data); 248 | } 249 | catch (Exception e) 250 | { 251 | task.cancel(true); 252 | return null; 253 | } 254 | } 255 | 256 | private Point getScreenSize(Context context){ 257 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 258 | Display display = wm.getDefaultDisplay(); 259 | Point size = new Point(); 260 | display.getSize(size); 261 | return size; 262 | } 263 | 264 | 265 | private Bitmap createBitmap(CommitsBase base, int weeksNumber, Point size, String theme){ 266 | float SPACE_RATIO = 0.1f; 267 | int TEXT_GRAPH_SPACE = 7; 268 | 269 | float daysLabelSpaceRatio = showDaysLabel ? 0.8f : 0; 270 | 271 | 272 | float side = size.x/(weeksNumber+daysLabelSpaceRatio) * (1-SPACE_RATIO); 273 | float space = size.x/(weeksNumber+daysLabelSpaceRatio) - side; 274 | float textSize = side*0.87f; 275 | 276 | int height = (int)(7*(side+space)+textSize+TEXT_GRAPH_SPACE); 277 | 278 | ColorTheme colorTheme = new ColorTheme(); 279 | 280 | Bitmap bitmap = Bitmap.createBitmap(size.x, height, Bitmap.Config.ARGB_8888); 281 | Canvas canvas = new Canvas(bitmap); 282 | 283 | Paint paint = new Paint(); 284 | paint.setStyle(Paint.Style.FILL); 285 | 286 | Paint paintText = new Paint(Paint.ANTI_ALIAS_FLAG); 287 | paintText.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)); 288 | paintText.setTextSize(textSize); 289 | paintText.setColor(Color.GRAY); 290 | 291 | if(base!=null){ 292 | float x = 0, y; 293 | 294 | // Draw days labels. 295 | if(showDaysLabel){ 296 | y = startOnMonday ? textSize*2+TEXT_GRAPH_SPACE : textSize*2+TEXT_GRAPH_SPACE+side; 297 | canvas.drawText(context.getString(R.string.m), 0, y, paintText); 298 | canvas.drawText(context.getString(R.string.w), 0, y+2*(side+space), paintText); 299 | canvas.drawText(context.getString(R.string.f), textSize*0.1f, y+4*(side+space), paintText); 300 | if(startOnMonday) 301 | canvas.drawText(context.getString(R.string.s), textSize*0.1f, y+6*(side+space), paintText); 302 | 303 | x = textSize; 304 | } 305 | 306 | y = textSize+TEXT_GRAPH_SPACE; 307 | 308 | ArrayList> weeks = base.getWeeks(); 309 | 310 | int firstWeek = base.getFirstWeekOfMonth(weeksNumber); //Number of the week above which there will be the first month name. 311 | 312 | for(int i = weeks.size() - weeksNumber; i 1){ 349 | switch (numColumns){ 350 | case 2: editor.putString("months", "2"); 351 | break; 352 | case 3: editor.putString("months", "4"); 353 | break; 354 | case 4: editor.putString("months", "5"); 355 | break; 356 | case 5: editor.putString("months", "5"); 357 | break; 358 | case 6: editor.putString("months", "7"); 359 | break; 360 | case 8: editor.putString("months", "9"); 361 | break; 362 | default: editor.putString("months", "12"); 363 | } 364 | } else { 365 | switch (numColumns){ 366 | case 2: editor.putString("months", "2"); 367 | break; 368 | case 3: editor.putString("months", "5"); 369 | break; 370 | case 4: editor.putString("months", "6"); 371 | break; 372 | case 5: editor.putString("months", "7"); 373 | break; 374 | case 6: editor.putString("months", "11"); 375 | break; 376 | default: editor.putString("months", "12"); 377 | } 378 | } 379 | editor.commit(); 380 | } 381 | 382 | private void printMessage(String msg){ 383 | remoteViews.setTextViewText(R.id.total, ""); 384 | remoteViews.setTextViewText(R.id.totalTextView, ""); 385 | remoteViews.setTextViewText(R.id.days, ""); 386 | remoteViews.setTextViewText(R.id.daysTextView, msg); 387 | } 388 | 389 | public void setStatus(int status){ 390 | this.status = status; 391 | } 392 | 393 | } 394 | --------------------------------------------------------------------------------