├── .gitignore ├── GithubStatus ├── build.gradle ├── dev.sh └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── Roboto-Black.ttf │ ├── Roboto-Bold.ttf │ ├── Roboto-Light.ttf │ ├── Roboto-LightItalic.ttf │ ├── Roboto-Thin.ttf │ └── Roboto-ThinItalic.ttf │ ├── java │ └── com │ │ └── deange │ │ └── githubstatus │ │ ├── MainApplication.java │ │ ├── PlatformUtils.java │ │ ├── Utils.java │ │ ├── controller │ │ ├── GsonController.java │ │ ├── NotificationController.java │ │ └── StateController.java │ │ ├── http │ │ ├── BaseApi.java │ │ ├── GithubApi.java │ │ ├── GithubStatusApi.java │ │ ├── GithubStatusMessagesApi.java │ │ ├── HttpIOException.java │ │ ├── HttpTask.java │ │ ├── OneParamApi.java │ │ └── SimpleApi.java │ │ ├── model │ │ ├── Level.java │ │ ├── Objects.java │ │ ├── SettingsInfo.java │ │ └── Status.java │ │ ├── push │ │ ├── BackoffHandler.java │ │ ├── OnPushMessageReceivedListener.java │ │ ├── PushBaseActivity.java │ │ ├── PushBroadcastReceiver.java │ │ ├── PushConstants.java │ │ ├── PushIntentService.java │ │ ├── PushMessageReceiver.java │ │ ├── PushRegistrar.java │ │ ├── PushServerRegistrar.java │ │ └── PushUtils.java │ │ └── ui │ │ ├── MainActivity.java │ │ ├── MainFragment.java │ │ ├── MessagesAdapter.java │ │ ├── SettingsFragment.java │ │ ├── TrackedActivity.java │ │ ├── ViewUtils.java │ │ └── view │ │ ├── AutoScaleTextView.java │ │ ├── FontTextView.java │ │ ├── SelectableRoundedImageView.java │ │ └── SliceView.java │ └── res │ ├── drawable-hdpi │ ├── ic_action_info.png │ ├── ic_action_overflow.png │ ├── ic_launcher.png │ └── ic_stat_octocat.png │ ├── drawable-mdpi │ ├── ic_action_info.png │ ├── ic_action_overflow.png │ ├── ic_launcher.png │ └── ic_stat_octocat.png │ ├── drawable-nodpi │ ├── bkg_menu_banner.png │ ├── octocat_notif_green.png │ ├── octocat_notif_red.png │ ├── octocat_notif_yellow.png │ └── thumbnail.jpg │ ├── drawable-xhdpi │ ├── ic_action_info.png │ ├── ic_action_overflow.png │ ├── ic_launcher.png │ └── ic_stat_octocat.png │ ├── drawable-xxhdpi │ ├── ic_action_info.png │ ├── ic_action_overflow.png │ ├── ic_launcher.png │ └── ic_stat_octocat.png │ ├── drawable │ ├── background_tile.xml │ └── message_status_indicator.xml │ ├── layout-land │ ├── activity_main.xml │ ├── fragment_main.xml │ └── view_flipper_item.xml │ ├── layout │ ├── activity_gcm.xml │ ├── activity_main.xml │ ├── dialog_about.xml │ ├── fragment_main.xml │ ├── fragment_settings.xml │ ├── list_item_message.xml │ └── view_flipper_item.xml │ ├── menu │ ├── activity_menu.xml │ └── options_menu.xml │ ├── values-land-v21 │ └── styles.xml │ ├── values-land │ └── bools.xml │ ├── values-v19 │ └── styles.xml │ ├── values-v21 │ └── styles.xml │ └── values │ ├── attrs.xml │ ├── bools.xml │ ├── colours.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | local.properties 2 | 3 | *.iml 4 | *.ipr 5 | *.iws 6 | 7 | .gradle/ 8 | .idea/ 9 | build/ 10 | design/ 11 | releases/ -------------------------------------------------------------------------------- /GithubStatus/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:1.0.0' 7 | } 8 | } 9 | 10 | apply plugin: 'com.android.application' 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | def majorVersion = 1; 17 | def minorVersion = 1; 18 | def buildVersion = 0; 19 | 20 | 21 | android { 22 | compileSdkVersion 21 23 | buildToolsVersion "20.0.0" 24 | 25 | defaultConfig { 26 | minSdkVersion 14 27 | targetSdkVersion 21 28 | 29 | versionName "${majorVersion}.${minorVersion}" + (buildVersion == 0 ? "" : "${buildVersion}") 30 | versionCode ((majorVersion * 100) + (minorVersion * 10) + (buildVersion * 1)) 31 | } 32 | 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_1_7 35 | targetCompatibility = JavaVersion.VERSION_1_7 36 | } 37 | 38 | signingConfigs { 39 | release { 40 | storeFile new File("notYourRealFileName") 41 | keyAlias "notYourRealKeystoreAlias" 42 | storePassword "notYourRealPassword" 43 | keyPassword "notYourRealPassword" 44 | } 45 | } 46 | 47 | buildTypes { 48 | 49 | all { 50 | buildConfigField "String", "SENDER_ID", "\"196611706338\"" 51 | } 52 | 53 | debug { 54 | def proc = (project.projectDir.absolutePath + "/dev.sh").execute() 55 | proc.waitFor() 56 | def localhostIpAddr = "http://" + proc.in.text.split("\n")[0] + ":8080" 57 | 58 | buildConfigField "String", "SERVER_URL", "\"${localhostIpAddr}\"" 59 | } 60 | 61 | release { 62 | buildConfigField "String", "SERVER_URL", "\"http://githubstatus.appspot.com\"" 63 | 64 | signingConfig signingConfigs.release 65 | } 66 | } 67 | 68 | lintOptions { 69 | abortOnError false 70 | } 71 | } 72 | 73 | task askForPasswords << { 74 | 75 | if (System.console() == null) { 76 | throw new RuntimeException("No console available!") 77 | } 78 | 79 | def filePath = new File(System.getenv("KEYSTORE_PATH")) 80 | def alias = System.getenv("KEYSTORE_ALIAS") 81 | def storePw = new String(System.console().readPassword("\nKeystore password: ")) 82 | def keyPw = new String(System.console().readPassword("Key password for '" + alias + "': ")) 83 | 84 | android.signingConfigs.release.storeFile = filePath 85 | android.signingConfigs.release.keyAlias = alias 86 | android.signingConfigs.release.storePassword = storePw 87 | android.signingConfigs.release.keyPassword = keyPw 88 | } 89 | 90 | tasks.whenTaskAdded { theTask -> 91 | if ("packageRelease".equals(theTask.name)) { 92 | theTask.dependsOn "askForPasswords" 93 | } 94 | } 95 | 96 | dependencies { 97 | compile 'com.google.android.gms:play-services:3.1.+' 98 | compile 'com.android.support:appcompat-v7:21.0.3' 99 | compile 'com.squareup.okhttp:okhttp:1.5.3' 100 | compile 'com.google.code.gson:gson:2.3' 101 | compile 'com.makeramen:roundedimageview:1.3.0' 102 | compile 'com.melnykov:floatingactionbutton:1.2.0' 103 | } 104 | -------------------------------------------------------------------------------- /GithubStatus/dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ifconfig | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p' | head -n 1 -------------------------------------------------------------------------------- /GithubStatus/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /GithubStatus/src/main/assets/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/assets/Roboto-Black.ttf -------------------------------------------------------------------------------- /GithubStatus/src/main/assets/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/assets/Roboto-Bold.ttf -------------------------------------------------------------------------------- /GithubStatus/src/main/assets/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/assets/Roboto-Light.ttf -------------------------------------------------------------------------------- /GithubStatus/src/main/assets/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/assets/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /GithubStatus/src/main/assets/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/assets/Roboto-Thin.ttf -------------------------------------------------------------------------------- /GithubStatus/src/main/assets/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/assets/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | 6 | import com.deange.githubstatus.controller.GsonController; 7 | import com.deange.githubstatus.controller.NotificationController; 8 | import com.deange.githubstatus.controller.StateController; 9 | 10 | public class MainApplication extends Application { 11 | 12 | private static final String TAG = MainApplication.class.getSimpleName(); 13 | 14 | @Override 15 | public void onCreate() { 16 | Log.v(TAG, "onCreate()"); 17 | super.onCreate(); 18 | 19 | try { 20 | // Initialize the Gson singleton 21 | GsonController.getInstance(); 22 | 23 | // Initialize the SharedPreferences wrapper 24 | StateController.createInstance(getApplicationContext()); 25 | 26 | // Initialize the Notification service 27 | NotificationController.createInstance(getApplicationContext()); 28 | 29 | } catch (final Exception e) { 30 | Log.wtf(TAG, "Fatal error occured!", e); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/PlatformUtils.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus; 2 | 3 | import android.os.Build; 4 | 5 | public final class PlatformUtils { 6 | 7 | private PlatformUtils() { 8 | throw new UnsupportedOperationException(); 9 | } 10 | 11 | public static boolean hasIcs() { 12 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; 13 | } 14 | 15 | public static boolean hasIcsMR2() { 16 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1; 17 | } 18 | 19 | public static boolean hasJellybean() { 20 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; 21 | } 22 | 23 | public static boolean hasJellybeanMR1() { 24 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; 25 | } 26 | 27 | public static boolean hasJellybeanMR2() { 28 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; 29 | } 30 | 31 | public static boolean hasKitKat() { 32 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 33 | } 34 | 35 | public static boolean hasLollipop() { 36 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/Utils.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageManager; 5 | import android.content.res.Resources; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.io.UnsupportedEncodingException; 12 | import java.security.MessageDigest; 13 | import java.security.NoSuchAlgorithmException; 14 | import java.util.Arrays; 15 | 16 | public final class Utils { 17 | 18 | private Utils() { 19 | // Uninstantiable 20 | } 21 | 22 | public static String hash(final String toHash) { 23 | 24 | String hash = null; 25 | 26 | try { 27 | final MessageDigest digest = MessageDigest.getInstance("SHA-1"); 28 | byte[] bytes = toHash.getBytes("UTF-8"); 29 | digest.update(bytes, 0, bytes.length); 30 | bytes = digest.digest(); 31 | final StringBuilder sb = new StringBuilder(); 32 | for (byte b : bytes) { 33 | sb.append(String.format("%02X", b)); 34 | } 35 | hash = sb.toString(); 36 | 37 | } catch (final NoSuchAlgorithmException e) { 38 | e.printStackTrace(); 39 | hash = String.valueOf(Arrays.hashCode(e.getStackTrace())); 40 | 41 | } catch (final UnsupportedEncodingException e) { 42 | e.printStackTrace(); 43 | hash = String.valueOf(Arrays.hashCode(e.getStackTrace())); 44 | } 45 | 46 | return hash; 47 | } 48 | 49 | public static String streamToString(final InputStream in) throws IOException { 50 | 51 | final BufferedReader reader = new BufferedReader(new InputStreamReader(in)); 52 | final StringBuilder sb = new StringBuilder(); 53 | 54 | String line; 55 | while ((line = reader.readLine()) != null) { 56 | sb.append(line); 57 | } 58 | 59 | reader.close(); 60 | 61 | return sb.toString(); 62 | } 63 | 64 | public static boolean showNiceView(final Context context) { 65 | return context.getResources().getBoolean(R.bool.nice_view); 66 | } 67 | 68 | public static String getVersionName() { 69 | return BuildConfig.VERSION_NAME; 70 | } 71 | 72 | public static int getVersionCode() { 73 | return BuildConfig.VERSION_CODE; 74 | } 75 | 76 | public static String buildAction(final String action) { 77 | return BuildConfig.APPLICATION_ID + "." + action; 78 | } 79 | 80 | public static String buildPreferences(final String name) { 81 | return BuildConfig.APPLICATION_ID + "." + "prefs" + "." + name; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/controller/GsonController.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.controller; 2 | 3 | import com.google.gson.Gson; 4 | 5 | public class GsonController { 6 | public static final String TAG = GsonController.class.getSimpleName(); 7 | 8 | public static Gson sCache = null; 9 | private static final Object sLock = new Object(); 10 | 11 | public static synchronized void createInstance() { 12 | if (sCache == null) { 13 | sCache = new Gson(); 14 | } 15 | } 16 | 17 | public static Gson getInstance() { 18 | synchronized (sLock) { 19 | if (sCache == null) { 20 | createInstance(); 21 | } 22 | return sCache; 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/controller/NotificationController.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.controller; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationManager; 5 | import android.app.PendingIntent; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.graphics.Bitmap; 9 | import android.graphics.BitmapFactory; 10 | import android.support.v4.app.NotificationCompat; 11 | 12 | import com.deange.githubstatus.R; 13 | import com.deange.githubstatus.model.Level; 14 | import com.deange.githubstatus.model.Status; 15 | import com.deange.githubstatus.ui.MainActivity; 16 | 17 | public class NotificationController { 18 | 19 | private static final int NOTIFICATION_ID = 0xCafeBabe; 20 | private static final Object sLock = new Object(); 21 | private static NotificationController sInstance; 22 | 23 | private final Context mContext; 24 | private final NotificationManager mNotificationManager; 25 | 26 | public static synchronized NotificationController createInstance(final Context context) { 27 | if (sInstance == null) { 28 | sInstance = new NotificationController(context.getApplicationContext()); 29 | } 30 | return sInstance; 31 | } 32 | 33 | public static NotificationController getInstance() { 34 | synchronized (sLock) { 35 | if (sInstance == null) { 36 | throw new IllegalStateException("Notification controller instance invalid"); 37 | } 38 | return sInstance; 39 | } 40 | } 41 | 42 | private NotificationController(final Context context) { 43 | mContext = context; 44 | mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 45 | } 46 | 47 | public void notificationForStatus(final Status status) { 48 | 49 | if (status == null || status.isSpecialStatus()) { 50 | // Do not show status for a special or null status 51 | return; 52 | } 53 | 54 | final String title = mContext.getString(R.string.app_name); 55 | final String level = Status.getTranslatedStatus(mContext, status).toUpperCase(); 56 | final String body = status.getBody(); 57 | final String notifBody = level + " - " + body; 58 | 59 | final long when = System.currentTimeMillis(); 60 | final int tickerIcon = R.drawable.ic_stat_octocat; 61 | final int icon = statusIconForLevel(status.getLevel()); 62 | final Bitmap largeIcon = BitmapFactory.decodeResource(mContext.getResources(), icon); 63 | 64 | final Intent notificationIntent = new Intent(mContext, MainActivity.class) 65 | .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); 66 | final PendingIntent intent = PendingIntent.getActivity( 67 | mContext, NOTIFICATION_ID, notificationIntent, 0); 68 | 69 | final NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle() 70 | .bigText(notifBody); 71 | 72 | final Notification notification = new NotificationCompat.Builder(mContext) 73 | .setContentTitle(title) 74 | .setContentText(notifBody) 75 | .setSmallIcon(tickerIcon) 76 | .setLargeIcon(largeIcon) 77 | .setTicker(notifBody) 78 | .setStyle(style) 79 | .setWhen(when) 80 | .setContentIntent(intent) 81 | .setAutoCancel(true) 82 | .build(); 83 | 84 | mNotificationManager.notify(NOTIFICATION_ID, notification); 85 | } 86 | 87 | private int statusIconForLevel(final Level level) { 88 | 89 | if (level == null) { 90 | return R.drawable.octocat_notif_green; 91 | 92 | } else { 93 | switch (level) { 94 | case MAJOR: 95 | return R.drawable.octocat_notif_red; 96 | 97 | case MINOR: 98 | return R.drawable.octocat_notif_yellow; 99 | 100 | case GOOD: 101 | default: 102 | return R.drawable.octocat_notif_green; 103 | } 104 | } 105 | 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/controller/StateController.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.controller; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import com.deange.githubstatus.model.Status; 7 | 8 | public class StateController { 9 | 10 | private static final String TAG = StateController.class.getSimpleName(); 11 | 12 | private static final String PREFERENCES_NAME = TAG + ".prefs"; 13 | private static final String KEY_SAVED_STATUS = "last_status"; 14 | 15 | private static final Object sLock = new Object(); 16 | private static StateController sInstance; 17 | 18 | private SharedPreferences mPreferences; 19 | 20 | public static synchronized void createInstance(final Context context) { 21 | if (sInstance == null) { 22 | sInstance = new StateController(context.getApplicationContext()); 23 | } 24 | } 25 | 26 | public void setStatus(final Status status) { 27 | synchronized (sLock) { 28 | mPreferences.edit().putString( 29 | KEY_SAVED_STATUS, GsonController.getInstance().toJson(status)).apply(); 30 | } 31 | } 32 | 33 | public Status getStatus() { 34 | synchronized (sLock) { 35 | return GsonController.getInstance().fromJson( 36 | mPreferences.getString(KEY_SAVED_STATUS, null), Status.class); 37 | } 38 | } 39 | 40 | public void clear() { 41 | synchronized (sLock) { 42 | mPreferences.edit().clear().apply(); 43 | } 44 | } 45 | 46 | public static StateController getInstance() { 47 | synchronized (sLock) { 48 | if (sInstance == null) { 49 | throw new IllegalStateException("StateController has not been created"); 50 | } 51 | return sInstance; 52 | } 53 | } 54 | 55 | private StateController(final Context context) { 56 | mPreferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/http/BaseApi.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.http; 2 | 3 | import android.content.Context; 4 | 5 | import com.squareup.okhttp.OkHttpClient; 6 | 7 | import java.io.IOException; 8 | import java.lang.reflect.Type; 9 | 10 | public abstract class BaseApi { 11 | 12 | protected final Context mContext; 13 | private static final OkHttpClient sClient = new OkHttpClient(); 14 | 15 | public BaseApi(final Context context) { 16 | mContext = context; 17 | } 18 | 19 | public abstract String getBaseApiEndpoint(); 20 | 21 | public OkHttpClient getClient() { 22 | return sClient; 23 | } 24 | 25 | public S get(final Type clazz, final String url) throws IOException { 26 | throw new UnsupportedOperationException(); 27 | } 28 | 29 | public S post(final T entity, final String url) throws IOException { 30 | throw new UnsupportedOperationException(); 31 | } 32 | 33 | public S put(final T entity, final String url) throws IOException { 34 | throw new UnsupportedOperationException(); 35 | } 36 | 37 | public S delete(final T entity, final String url) throws IOException { 38 | throw new UnsupportedOperationException(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/http/GithubApi.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.http; 2 | 3 | import android.content.Context; 4 | import android.os.AsyncTask; 5 | 6 | import com.deange.githubstatus.model.Status; 7 | import com.google.gson.reflect.TypeToken; 8 | 9 | import java.io.IOException; 10 | import java.lang.reflect.Type; 11 | import java.util.List; 12 | 13 | public class GithubApi { 14 | 15 | // URL ENDPOINTS 16 | public static final String BASE_URL = "https://status.github.com"; 17 | public static final String BASE_API_URL = BASE_URL + "/api"; 18 | public static final String JSON = ".json"; 19 | 20 | public static final String STATUS = "/status" + JSON; 21 | public static final String LAST_MESSAGE = "/last-message" + JSON; 22 | public static final String LAST_MESSAGES = "/messages" + JSON; 23 | 24 | 25 | // STATUS CODES (must be lowercase!) 26 | public static final String STATUS_UNAVAILABLE = "unavailable"; 27 | public static final String STATUS_GOOD = "good"; 28 | public static final String STATUS_MINOR = "minor"; 29 | public static final String STATUS_MAJOR = "major"; 30 | 31 | // HTTP METHODS 32 | public static void getStatus(final Context context, final HttpTask.Listener listener) { 33 | doApiGet(new GithubStatusApi(context), Status.class, GithubApi.LAST_MESSAGE, listener); 34 | } 35 | 36 | public static Status getStatus(final Context context) throws IOException { 37 | return doApiGet(new GithubStatusApi(context), Status.class, GithubApi.LAST_MESSAGE); 38 | } 39 | 40 | public static void getMessages(final Context context, final HttpTask.Listener> listener) { 41 | doApiGet(new GithubStatusMessagesApi(context), new TypeToken>(){}.getType(), GithubApi.LAST_MESSAGES, listener); 42 | } 43 | 44 | public static List getMessages(final Context context) throws IOException { 45 | return doApiGet(new GithubStatusMessagesApi(context), new TypeToken>(){}.getType(), GithubApi.LAST_MESSAGES); 46 | } 47 | 48 | private static void doApiGet(final BaseApi api, final Type clazz, final String url, final HttpTask.Listener listener) { 49 | 50 | final AsyncTask getTask = new AsyncTask() { 51 | 52 | Exception ex = null; 53 | 54 | @Override 55 | public T doInBackground(final Void... params) { 56 | try { 57 | return api.get(clazz, url); 58 | } catch (IOException e) { 59 | 60 | ex = e; 61 | return null; 62 | } 63 | } 64 | 65 | @Override 66 | public void onPostExecute(final T entity) { 67 | if (listener != null) { 68 | listener.onGet(entity, ex); 69 | } 70 | } 71 | }; 72 | 73 | getTask.execute(); 74 | } 75 | 76 | private static T doApiGet(final BaseApi api, final Type clazz, final String url) throws IOException { 77 | return api.get(clazz, url); 78 | } 79 | 80 | private GithubApi() { 81 | // Uninstantiable 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/http/GithubStatusApi.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.http; 2 | 3 | import android.content.Context; 4 | 5 | import com.deange.githubstatus.model.Status; 6 | 7 | public class GithubStatusApi extends SimpleApi { 8 | 9 | public GithubStatusApi(final Context context) { 10 | super(context); 11 | } 12 | 13 | @Override 14 | public String getBaseApiEndpoint() { 15 | return GithubApi.BASE_API_URL; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/http/GithubStatusMessagesApi.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.http; 2 | 3 | import android.content.Context; 4 | 5 | import com.deange.githubstatus.model.Status; 6 | 7 | import java.util.List; 8 | 9 | public class GithubStatusMessagesApi extends SimpleApi> { 10 | 11 | public GithubStatusMessagesApi(final Context context) { 12 | super(context); 13 | } 14 | 15 | @Override 16 | public String getBaseApiEndpoint() { 17 | return GithubApi.BASE_API_URL; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/http/HttpIOException.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.http; 2 | 3 | import java.io.IOException; 4 | 5 | public class HttpIOException extends IOException { 6 | 7 | private int mStatusCode; 8 | 9 | public HttpIOException(final int statusCode) { 10 | super(); 11 | setStatusCode(statusCode); 12 | } 13 | 14 | public HttpIOException(final String detailMessage, final int statusCode) { 15 | super(detailMessage); 16 | setStatusCode(statusCode); 17 | } 18 | 19 | public HttpIOException(final Throwable cause, final int statusCode) { 20 | super(cause); 21 | setStatusCode(statusCode); 22 | } 23 | 24 | public HttpIOException(final String message, final Throwable cause, final int statusCode) { 25 | super(message, cause); 26 | setStatusCode(statusCode); 27 | } 28 | 29 | private void setStatusCode(final int statusCode) { 30 | mStatusCode = statusCode; 31 | } 32 | 33 | public int getStatusCode() { 34 | return mStatusCode; 35 | } 36 | 37 | public boolean isInformational() { 38 | return mStatusCode >= 100 && mStatusCode <= 199; 39 | } 40 | 41 | public boolean isSuccess() { 42 | return mStatusCode >= 200 && mStatusCode <= 299; 43 | } 44 | 45 | public boolean isRedirection() { 46 | return mStatusCode >= 300 && mStatusCode <= 399; 47 | } 48 | 49 | public boolean isClientError() { 50 | return mStatusCode >= 400 && mStatusCode <= 499; 51 | } 52 | 53 | public boolean isServerError() { 54 | return mStatusCode >= 500 && mStatusCode <= 599; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/http/HttpTask.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.http; 2 | 3 | public class HttpTask { 4 | 5 | public static class Listener implements OnHttpRequestDoneListener { 6 | 7 | @Override 8 | public void onGet(final T entity, final Exception exception) { 9 | } 10 | 11 | @Override 12 | public void onPost(final T entity, final Exception exception) { 13 | } 14 | } 15 | 16 | public interface OnHttpRequestDoneListener { 17 | void onGet(final T entity, final Exception exception); 18 | 19 | void onPost(final T entity, final Exception exception); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/http/OneParamApi.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.http; 2 | 3 | import android.content.Context; 4 | 5 | public abstract class OneParamApi extends BaseApi { 6 | 7 | public OneParamApi(final Context context) { 8 | super(context); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/http/SimpleApi.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.http; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.deange.githubstatus.Utils; 7 | import com.deange.githubstatus.controller.GsonController; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.lang.reflect.Type; 13 | import java.net.HttpURLConnection; 14 | import java.net.URL; 15 | 16 | public abstract class SimpleApi extends OneParamApi { 17 | 18 | private static final String TAG = SimpleApi.class.getSimpleName(); 19 | 20 | public SimpleApi(final Context context) { 21 | super(context); 22 | } 23 | 24 | @Override 25 | @SuppressWarnings("unchecked") 26 | public T get(final Type clazz, final String url) throws IOException { 27 | 28 | // Create HTTP request 29 | final String apiUrl = normalizeUrl(url); 30 | final HttpURLConnection connection = getClient().open(new URL(apiUrl)); 31 | 32 | // Retrieve GET response 33 | final InputStream in = connection.getInputStream(); 34 | final String outputJson = Utils.streamToString(in); 35 | in.close(); 36 | 37 | try { 38 | return (T) GsonController.getInstance().fromJson(outputJson, clazz); 39 | 40 | } catch (final Exception ex) { 41 | Log.w(TAG, "GET from " + apiUrl + " failed with " + connection.getResponseCode() + "."); 42 | ex.printStackTrace(); 43 | return null; 44 | } 45 | 46 | } 47 | 48 | @Override 49 | @SuppressWarnings("unchecked") 50 | public T post(final T entity, final String url) throws IOException { 51 | 52 | // Create HTTP request 53 | final String apiUrl = normalizeUrl(url); 54 | final String json = GsonController.getInstance().toJson(entity); 55 | final HttpURLConnection connection = getClient().open(new URL(apiUrl)); 56 | 57 | // Write POST request 58 | connection.setRequestMethod("POST"); 59 | final OutputStream out = connection.getOutputStream(); 60 | out.write(json.getBytes()); 61 | out.close(); 62 | 63 | // Retrieve POST response 64 | final InputStream in = connection.getInputStream(); 65 | final String outputJson = Utils.streamToString(in); 66 | in.close(); 67 | 68 | try { 69 | return GsonController.getInstance().fromJson(outputJson, (Class) entity.getClass()); 70 | 71 | } catch (final Exception ex) { 72 | Log.w(TAG, "POST " + json + " to " + apiUrl + " failed with " + connection.getResponseCode() + "."); 73 | ex.printStackTrace(); 74 | return null; 75 | } 76 | 77 | } 78 | 79 | private String normalizeUrl(final String apiEndpoint) { 80 | 81 | String base = getBaseApiEndpoint(); 82 | if (!base.isEmpty() && base.charAt(base.length() - 1) == '/') { 83 | // Trim trailing slash 84 | base = base.substring(0, base.length() - 1); 85 | } 86 | 87 | // Ensure API path has leading slash 88 | final String apiPath; 89 | if (apiEndpoint.startsWith("/")) { 90 | apiPath = apiEndpoint; 91 | 92 | } else { 93 | apiPath = "/" + apiEndpoint; 94 | } 95 | 96 | return base + apiPath; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/model/Level.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.model; 2 | 3 | import com.deange.githubstatus.http.GithubApi; 4 | 5 | public enum Level { 6 | GOOD(GithubApi.STATUS_GOOD), 7 | MINOR(GithubApi.STATUS_MINOR), 8 | MAJOR(GithubApi.STATUS_MAJOR); 9 | 10 | public String type; 11 | 12 | Level(final String t) { 13 | type = t; 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | return type; 19 | } 20 | 21 | public static Level from(final String string) { 22 | for (final Level level : values()) { 23 | if (level.type.equalsIgnoreCase(string)) { 24 | return level; 25 | } 26 | } 27 | 28 | return null; 29 | } 30 | 31 | public boolean isLessThan(final Level level) { 32 | return level.isHigherThan(this); 33 | } 34 | 35 | public boolean isHigherThan(final Level level) { 36 | // Use an example as a reference to test against 37 | return Math.signum(compareTo(level)) == Math.signum(MAJOR.compareTo(GOOD)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/model/Objects.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.model; 2 | 3 | import java.util.Arrays; 4 | import java.util.Comparator; 5 | 6 | public class Objects { 7 | 8 | private Objects() { 9 | // Uninstantiable 10 | } 11 | 12 | public static boolean equals(Object a, Object b) { 13 | return (a == b) || (a != null && a.equals(b)); 14 | } 15 | 16 | public static boolean deepEquals(Object[] a, Object[] b) { 17 | if (a == b) 18 | return true; 19 | else if (a == null || b == null) 20 | return false; 21 | else 22 | return Arrays.deepEquals(a, b); 23 | } 24 | 25 | public static int hashCode(Object o) { 26 | return o != null ? o.hashCode() : 0; 27 | } 28 | 29 | public static int hash(Object... values) { 30 | return Arrays.hashCode(values); 31 | } 32 | 33 | public static String toString(Object o) { 34 | return String.valueOf(o); 35 | } 36 | 37 | public static String toString(Object o, String nullDefault) { 38 | return (o != null) ? o.toString() : nullDefault; 39 | } 40 | 41 | public static int compare(T a, T b, Comparator c) { 42 | return (a == b) ? 0 : c.compare(a, b); 43 | } 44 | 45 | public static T requireNonNull(T obj) { 46 | if (obj == null) 47 | throw new NullPointerException(); 48 | return obj; 49 | } 50 | 51 | public static T requireNonNull(T obj, String message) { 52 | if (obj == null) 53 | throw new NullPointerException(message); 54 | return obj; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/model/SettingsInfo.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.model; 2 | 3 | public class SettingsInfo { 4 | 5 | private final boolean mGcmEnabled; 6 | 7 | private SettingsInfo(final Builder builder) { 8 | mGcmEnabled = builder.gcmEnabled; 9 | } 10 | 11 | public boolean isGCMEnabled() { 12 | return mGcmEnabled; 13 | } 14 | 15 | public static final class Builder { 16 | 17 | private boolean gcmEnabled; 18 | 19 | public Builder gcm(final boolean enabled) { 20 | gcmEnabled = enabled; 21 | return this; 22 | } 23 | 24 | public SettingsInfo build() { 25 | return new SettingsInfo(this); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/model/Status.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.model; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | import android.text.format.Time; 6 | 7 | import com.deange.githubstatus.R; 8 | import com.deange.githubstatus.http.GithubApi; 9 | import com.google.gson.annotations.SerializedName; 10 | 11 | import java.util.Collections; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | public class Status { 16 | 17 | public static final String STATUS = "status"; 18 | public static final String BODY = "body"; 19 | public static final String CREATED_ON = "created_on"; 20 | 21 | private static final Map STATUS_MAP; 22 | static { 23 | final Map map = new HashMap<>(); 24 | map.put(GithubApi.STATUS_UNAVAILABLE, R.string.error_server_unavailable_status); 25 | map.put(GithubApi.STATUS_GOOD, R.string.status_good); 26 | map.put(GithubApi.STATUS_MINOR, R.string.status_minor); 27 | map.put(GithubApi.STATUS_MAJOR, R.string.status_major); 28 | STATUS_MAP = Collections.unmodifiableMap(map); 29 | } 30 | 31 | public enum SpecialType { 32 | ERROR, LOADING 33 | } 34 | 35 | @SerializedName(STATUS) 36 | private String mStatus; 37 | 38 | @SerializedName(BODY) 39 | private String mBody; 40 | 41 | @SerializedName(CREATED_ON) 42 | private String mCreatedOn; 43 | 44 | private boolean mSpecial = false; 45 | 46 | public static Status getSpecialStatus(final Context context, final SpecialType type) { 47 | 48 | final Time now = new Time(); 49 | now.setToNow(); 50 | 51 | final Status specialStatus = new Status(); 52 | specialStatus.mCreatedOn = now.format3339(false); 53 | specialStatus.mSpecial = true; 54 | 55 | switch (type) { 56 | 57 | case ERROR: 58 | specialStatus.mStatus = context.getString(R.string.error_server_unavailable_status); 59 | specialStatus.mBody = context.getString(R.string.error_server_unavailable_message); 60 | break; 61 | 62 | case LOADING: 63 | specialStatus.mStatus = context.getString(R.string.loading_status); 64 | specialStatus.mBody = context.getString(R.string.loading_message); 65 | break; 66 | 67 | } 68 | 69 | return specialStatus; 70 | } 71 | 72 | public Status() { 73 | } 74 | 75 | public boolean isSpecialStatus() { 76 | return mSpecial; 77 | } 78 | 79 | public String getStatus() { 80 | return mStatus == null ? "" : mStatus; 81 | } 82 | 83 | public String getBody() { 84 | return mBody == null ? "" : mBody; 85 | } 86 | 87 | public Level getLevel() { 88 | return Level.from(getStatus()); 89 | } 90 | 91 | public static String getTranslatedStatus(final Context context, final Status status) { 92 | 93 | final String translatedStatus; 94 | 95 | if (status != null && status.getStatus() != null) { 96 | final String key = status.getStatus().toLowerCase(); 97 | if (!STATUS_MAP.containsKey(key)) { 98 | // Fallback to default string 99 | translatedStatus = key; 100 | 101 | } else { 102 | final Integer statusResId = STATUS_MAP.get(key); 103 | translatedStatus = context.getString(statusResId); 104 | } 105 | 106 | } else { 107 | 108 | if (context != null) { 109 | translatedStatus = context.getString(R.string.error_server_unavailable_status); 110 | 111 | } else { 112 | translatedStatus = "Unavailable"; 113 | } 114 | 115 | } 116 | 117 | return translatedStatus; 118 | } 119 | 120 | public Time getCreatedOn() { 121 | if (TextUtils.isEmpty(mCreatedOn)) { 122 | return null; 123 | 124 | } else { 125 | final Time time = new Time(Time.TIMEZONE_UTC); 126 | time.parse3339(mCreatedOn); 127 | time.switchTimezone(Time.getCurrentTimezone()); 128 | return time; 129 | } 130 | } 131 | 132 | public static boolean shouldAlert(final Status oldStatus, final Status newStatus) { 133 | 134 | if (oldStatus == null) { 135 | // First request ever, no alert necessary 136 | return false; 137 | 138 | } else if (newStatus == null) { 139 | // Prevent a NullPointerException, but this *really* shouldn't happen 140 | return false; 141 | 142 | } else if (newStatus.getLevel().isHigherThan(Level.GOOD)) { 143 | // Any time time the status changes and it is above GOOD, 144 | // we should be alerting the change. 145 | // Clients may block it if the user decides to. 146 | return !Objects.equals(oldStatus, newStatus); 147 | 148 | } else { 149 | // Alert on a status level change 150 | return oldStatus.getLevel() != newStatus.getLevel(); 151 | } 152 | } 153 | 154 | @Override 155 | public boolean equals(Object obj) { 156 | 157 | if (super.equals(obj)) { 158 | return true; 159 | } 160 | 161 | if (!(obj instanceof Status)) { 162 | return false; 163 | } 164 | 165 | final Status status = (Status) obj; 166 | 167 | if (!Objects.equals(getBody(), status.getBody())) { 168 | return false; 169 | 170 | } else if (!Objects.equals(getStatus(), status.getStatus())) { 171 | return false; 172 | 173 | } else if (!Objects.equals(getLevel(), status.getLevel())) { 174 | return false; 175 | 176 | } else if (!Objects.equals(getCreatedOn(), status.getCreatedOn())) { 177 | return false; 178 | } 179 | 180 | return true; 181 | } 182 | 183 | @Override 184 | public String toString() { 185 | return "Status{" + 186 | "mStatus='" + mStatus + '\'' + 187 | ", mBody='" + mBody + '\'' + 188 | ", mCreatedOn='" + mCreatedOn + '\'' + 189 | '}'; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/push/BackoffHandler.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.push; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | import java.util.Random; 7 | 8 | public abstract class BackoffHandler implements Runnable { 9 | 10 | private static final Handler sMainHandler = new Handler(Looper.getMainLooper()); 11 | private static final Random sRandom = new Random(); 12 | 13 | private final int mMaxTries; 14 | private final boolean mAsync; 15 | private boolean mCancel; 16 | 17 | public BackoffHandler(final int maxTries) { 18 | this(maxTries, false); 19 | } 20 | 21 | public BackoffHandler(final int maxTries, final boolean async) { 22 | mMaxTries = maxTries; 23 | mAsync = async; 24 | } 25 | 26 | public boolean isAsync() { 27 | return mAsync; 28 | } 29 | 30 | public void start() { 31 | if (mAsync) { 32 | new Thread(this).start(); 33 | } else { 34 | run(); 35 | } 36 | } 37 | 38 | public void cancel() { 39 | mCancel = true; 40 | } 41 | 42 | @Override 43 | public void run() { 44 | 45 | long maxDelay = 500L; 46 | int attempt = 0; 47 | 48 | for (;;) { 49 | boolean exit = false; 50 | try { 51 | exit = performAction(); 52 | } catch (final Throwable ignored) { 53 | } 54 | 55 | if (exit) { 56 | // Successful 57 | break; 58 | } 59 | 60 | if (++attempt == mMaxTries) { 61 | // Unsuccessful 62 | break; 63 | } 64 | 65 | try { 66 | final long delay = sRandom.nextLong() % maxDelay; 67 | Thread.sleep(delay); 68 | } catch (final InterruptedException e) { 69 | throw new RuntimeException("This thread cannot be waited on!", e); 70 | } 71 | 72 | if (mCancel || Thread.currentThread().isInterrupted()) { 73 | // Action cancelled 74 | break; 75 | } 76 | 77 | maxDelay *= 2; 78 | } 79 | 80 | final int totalAttempts = attempt; 81 | sMainHandler.post(new Runnable() { 82 | @Override 83 | public void run() { 84 | onActionCompleted(!mCancel && totalAttempts != mMaxTries); 85 | } 86 | }); 87 | } 88 | 89 | /** 90 | * @return true if this action completed successfully 91 | */ 92 | public abstract boolean performAction() throws Throwable; 93 | 94 | /** 95 | * @param success true if this action completed successfully 96 | */ 97 | public abstract void onActionCompleted(final boolean success); 98 | } 99 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/push/OnPushMessageReceivedListener.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.push; 2 | 3 | import android.content.Intent; 4 | 5 | public interface OnPushMessageReceivedListener { 6 | public void onPushMessageReceived(final Intent intent); 7 | } 8 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/push/PushBaseActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.deange.githubstatus.push; 17 | 18 | import android.app.Dialog; 19 | import android.content.DialogInterface; 20 | import android.os.Bundle; 21 | import android.support.v4.app.FragmentActivity; 22 | import android.support.v7.app.ActionBarActivity; 23 | import android.util.Log; 24 | 25 | import com.deange.githubstatus.ui.TrackedActivity; 26 | import com.google.android.gms.common.ConnectionResult; 27 | import com.google.android.gms.common.GooglePlayServicesUtil; 28 | 29 | public abstract class PushBaseActivity 30 | extends TrackedActivity 31 | implements OnPushMessageReceivedListener { 32 | 33 | private static final String TAG = PushBaseActivity.class.getSimpleName(); 34 | 35 | private final PushMessageReceiver mHandleGcmMessageReceiver = new PushMessageReceiver(this); 36 | private boolean mNeedToCheckPlayServices = true; 37 | 38 | @Override 39 | protected void onCreate(final Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | 42 | // Listener for when a GCM message is received 43 | PushUtils.listenForGcmMessages(this, mHandleGcmMessageReceiver); 44 | } 45 | 46 | protected void registerIfNecessary() { 47 | final String regId = PushRegistrar.getRegistrationId(this); 48 | if (!PushRegistrar.isRegistered(this)) { 49 | PushRegistrar.register(this); 50 | 51 | } else if (!PushServerRegistrar.isRegisteredOnServer(this)) { 52 | PushServerRegistrar.register(PushBaseActivity.this, regId, true); 53 | } 54 | } 55 | 56 | protected void unregisterIfNecessary() { 57 | final String regId = PushRegistrar.getRegistrationId(this); 58 | if (!regId.isEmpty()) { 59 | // Device is already registered on GCM, check server. 60 | if (PushServerRegistrar.isRegisteredOnServer(this)) { 61 | // Device is registered on server, unregister them 62 | PushRegistrar.unregister(this); 63 | } 64 | } 65 | } 66 | 67 | protected boolean checkPlayServices() { 68 | return !mNeedToCheckPlayServices || performPlayServicesCheck(); 69 | } 70 | 71 | private boolean performPlayServicesCheck() { 72 | 73 | mNeedToCheckPlayServices = false; 74 | final int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); 75 | if (resultCode == ConnectionResult.SUCCESS) { 76 | return true; 77 | 78 | } else { 79 | Log.d(TAG, "isGooglePlayServicesAvailable = " + resultCode); 80 | 81 | if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) { 82 | final Dialog dialog = GooglePlayServicesUtil.getErrorDialog(resultCode, this, 0); 83 | dialog.setCancelable(false); 84 | dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { 85 | @Override 86 | public void onDismiss(DialogInterface dialog) { 87 | mNeedToCheckPlayServices = true; 88 | } 89 | }); 90 | dialog.show(); 91 | 92 | } else { 93 | Log.e(TAG, "Unrecoverable error checking Google Play Services."); 94 | finish(); 95 | } 96 | 97 | return false; 98 | } 99 | } 100 | 101 | @Override 102 | protected void onDestroy() { 103 | PushUtils.unregisterForGcmMessages(this, mHandleGcmMessageReceiver); 104 | super.onDestroy(); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/push/PushBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.push; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.support.v4.content.WakefulBroadcastReceiver; 7 | import android.util.Log; 8 | 9 | public class PushBroadcastReceiver extends WakefulBroadcastReceiver { 10 | 11 | private static final String TAG = PushBroadcastReceiver.class.getSimpleName(); 12 | 13 | @Override 14 | public final void onReceive(final Context context, final Intent intent) { 15 | Log.v(TAG, "onReceive(): " + intent.getAction()); 16 | 17 | PushIntentService.runIntentInService(context, intent, PushIntentService.class.getName()); 18 | setResult(Activity.RESULT_OK, null, null); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/push/PushConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.deange.githubstatus.push; 18 | 19 | /** 20 | * Constants used by the GCM library. 21 | */ 22 | public final class PushConstants { 23 | 24 | private static final String C2DM_PACKAGE = "com.google.android.c2dm"; 25 | 26 | public static final String INTENT_REGISTRATION_CALLBACK = build("REGISTRATION"); 27 | public static final String INTENT_MESSAGE = build("RECEIVE"); 28 | 29 | public static final String EXTRA_UNREGISTERED = "unregistered"; 30 | public static final String EXTRA_ERROR = "error"; 31 | public static final String EXTRA_REGISTRATION_ID = "registration_id"; 32 | public static final String EXTRA_SPECIAL_MESSAGE = "message_type"; 33 | 34 | private static String build(final String name) { 35 | return C2DM_PACKAGE + "." + "intent" + "." + name; 36 | } 37 | 38 | private PushConstants() { 39 | throw new UnsupportedOperationException(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/push/PushIntentService.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.push; 2 | 3 | import android.app.IntentService; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.PowerManager; 7 | import android.util.Log; 8 | 9 | import com.deange.githubstatus.Utils; 10 | import com.deange.githubstatus.controller.NotificationController; 11 | import com.deange.githubstatus.controller.StateController; 12 | import com.deange.githubstatus.http.GithubApi; 13 | import com.deange.githubstatus.model.Status; 14 | import com.deange.githubstatus.ui.TrackedActivity; 15 | 16 | import java.io.IOException; 17 | 18 | public class PushIntentService extends IntentService { 19 | 20 | private static final String TAG = PushIntentService.class.getSimpleName(); 21 | 22 | private static final String WAKELOCK_KEY = Utils.buildAction("wakelock.key"); 23 | private static final Object sLock = new Object(); 24 | 25 | private static PowerManager.WakeLock sWakeLock; 26 | 27 | public PushIntentService() { 28 | super(TAG); 29 | } 30 | 31 | @Override 32 | protected void onHandleIntent(final Intent intent) { 33 | 34 | try { 35 | final Context context = getApplicationContext(); 36 | final String action = intent.getAction(); 37 | 38 | if (action == null) { 39 | Log.v(TAG, "Empty action from intent: \'" + intent + "\'"); 40 | 41 | } else if (action.equals(PushConstants.INTENT_REGISTRATION_CALLBACK)) { 42 | handleRegistration(context, intent); 43 | 44 | } else if (action.equals(PushConstants.INTENT_MESSAGE)) { 45 | final String type = intent.getStringExtra(PushConstants.EXTRA_SPECIAL_MESSAGE); 46 | onMessage(context, intent, type); 47 | } 48 | 49 | } finally { 50 | synchronized (sLock) { 51 | if (sWakeLock != null && sWakeLock.isHeld()) { 52 | Log.v(TAG, "Releasing wakelock"); 53 | sWakeLock.release(); 54 | } 55 | } 56 | } 57 | } 58 | 59 | static void runIntentInService(final Context context, final Intent intent, 60 | final String className) { 61 | synchronized (sLock) { 62 | if (sWakeLock == null) { 63 | sWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)) 64 | .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY); 65 | } 66 | Log.v(TAG, "Acquiring wakelock"); 67 | sWakeLock.acquire(); 68 | } 69 | 70 | intent.setClassName(context, className); 71 | context.startService(intent); 72 | } 73 | 74 | private void handleRegistration(final Context context, Intent intent) { 75 | final String error = intent.getStringExtra(PushConstants.EXTRA_ERROR); 76 | final String registrationId = intent.getStringExtra(PushConstants.EXTRA_REGISTRATION_ID); 77 | final String unregistered = intent.getStringExtra(PushConstants.EXTRA_UNREGISTERED); 78 | Log.v(TAG, "handleRegistration: registrationId = " + registrationId + 79 | ", error = " + error + ", unregistered = " + unregistered); 80 | 81 | // registration succeeded 82 | if (registrationId != null) { 83 | PushRegistrar.setRegistrationId(context, registrationId); 84 | onRegistered(context, registrationId); 85 | return; 86 | } 87 | 88 | // unregistration succeeded 89 | if (unregistered != null) { 90 | final String oldRegistrationId = PushRegistrar.clearRegistrationId(context); 91 | onUnregistered(context, oldRegistrationId); 92 | return; 93 | } 94 | 95 | // last operation (registration or unregistration) returned an error; 96 | Log.v(TAG, "Registration error: " + error); 97 | 98 | // Registration failed 99 | onError(context, error); 100 | 101 | } 102 | 103 | public void onRegistered(final Context context, final String registrationId) { 104 | 105 | } 106 | 107 | public void onUnregistered(final Context context, final String oldRegistrationId) { 108 | 109 | } 110 | 111 | public void onMessage(final Context context, final Intent intent, final String type) { 112 | 113 | final Status newStatus; 114 | try { 115 | newStatus = GithubApi.getStatus(context); 116 | 117 | } catch (final IOException e) { 118 | // Cannot retrieve new status information 119 | return; 120 | } 121 | 122 | final Status oldStatus = StateController.getInstance().getStatus(); 123 | final boolean alert = Status.shouldAlert(oldStatus, newStatus); 124 | 125 | // Save the new status 126 | StateController.getInstance().setStatus(newStatus); 127 | 128 | if (alert) { 129 | if (TrackedActivity.getVisibleActivities() == 0) { 130 | // Pop a notification that the status has now changed! 131 | NotificationController.getInstance().notificationForStatus(newStatus); 132 | } 133 | } 134 | 135 | // Let other active listeners know that we received a message 136 | final Intent broadcast = new Intent(); 137 | broadcast.setAction(PushUtils.ACTION_GCM_MESSAGE_RECEIVED); 138 | context.sendBroadcast(broadcast); 139 | } 140 | 141 | public void onError(final Context context, final String error) { 142 | 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/push/PushMessageReceiver.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.push; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.util.Log; 7 | 8 | public class PushMessageReceiver 9 | extends BroadcastReceiver { 10 | 11 | private static final String TAG = PushMessageReceiver.class.getSimpleName(); 12 | 13 | private final OnPushMessageReceivedListener mListener; 14 | private final String mAction; 15 | 16 | public PushMessageReceiver(final OnPushMessageReceivedListener listener) { 17 | mListener = listener; 18 | mAction = PushUtils.ACTION_GCM_MESSAGE_RECEIVED; 19 | } 20 | 21 | public String getAction() { 22 | return mAction; 23 | } 24 | 25 | @Override 26 | public void onReceive(final Context context, final Intent intent) { 27 | 28 | if (!mAction.equals(intent.getAction())) { 29 | return; 30 | } 31 | 32 | if (mListener == null) { 33 | Log.v(TAG, "mListener is null!"); 34 | 35 | } else { 36 | mListener.onPushMessageReceived(intent); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/push/PushRegistrar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.deange.githubstatus.push; 18 | 19 | import android.content.Context; 20 | import android.content.SharedPreferences; 21 | import android.content.SharedPreferences.Editor; 22 | import android.text.TextUtils; 23 | import android.util.Log; 24 | 25 | import com.deange.githubstatus.BuildConfig; 26 | import com.deange.githubstatus.Utils; 27 | import com.google.android.gms.gcm.GoogleCloudMessaging; 28 | 29 | /** 30 | * Utilities for device registration. 31 | *

32 | * Note: this class uses a private {@link SharedPreferences} 33 | * object to keep track of the registration token. 34 | */ 35 | public final class PushRegistrar { 36 | 37 | private static final String TAG = PushRegistrar.class.getSimpleName(); 38 | 39 | private static final int DEFAULT_MAX_ATTEMPTS = 10; 40 | private static final String PREFERENCES = Utils.buildPreferences("registrar"); 41 | private static final String PROPERTY_REG_ID = "regId"; 42 | private static final String PROPERTY_APP_VERSION = "appVersion"; 43 | 44 | private static PushRegisterTask sRegisterTask; 45 | private static PushUnregisterTask sUnregisterTask; 46 | 47 | public static void register(final Context context) { 48 | if (!isRegistered(context)) { 49 | Log.v(TAG, "Registering app " + context.getPackageName()); 50 | 51 | if (sRegisterTask != null) { 52 | sRegisterTask.cancel(); 53 | } 54 | 55 | sRegisterTask = new PushRegisterTask(context, DEFAULT_MAX_ATTEMPTS); 56 | sRegisterTask.start(); 57 | } 58 | } 59 | 60 | public static void unregister(final Context context) { 61 | if (isRegistered(context)) { 62 | Log.v(TAG, "Unregistering app " + context.getPackageName()); 63 | 64 | if (sUnregisterTask != null) { 65 | sUnregisterTask.cancel(); 66 | } 67 | 68 | sUnregisterTask = new PushUnregisterTask(context, DEFAULT_MAX_ATTEMPTS); 69 | sUnregisterTask.start(); 70 | } 71 | } 72 | 73 | public static String getRegistrationId(final Context context) { 74 | final SharedPreferences prefs = getGCMPreferences(context); 75 | String registrationId = prefs.getString(PROPERTY_REG_ID, ""); 76 | 77 | // check if app was updated; if so, it must clear registration id to 78 | // avoid a race condition if GCM sends a message 79 | final int oldVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE); 80 | final int newVersion = BuildConfig.VERSION_CODE; 81 | if (oldVersion != Integer.MIN_VALUE && oldVersion != newVersion) { 82 | Log.v(TAG, "App version changed from " + oldVersion + " to " + newVersion 83 | + "; resetting registration id"); 84 | clearRegistrationId(context); 85 | registrationId = ""; 86 | } 87 | 88 | return registrationId; 89 | } 90 | 91 | public static boolean isRegistered(final Context context) { 92 | return !TextUtils.isEmpty(getRegistrationId(context)); 93 | } 94 | 95 | static String clearRegistrationId(final Context context) { 96 | return setRegistrationId(context, ""); 97 | } 98 | 99 | static String setRegistrationId(final Context context, final String regId) { 100 | final SharedPreferences prefs = getGCMPreferences(context); 101 | final String oldRegistrationId = prefs.getString(PROPERTY_REG_ID, ""); 102 | final int appVersion = BuildConfig.VERSION_CODE; 103 | 104 | Log.v(TAG, "Saving regId on app version " + appVersion); 105 | 106 | final Editor editor = prefs.edit(); 107 | editor.putString(PROPERTY_REG_ID, regId); 108 | editor.putInt(PROPERTY_APP_VERSION, appVersion); 109 | editor.apply(); 110 | 111 | return oldRegistrationId; 112 | } 113 | 114 | private static SharedPreferences getGCMPreferences(final Context context) { 115 | return context.getApplicationContext() 116 | .getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); 117 | } 118 | 119 | private PushRegistrar() { 120 | throw new UnsupportedOperationException(); 121 | } 122 | 123 | public static class PushRegisterTask extends BackoffHandler { 124 | 125 | private final Context mContext; 126 | private final GoogleCloudMessaging mGcm; 127 | private String mRegistrationId = null; 128 | 129 | public PushRegisterTask(final Context context, final int maxTries) { 130 | super(maxTries, true); 131 | mContext = context.getApplicationContext(); 132 | mGcm = GoogleCloudMessaging.getInstance(context); 133 | } 134 | 135 | @Override 136 | public boolean performAction() throws Throwable { 137 | mRegistrationId = mGcm.register(BuildConfig.SENDER_ID); 138 | PushServerRegistrar.register(mContext, mRegistrationId); 139 | return !TextUtils.isEmpty(mRegistrationId); 140 | } 141 | 142 | @Override 143 | public void onActionCompleted(final boolean success) { 144 | if (success) { 145 | setRegistrationId(mContext, mRegistrationId); 146 | } 147 | } 148 | } 149 | 150 | public static class PushUnregisterTask extends BackoffHandler { 151 | 152 | private final Context mContext; 153 | private final GoogleCloudMessaging mGcm; 154 | 155 | public PushUnregisterTask(final Context context, final int maxTries) { 156 | super(maxTries, true); 157 | mContext = context.getApplicationContext(); 158 | mGcm = GoogleCloudMessaging.getInstance(context); 159 | } 160 | 161 | @Override 162 | public boolean performAction() throws Throwable { 163 | mGcm.unregister(); 164 | PushServerRegistrar.unregister(mContext, getRegistrationId(mContext)); 165 | return true; 166 | } 167 | 168 | @Override 169 | public void onActionCompleted(final boolean success) { 170 | if (success) { 171 | clearRegistrationId(mContext); 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/push/PushServerRegistrar.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.deange.githubstatus.push; 17 | 18 | import android.content.Context; 19 | import android.content.SharedPreferences; 20 | import android.util.Log; 21 | 22 | import com.deange.githubstatus.BuildConfig; 23 | import com.deange.githubstatus.Utils; 24 | import com.deange.githubstatus.http.HttpIOException; 25 | 26 | import java.io.IOException; 27 | import java.io.OutputStream; 28 | import java.net.HttpURLConnection; 29 | import java.net.MalformedURLException; 30 | import java.net.URL; 31 | import java.sql.Timestamp; 32 | import java.util.HashMap; 33 | import java.util.Iterator; 34 | import java.util.Map; 35 | import java.util.Map.Entry; 36 | import java.util.concurrent.TimeUnit; 37 | 38 | public final class PushServerRegistrar { 39 | 40 | private static final String TAG = PushServerRegistrar.class.getSimpleName(); 41 | /** 42 | * Default lifespan (7 days) of the {@link PushServerRegistrar#isRegisteredOnServer(Context)} 43 | * flag until it is considered expired. 44 | */ 45 | public static final long DEFAULT_ON_SERVER_LIFESPAN_MS = TimeUnit.DAYS.toMillis(7); 46 | 47 | private static final String PREFERENCES = Utils.buildPreferences("server.registrar"); 48 | private static final String PROPERTY_ON_SERVER = "onServer"; 49 | private static final String PROPERTY_ON_SERVER_EXPIRATION_TIME = "onServerExpirationTime"; 50 | 51 | private static final String RELEASE_SERVER_URL = "http://githubstatus.appspot.com"; 52 | private static final String SERVER_URL = BuildConfig.SERVER_URL; 53 | private static final int MAX_ATTEMPTS = 5; 54 | 55 | static void register(final Context context, final String regId) { 56 | register(context, regId, false); 57 | } 58 | 59 | static void register(final Context context, final String regId, final boolean async) { 60 | Log.i(TAG, "registering device (regId = " + regId + ")"); 61 | 62 | final String serverUrl = SERVER_URL + "/register"; 63 | final Map params = new HashMap<>(); 64 | params.put("id", regId); 65 | 66 | Log.v(TAG, "Registering at '" + serverUrl + "'"); 67 | 68 | final BackoffHandler task = new BackoffHandler(MAX_ATTEMPTS, async) { 69 | @Override 70 | public boolean performAction() throws Throwable { 71 | post(serverUrl, params); 72 | return true; 73 | } 74 | 75 | @Override 76 | public void onActionCompleted(final boolean success) { 77 | setRegisteredOnServer(context, success); 78 | } 79 | }; 80 | 81 | task.start(); 82 | } 83 | 84 | static void unregister(final Context context, final String regId) { 85 | Log.i(TAG, "unregistering device (regId = " + regId + ")"); 86 | 87 | final String serverUrl = SERVER_URL + "/unregister"; 88 | final Map params = new HashMap<>(); 89 | params.put("id", regId); 90 | 91 | try { 92 | post(serverUrl, params); 93 | 94 | } catch (final IOException e) { 95 | // At this point the device is unregistered from GCM, but still 96 | // registered in the server. 97 | // We could try to unregister again, but it is not necessary: 98 | // if the server tries to send a message to the device, it will get 99 | // a "NotRegistered" error message and should unregister the device. 100 | } finally { 101 | setRegisteredOnServer(context, false); 102 | } 103 | } 104 | 105 | private static void post(final String endpoint, final Map params) 106 | throws IOException { 107 | 108 | final URL url; 109 | try { 110 | url = new URL(endpoint); 111 | 112 | } catch (final MalformedURLException e) { 113 | throw new IllegalArgumentException("invalid url: " + endpoint); 114 | } 115 | 116 | final StringBuilder bodyBuilder = new StringBuilder(); 117 | final Iterator> iterator = params.entrySet().iterator(); 118 | 119 | // constructs the POST body using the parameters 120 | while (iterator.hasNext()) { 121 | final Entry param = iterator.next(); 122 | bodyBuilder.append(param.getKey()).append('=').append(param.getValue()); 123 | if (iterator.hasNext()) { 124 | bodyBuilder.append('&'); 125 | } 126 | } 127 | 128 | final String body = bodyBuilder.toString(); 129 | final byte[] bytes = body.getBytes(); 130 | Log.v(TAG, "Posting '" + body + "' to " + url); 131 | HttpURLConnection conn = null; 132 | 133 | try { 134 | conn = (HttpURLConnection) url.openConnection(); 135 | conn.setDoOutput(true); 136 | conn.setUseCaches(false); 137 | conn.setFixedLengthStreamingMode(bytes.length); 138 | conn.setRequestMethod("POST"); 139 | conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); 140 | 141 | // post the request 142 | final OutputStream out = conn.getOutputStream(); 143 | out.write(bytes); 144 | out.close(); 145 | 146 | // handle the response 147 | final int status = conn.getResponseCode(); 148 | if (status < 200 || status > 299) { 149 | throw new HttpIOException("Post failed with error code " + status, status); 150 | } 151 | 152 | } finally { 153 | if (conn != null) { 154 | conn.disconnect(); 155 | } 156 | } 157 | 158 | } 159 | 160 | public static boolean isRegisteredOnServer(final Context context) { 161 | final SharedPreferences prefs = getGCMPreferences(context); 162 | final boolean isRegistered = prefs.getBoolean(PROPERTY_ON_SERVER, false); 163 | Log.v(TAG, "Is registered on server: " + isRegistered); 164 | 165 | if (isRegistered) { 166 | // checks if the information is not stale 167 | final long expirationTime = prefs.getLong(PROPERTY_ON_SERVER_EXPIRATION_TIME, -1); 168 | if (System.currentTimeMillis() > expirationTime) { 169 | Log.v(TAG, "flag expired on: " + new Timestamp(expirationTime)); 170 | return false; 171 | } 172 | } 173 | 174 | return isRegistered; 175 | } 176 | 177 | public static void setRegisteredOnServer(final Context context, final boolean flag) { 178 | final SharedPreferences prefs = getGCMPreferences(context); 179 | final long lifespan = DEFAULT_ON_SERVER_LIFESPAN_MS; 180 | final long expirationTime = System.currentTimeMillis() + lifespan; 181 | Log.v(TAG, "Setting registeredOnServer status as " + flag + " until " 182 | + new Timestamp(expirationTime)); 183 | 184 | final SharedPreferences.Editor editor = prefs.edit(); 185 | editor.putBoolean(PROPERTY_ON_SERVER, flag); 186 | editor.putLong(PROPERTY_ON_SERVER_EXPIRATION_TIME, expirationTime); 187 | editor.apply(); 188 | } 189 | 190 | private static SharedPreferences getGCMPreferences(final Context context) { 191 | return context.getApplicationContext() 192 | .getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/push/PushUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.deange.githubstatus.push; 17 | 18 | import android.content.Context; 19 | import android.content.IntentFilter; 20 | 21 | import com.deange.githubstatus.Utils; 22 | 23 | public final class PushUtils { 24 | 25 | public static final String TAG = PushUtils.class.getSimpleName(); 26 | static final String ACTION_GCM_MESSAGE_RECEIVED = Utils.buildAction("GCM_MESSAGE_RECEIVED"); 27 | 28 | public static void listenForGcmMessages( 29 | final Context context, 30 | final PushMessageReceiver receiver) { 31 | context.registerReceiver(receiver, new IntentFilter(receiver.getAction())); 32 | } 33 | 34 | public static void unregisterForGcmMessages( 35 | final Context context, 36 | final PushMessageReceiver receiver) { 37 | context.unregisterReceiver(receiver); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.ui; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.AlertDialog; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | import android.support.v4.app.FragmentTransaction; 10 | import android.support.v7.widget.Toolbar; 11 | import android.view.Menu; 12 | import android.view.MenuItem; 13 | import android.view.View; 14 | import android.widget.PopupMenu; 15 | import android.widget.TextView; 16 | 17 | import com.deange.githubstatus.PlatformUtils; 18 | import com.deange.githubstatus.R; 19 | import com.deange.githubstatus.Utils; 20 | import com.deange.githubstatus.model.SettingsInfo; 21 | import com.deange.githubstatus.push.PushBaseActivity; 22 | import com.melnykov.fab.FloatingActionButton; 23 | 24 | import java.util.Calendar; 25 | 26 | public class MainActivity 27 | extends PushBaseActivity 28 | implements 29 | View.OnClickListener, 30 | SettingsFragment.OnSettingsChangedListener, 31 | PopupMenu.OnMenuItemClickListener { 32 | 33 | private MainFragment mFragment; 34 | 35 | private static final String AVATAR_URL = "https://plus.google.com/+ChristianDeAngelis"; 36 | 37 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | 41 | if (Utils.showNiceView(this) && PlatformUtils.hasJellybean()) { 42 | getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); 43 | } 44 | 45 | super.onCreate(savedInstanceState); 46 | setContentView(R.layout.activity_main); 47 | 48 | setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); 49 | getSupportActionBar().setTitle(R.string.app_name); 50 | 51 | mFragment = (MainFragment) getSupportFragmentManager().findFragmentByTag(MainFragment.TAG); 52 | if (mFragment == null) { 53 | mFragment = MainFragment.newInstance(); 54 | } 55 | 56 | if (!mFragment.isAdded()) { 57 | getSupportFragmentManager().beginTransaction().add( 58 | R.id.content_frame, mFragment, MainFragment.TAG).commit(); 59 | } 60 | 61 | final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.content_fab); 62 | if (fab != null) { 63 | fab.setOnClickListener(this); 64 | } 65 | } 66 | 67 | private void showInfoDialog() { 68 | 69 | final String developerName = getString( 70 | R.string.about_developer_name, 71 | Calendar.getInstance().get(Calendar.YEAR)); 72 | 73 | final View dialogContentView = getLayoutInflater().inflate(R.layout.dialog_about, null); 74 | ((TextView) dialogContentView.findViewById(R.id.dialog_about_developer_name)) 75 | .setText(developerName); 76 | dialogContentView.findViewById(R.id.dialog_about_avatar).setOnClickListener(this); 77 | 78 | new AlertDialog.Builder(this) 79 | .setView(dialogContentView) 80 | .show(); 81 | } 82 | 83 | private void showOverflowMenu(final FloatingActionButton view) { 84 | final PopupMenu menu = new PopupMenu(this, view); 85 | menu.setOnMenuItemClickListener(this); 86 | menu.inflate(R.menu.activity_menu); 87 | menu.show(); 88 | } 89 | 90 | private void showSettings() { 91 | 92 | if (!checkPlayServices()) { 93 | return; 94 | } 95 | 96 | final String tag = SettingsFragment.TAG; 97 | final FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); 98 | SettingsFragment fragment = 99 | (SettingsFragment) getSupportFragmentManager().findFragmentByTag(tag); 100 | if (fragment != null) { 101 | transaction.remove(fragment); 102 | } 103 | 104 | fragment = new SettingsFragment(); 105 | fragment.show(transaction, tag); 106 | } 107 | 108 | @Override 109 | public boolean onCreateOptionsMenu(Menu menu) { 110 | getMenuInflater().inflate(R.menu.activity_menu, menu); 111 | return super.onCreateOptionsMenu(menu); 112 | } 113 | 114 | @Override 115 | public boolean onOptionsItemSelected(MenuItem item) { 116 | 117 | switch (item.getItemId()) { 118 | case R.id.menu_sync: 119 | refresh(); 120 | return true; 121 | 122 | case R.id.menu_settings: 123 | showSettings(); 124 | return true; 125 | 126 | case R.id.menu_info: 127 | showInfoDialog(); 128 | return true; 129 | 130 | default: 131 | return super.onOptionsItemSelected(item); 132 | 133 | } 134 | } 135 | 136 | @Override 137 | public boolean onMenuItemClick(final MenuItem item) { 138 | // This is from the PopupMenu 139 | return onOptionsItemSelected(item); 140 | } 141 | 142 | private void refresh() { 143 | if (mFragment != null) { 144 | mFragment.refresh(); 145 | } 146 | } 147 | 148 | @Override 149 | public void onPushMessageReceived(final Intent intent) { 150 | // Reload the fragment's content view 151 | refresh(); 152 | } 153 | 154 | @Override 155 | public void onClick(final View v) { 156 | 157 | switch (v.getId()) { 158 | case R.id.content_fab: 159 | showOverflowMenu((FloatingActionButton) v); 160 | break; 161 | 162 | case R.id.dialog_about_avatar: 163 | startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(AVATAR_URL))); 164 | break; 165 | } 166 | } 167 | 168 | @Override 169 | public void onSettingsChanged(final SettingsInfo settings) { 170 | if (settings.isGCMEnabled()) { 171 | // User is enabling push notifications 172 | registerIfNecessary(); 173 | 174 | } else { 175 | // User is disabling push notifications 176 | unregisterIfNecessary(); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/ui/MainFragment.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.ui; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ArgbEvaluator; 6 | import android.animation.ObjectAnimator; 7 | import android.animation.ValueAnimator; 8 | import android.content.res.Configuration; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.drawable.Drawable; 11 | import android.os.Bundle; 12 | import android.os.Handler; 13 | import android.support.v4.app.Fragment; 14 | import android.support.v4.widget.SwipeRefreshLayout; 15 | import android.util.Log; 16 | import android.view.LayoutInflater; 17 | import android.view.View; 18 | import android.view.ViewGroup; 19 | import android.widget.AbsListView; 20 | import android.widget.ListView; 21 | import android.widget.TextView; 22 | import android.widget.ViewSwitcher; 23 | 24 | import com.deange.githubstatus.R; 25 | import com.deange.githubstatus.Utils; 26 | import com.deange.githubstatus.controller.GsonController; 27 | import com.deange.githubstatus.http.GithubApi; 28 | import com.deange.githubstatus.http.HttpTask; 29 | import com.deange.githubstatus.model.Status; 30 | import com.deange.githubstatus.ui.view.SliceView; 31 | 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | import java.util.concurrent.atomic.AtomicInteger; 35 | 36 | public class MainFragment 37 | extends Fragment 38 | implements 39 | SwipeRefreshLayout.OnRefreshListener, 40 | AbsListView.OnScrollListener, 41 | ViewSwitcher.ViewFactory { 42 | 43 | public static final String TAG = MainFragment.class.getSimpleName(); 44 | 45 | private static final String KEY_STATUS = TAG + ".status"; 46 | private static final long MINIMUM_UPDATE_DURATION = 1000; 47 | private static final int TOTAL_COMPONENTS = 2; 48 | 49 | private View mView; 50 | private SwipeRefreshLayout mSwipeLayout; 51 | private SliceView mSliceView; 52 | private TextView mLoadingView; 53 | private TextView mNothingView; 54 | private ListView mListView; 55 | 56 | private ViewSwitcher mStatusView; 57 | private Status mStatus; 58 | 59 | private List mMessages; 60 | 61 | private MessagesAdapter mAdapter; 62 | private final AtomicInteger mComponentsLoaded = new AtomicInteger(); 63 | private final Handler mHandler = new Handler(); 64 | private long mLastUpdate = 0; 65 | private ValueAnimator mAnimator; 66 | private int mColour; 67 | 68 | public static MainFragment newInstance() { 69 | return new MainFragment(); 70 | } 71 | 72 | public MainFragment() { 73 | super(); 74 | } 75 | 76 | @Override 77 | public void onCreate(final Bundle savedInstanceState) { 78 | Log.v(TAG, "onCreate()"); 79 | super.onCreate(savedInstanceState); 80 | 81 | mAdapter = new MessagesAdapter(getActivity(), R.layout.list_item_message); 82 | 83 | if (savedInstanceState != null) { 84 | mStatus = GsonController.getInstance().fromJson( 85 | savedInstanceState.getString(KEY_STATUS, null), Status.class); 86 | } 87 | if (mStatus == null) { 88 | mStatus = Status.getSpecialStatus(getActivity(), Status.SpecialType.LOADING); 89 | } 90 | } 91 | 92 | @Override 93 | public View onCreateView( 94 | final LayoutInflater inflater, 95 | final ViewGroup container, 96 | final Bundle savedInstanceState) { 97 | Log.v(TAG, "onCreateView()"); 98 | 99 | mView = inflater.inflate(R.layout.fragment_main, null); 100 | 101 | mStatusView = (ViewSwitcher) mView.findViewById(R.id.fragment_status_text_flipper); 102 | mStatusView.setFactory(this); 103 | mLoadingView = (TextView) mView.findViewById(R.id.loading_messages_view); 104 | mNothingView = (TextView) mView.findViewById(R.id.no_messages_view); 105 | 106 | mSwipeLayout = (SwipeRefreshLayout) mView.findViewById(R.id.fragment_swipe_container); 107 | mSwipeLayout.setOnRefreshListener(this); 108 | mSwipeLayout.setColorSchemeResources(R.color.status_good); 109 | 110 | mSliceView = (SliceView) mView.findViewById(R.id.fragment_slice_view); 111 | 112 | mListView = (ListView) mView.findViewById(R.id.fragment_messages_list_view); 113 | mListView.setDivider(null); 114 | mListView.setAdapter(mAdapter); 115 | mListView.setOnScrollListener(this); 116 | 117 | updateVisibility(); 118 | 119 | return mView; 120 | } 121 | 122 | @Override 123 | public void onViewCreated(final View view, final Bundle savedInstanceState) { 124 | Log.v(TAG, "onViewCreated()"); 125 | super.onViewCreated(view, savedInstanceState); 126 | 127 | if (mSliceView != null) { 128 | final double angle = Math.toDegrees(Math.atan2( 129 | mSliceView.getSliceHeight(), 130 | getResources().getDisplayMetrics().widthPixels)); 131 | mStatusView.setRotation((float) angle); 132 | 133 | mStatusView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 134 | @Override 135 | public void onLayoutChange( 136 | final View v, 137 | final int left, final int top, 138 | final int right, final int bottom, 139 | final int oldLeft, final int oldTop, 140 | final int oldRight, final int oldBottom) { 141 | 142 | mStatusView.setPivotX(0); 143 | mStatusView.setPivotY(mStatusView.getMeasuredHeight()); 144 | } 145 | }); 146 | } 147 | } 148 | 149 | @Override 150 | public void onActivityCreated(final Bundle savedInstanceState) { 151 | Log.v(TAG, "onActivityCreated()"); 152 | super.onActivityCreated(savedInstanceState); 153 | 154 | // Store this result ahead of time since the status and messages references 155 | // may point to intermediary (ie: pending) results 156 | final boolean shouldRefresh = (mStatus == null || mMessages == null); 157 | 158 | // Refresh status view 159 | mStatus = (mStatus == null) 160 | ? Status.getSpecialStatus(getActivity(), Status.SpecialType.LOADING) 161 | : mStatus; 162 | 163 | setStatus(mStatus); 164 | 165 | // Refresh messages view 166 | mMessages = (mMessages == null) ? new ArrayList() : mMessages; 167 | setMessages(mMessages); 168 | 169 | // Continue loading status info if necessary 170 | if (shouldRefresh) { 171 | refresh(); 172 | } 173 | 174 | } 175 | 176 | @Override 177 | public void onResume() { 178 | Log.v(TAG, "onResume()"); 179 | super.onResume(); 180 | } 181 | 182 | @Override 183 | public void onPause() { 184 | Log.v(TAG, "onPause()"); 185 | super.onPause(); 186 | } 187 | 188 | @Override 189 | public void onSaveInstanceState(final Bundle outState) { 190 | outState.putString(KEY_STATUS, GsonController.getInstance().toJson(mStatus)); 191 | super.onSaveInstanceState(outState); 192 | } 193 | 194 | @Override 195 | public void onRefresh() { 196 | // Called by SwipeRefreshLayout 197 | Log.v(TAG, "onRefresh()"); 198 | 199 | refresh(); 200 | } 201 | 202 | private void resetFieldsForRefresh() { 203 | mSwipeLayout.setRefreshing(true); 204 | mComponentsLoaded.set(0); 205 | mLastUpdate = System.currentTimeMillis(); 206 | } 207 | 208 | public void refresh() { 209 | 210 | resetFieldsForRefresh(); 211 | 212 | queryForStatus(); 213 | queryForMessages(); 214 | } 215 | 216 | private void queryForStatus() { 217 | 218 | GithubApi.getStatus(getActivity(), new HttpTask.Listener() { 219 | @Override 220 | public void onGet(final Status entity, final Exception exception) { 221 | 222 | mComponentsLoaded.incrementAndGet(); 223 | 224 | final Status status = (exception == null) 225 | ? entity 226 | : Status.getSpecialStatus(getActivity(), Status.SpecialType.ERROR); 227 | setStatus(status); 228 | } 229 | }); 230 | } 231 | 232 | private void queryForMessages() { 233 | 234 | GithubApi.getMessages(getActivity(), new HttpTask.Listener>() { 235 | @Override 236 | public void onGet(final List entity, final Exception exception) { 237 | 238 | mComponentsLoaded.incrementAndGet(); 239 | 240 | final List statuses = (exception == null) 241 | ? entity 242 | : new ArrayList(); 243 | setMessages(statuses); 244 | } 245 | }); 246 | } 247 | 248 | private void updateVisibility() { 249 | 250 | final boolean allDataLoaded = mComponentsLoaded.get() == TOTAL_COMPONENTS; 251 | 252 | ViewUtils.setVisibility(mLoadingView, mMessages == null); 253 | ViewUtils.setVisibility(mNothingView, (mMessages == null || mMessages.isEmpty())); 254 | 255 | if (allDataLoaded) { 256 | 257 | // We want to keep the refresh UI up for *at least* MINIMUM_UPDATE_DURATION 258 | // Otherwise it looks very choppy and overall not a pleasant look 259 | final long now = System.currentTimeMillis(); 260 | final long delay = MINIMUM_UPDATE_DURATION - (now - mLastUpdate); 261 | mLastUpdate = 0; 262 | 263 | mHandler.postDelayed(new Runnable() { 264 | @Override 265 | public void run() { 266 | mSwipeLayout.setRefreshing(false); 267 | } 268 | }, delay); 269 | } 270 | } 271 | 272 | @Override 273 | public void onScrollStateChanged(final AbsListView view, final int scrollState) { 274 | } 275 | 276 | @Override 277 | public void onScroll( 278 | final AbsListView view, 279 | final int firstVisibleItem, 280 | final int visibleItemCount, 281 | final int totalItemCount) { 282 | 283 | if (view == mListView) { 284 | int topRowVerticalPosition = (mListView == null || mListView.getChildCount() == 0) 285 | ? 0 : mListView.getChildAt(0).getTop(); 286 | mSwipeLayout.setEnabled(firstVisibleItem == 0 && topRowVerticalPosition >= 0); 287 | } 288 | } 289 | 290 | private void setStatus(final Status status) { 291 | mStatus = (status == null) 292 | ? Status.getSpecialStatus(getActivity(), Status.SpecialType.ERROR) 293 | : status; 294 | 295 | mStatusView.setDisplayedChild(mStatusView.getDisplayedChild() == 0 ? 1 : 0); 296 | updateStatusView((TextView) mStatusView.getChildAt(mStatusView.getDisplayedChild())); 297 | updateVisibility(); 298 | } 299 | 300 | private void setMessages(final List response) { 301 | mMessages = (response == null) ? new ArrayList() : response; 302 | mAdapter.refresh(mMessages); 303 | updateVisibility(); 304 | } 305 | 306 | @Override 307 | public View makeView() { 308 | final TextView view = (TextView) LayoutInflater.from(getActivity()).inflate( 309 | R.layout.view_flipper_item, (ViewGroup) getView(), false); 310 | updateStatusView(view); 311 | return view; 312 | } 313 | 314 | private void animateColorFilter() { 315 | final int startColour = mColour; 316 | final int endColour = ViewUtils.resolveStatusColour(getActivity(), mStatus); 317 | final Drawable background = mView.getBackground(); 318 | 319 | if (mAnimator != null) { 320 | mAnimator.cancel(); 321 | } 322 | 323 | mAnimator = ObjectAnimator.ofInt(startColour, endColour); 324 | mAnimator.setEvaluator(new ArgbEvaluator()); 325 | mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 326 | @Override 327 | public void onAnimationUpdate(final ValueAnimator animation) { 328 | mColour = (Integer) animation.getAnimatedValue(); 329 | background.setColorFilter(mColour, PorterDuff.Mode.SRC_ATOP); 330 | } 331 | }); 332 | 333 | mAnimator.addListener(new AnimatorListenerAdapter() { 334 | @Override 335 | public void onAnimationEnd(final Animator animation) { 336 | mAnimator = null; 337 | } 338 | }); 339 | 340 | mAnimator.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime)); 341 | mAnimator.start(); 342 | } 343 | 344 | private void updateStatusView(final TextView view) { 345 | if (Utils.showNiceView(getActivity())) { 346 | animateColorFilter(); 347 | 348 | } else { 349 | view.setTextColor(ViewUtils.resolveStatusColour(getActivity(), mStatus)); 350 | } 351 | 352 | view.setText(Status.getTranslatedStatus(getActivity(), mStatus).toUpperCase()); 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/ui/MessagesAdapter.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.ui; 2 | 3 | import android.content.Context; 4 | import android.graphics.PorterDuff; 5 | import android.graphics.drawable.Drawable; 6 | import android.text.format.Time; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ArrayAdapter; 11 | import android.widget.TextView; 12 | 13 | import com.deange.githubstatus.R; 14 | import com.deange.githubstatus.model.Status; 15 | 16 | import java.util.List; 17 | 18 | public class MessagesAdapter extends ArrayAdapter { 19 | 20 | public MessagesAdapter(Context context, int resource) { 21 | super(context, resource); 22 | } 23 | 24 | public void refresh(final List items) { 25 | clear(); 26 | 27 | if (items != null) { 28 | for (Status status : items) { 29 | if (status != null) { 30 | add(status); 31 | } 32 | } 33 | } 34 | 35 | notifyDataSetChanged(); 36 | } 37 | 38 | @Override 39 | public boolean isEnabled(int position) { 40 | return false; 41 | } 42 | 43 | @SuppressWarnings("deprecation") 44 | @Override 45 | public View getView(int position, final View convertView, ViewGroup parent) { 46 | 47 | final View view; 48 | 49 | if (convertView == null) { 50 | view = LayoutInflater.from(getContext()).inflate(R.layout.list_item_message, null); 51 | } else { 52 | view = convertView; 53 | } 54 | 55 | final Status status = getItem(position); 56 | 57 | ((TextView) view.findViewById(R.id.list_item_status)).setText(status.getBody()); 58 | 59 | final Time messageTime = status.getCreatedOn(); 60 | if (messageTime != null) { 61 | ((TextView) view.findViewById(R.id.list_item_timestamp)).setText( 62 | messageTime.format("%B %d %Y, %r")); 63 | } 64 | 65 | ViewUtils.setVisibility(view.findViewById(R.id.status_bar_top), position != 0); 66 | ViewUtils.setVisibility(view.findViewById(R.id.status_bar_bottom), position != getCount() - 1); 67 | 68 | final int statusColour = ViewUtils.resolveStatusColour(getContext(), status); 69 | 70 | final View statusIndicator = view.findViewById(R.id.status_indicator_circle); 71 | final Drawable drawable = statusIndicator.getBackground(); 72 | drawable.mutate().setColorFilter(statusColour, PorterDuff.Mode.SRC_ATOP); 73 | statusIndicator.setBackgroundDrawable(drawable); 74 | 75 | return view; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/ui/SettingsFragment.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.ui; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.app.Dialog; 6 | import android.content.DialogInterface; 7 | import android.os.Bundle; 8 | import android.support.v4.app.DialogFragment; 9 | import android.view.View; 10 | import android.widget.Switch; 11 | 12 | import com.deange.githubstatus.R; 13 | import com.deange.githubstatus.model.SettingsInfo; 14 | import com.deange.githubstatus.push.PushServerRegistrar; 15 | 16 | public class SettingsFragment extends DialogFragment { 17 | 18 | public static final String TAG = SettingsFragment.class.getSimpleName(); 19 | 20 | @Override 21 | public void onAttach(final Activity activity) { 22 | super.onAttach(activity); 23 | if (!(activity instanceof OnSettingsChangedListener)) { 24 | throw new IllegalStateException( 25 | "Activity must implement OnSettingsChangedListener!"); 26 | } 27 | } 28 | 29 | @Override 30 | public Dialog onCreateDialog(final Bundle savedInstanceState) { 31 | 32 | final Activity activity = getActivity(); 33 | final View root = View.inflate(activity, R.layout.fragment_settings, null); 34 | final Switch gcmSwitch = (Switch) root.findViewById(R.id.setting_gcm_switch); 35 | 36 | gcmSwitch.setChecked(PushServerRegistrar.isRegisteredOnServer(activity)); 37 | 38 | return new AlertDialog.Builder(activity) 39 | .setView(root) 40 | .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 41 | @Override 42 | public void onClick(final DialogInterface dialog, final int which) { 43 | ((OnSettingsChangedListener) activity).onSettingsChanged( 44 | new SettingsInfo.Builder() 45 | .gcm(gcmSwitch.isChecked()) 46 | .build() 47 | ); 48 | } 49 | }) 50 | .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 51 | @Override 52 | public void onClick(final DialogInterface dialog, final int which) { 53 | dismiss(); 54 | } 55 | }) 56 | .show(); 57 | } 58 | 59 | public interface OnSettingsChangedListener { 60 | void onSettingsChanged(final SettingsInfo settings); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/ui/TrackedActivity.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.ui; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.ActionBarActivity; 5 | 6 | public class TrackedActivity 7 | extends ActionBarActivity { 8 | 9 | private static int sActiveActivities = 0; 10 | private static int sVisibleActivities = 0; 11 | 12 | public static int getActiveActivities() { 13 | return sActiveActivities; 14 | } 15 | 16 | public static int getVisibleActivities() { 17 | return sVisibleActivities; 18 | } 19 | 20 | @Override 21 | protected void onCreate(final Bundle savedInstanceState) { 22 | sActiveActivities++; 23 | super.onCreate(savedInstanceState); 24 | } 25 | 26 | @Override 27 | protected void onResume() { 28 | sVisibleActivities++; 29 | super.onResume(); 30 | } 31 | 32 | @Override 33 | protected void onPause() { 34 | super.onPause(); 35 | sVisibleActivities--; 36 | } 37 | 38 | @Override 39 | protected void onDestroy() { 40 | super.onDestroy(); 41 | sActiveActivities--; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/ui/ViewUtils.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.ui; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.graphics.Color; 6 | import android.view.View; 7 | 8 | import com.deange.githubstatus.R; 9 | import com.deange.githubstatus.http.GithubApi; 10 | import com.deange.githubstatus.model.Status; 11 | 12 | public final class ViewUtils { 13 | 14 | public static int resolveStatusColour(final Context context, final Status status) { 15 | 16 | if (context == null) { 17 | return Color.BLACK; 18 | } 19 | 20 | if (status == null || status.getStatus() == null) { 21 | return context.getResources().getColor(R.color.status_major); 22 | } 23 | 24 | final int colourResId; 25 | final Resources res = context.getResources(); 26 | final String statusString = status.getStatus(); 27 | 28 | if (GithubApi.STATUS_GOOD.equalsIgnoreCase(statusString)) { 29 | colourResId = R.color.status_good; 30 | 31 | } else if (GithubApi.STATUS_MINOR.equalsIgnoreCase(statusString)) { 32 | colourResId = R.color.status_minor; 33 | 34 | } else if (GithubApi.STATUS_MAJOR.equalsIgnoreCase(statusString)) { 35 | colourResId = R.color.status_major; 36 | 37 | } else { 38 | colourResId = android.R.color.black; 39 | } 40 | 41 | return res.getColor(colourResId); 42 | 43 | } 44 | 45 | public static void setVisibility(final View view, final boolean visibility) { 46 | view.setVisibility(visibility ? View.VISIBLE : View.GONE); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/ui/view/AutoScaleTextView.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.ui.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.text.Layout; 6 | import android.text.StaticLayout; 7 | import android.text.TextPaint; 8 | import android.text.TextUtils; 9 | import android.util.AttributeSet; 10 | import android.util.TypedValue; 11 | 12 | import com.deange.githubstatus.R; 13 | 14 | public class AutoScaleTextView extends FontTextView { 15 | 16 | private float mScaleFactor; 17 | private float mMinTextSize; 18 | private float mMaxTextSize; 19 | 20 | private static final float DEFAULT_SCALED_FACTOR = 1f; 21 | private static final float DEFAULT_MIN_TEXT_SIZE = 10f; 22 | private static final float DEFAULT_MAX_TEXT_SIZE = 256f; 23 | private static final int DEFAULT_LINE_COUNT = 1; 24 | 25 | public AutoScaleTextView(final Context context) { 26 | this(context, null); 27 | } 28 | 29 | public AutoScaleTextView(final Context context, final AttributeSet attrs) { 30 | this(context, attrs, 0); 31 | } 32 | 33 | public AutoScaleTextView(final Context context, final AttributeSet attrs, final int defStyle) { 34 | super(context, attrs, defStyle); 35 | 36 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AutoScaleTextView, defStyle, 0); 37 | 38 | mScaleFactor = DEFAULT_SCALED_FACTOR; 39 | mMinTextSize = DEFAULT_MIN_TEXT_SIZE; 40 | mMaxTextSize = DEFAULT_MAX_TEXT_SIZE; 41 | 42 | if (a != null) { 43 | mScaleFactor = a.getFraction(R.styleable.AutoScaleTextView_scaleFactor, 1, 1, DEFAULT_SCALED_FACTOR); 44 | mMinTextSize = a.getDimension(R.styleable.AutoScaleTextView_minTextSize, DEFAULT_MIN_TEXT_SIZE); 45 | mMaxTextSize = a.getDimension(R.styleable.AutoScaleTextView_maxTextSize, DEFAULT_MAX_TEXT_SIZE); 46 | 47 | a.recycle(); 48 | } 49 | 50 | } 51 | 52 | private void rescaleText() { 53 | 54 | if (TextUtils.isEmpty(getText())) { 55 | return; 56 | } 57 | 58 | float size = mMinTextSize; 59 | 60 | final TextPaint paint = new TextPaint(getPaint()); 61 | paint.setTextSize(size); 62 | 63 | // Use modified gallop search to converge to an appropriate text size 64 | while (getLineCount(paint) <= DEFAULT_LINE_COUNT) { 65 | if (size >= mMaxTextSize) break; 66 | size *= 2; 67 | paint.setTextSize(size); 68 | } 69 | 70 | while (getLineCount(paint) > DEFAULT_LINE_COUNT) { 71 | if (size <= mMinTextSize) break; 72 | size -= 1; 73 | paint.setTextSize(size); 74 | } 75 | 76 | size *= mScaleFactor; 77 | 78 | // Renormalize 79 | size = Math.min(size, mMaxTextSize); 80 | size = Math.max(size, mMinTextSize); 81 | 82 | setTextSize(TypedValue.COMPLEX_UNIT_PX, size); 83 | } 84 | 85 | public void setScaleFactor(final float scaleFactor) { 86 | mScaleFactor = scaleFactor; 87 | } 88 | 89 | public float getScaleFactor() { 90 | return mScaleFactor; 91 | } 92 | 93 | public int getLineCount(final TextPaint paint) { 94 | final String text = getText().toString(); 95 | final int targetWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 96 | return new StaticLayout(text, paint, targetWidth, 97 | Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true).getLineCount(); 98 | } 99 | 100 | @Override 101 | protected void onLayout(final boolean changed, final int left, final int top, final int right, final int bottom) { 102 | super.onLayout(changed, left, top, right, bottom); // Does stuff for scroller and editor 103 | if (changed) { 104 | rescaleText(); 105 | } 106 | } 107 | 108 | @Override 109 | protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) { 110 | rescaleText(); 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/ui/view/FontTextView.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.ui.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Typeface; 6 | import android.text.TextUtils; 7 | import android.util.AttributeSet; 8 | import android.widget.TextView; 9 | 10 | import com.deange.githubstatus.R; 11 | 12 | public class FontTextView extends TextView { 13 | 14 | public FontTextView(final Context context) { 15 | this(context, null); 16 | } 17 | 18 | public FontTextView(final Context context, final AttributeSet attrs) { 19 | this(context, attrs, 0); 20 | } 21 | 22 | public FontTextView(final Context context, final AttributeSet attrs, final int defStyle) { 23 | super(context, attrs, defStyle); 24 | 25 | final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FontTextView, defStyle, 0); 26 | 27 | String fontName = null; 28 | if (a != null) { 29 | fontName = a.getString(R.styleable.FontTextView_fontName); 30 | a.recycle(); 31 | } 32 | 33 | if (isInEditMode()) { 34 | // Fix to view the TextFontView in resource previewer 35 | return; 36 | } 37 | 38 | if (!TextUtils.isEmpty(fontName)) { 39 | setTypeface(Typeface.createFromAsset(getContext().getAssets(), fontName)); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/ui/view/SelectableRoundedImageView.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.ui.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Rect; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | 8 | import com.makeramen.RoundedImageView; 9 | 10 | public class SelectableRoundedImageView extends RoundedImageView { 11 | 12 | private int mSelectedColour; 13 | private int mNormalColour; 14 | private final Rect mBounds = new Rect(); 15 | 16 | public SelectableRoundedImageView(final Context context) { 17 | super(context); 18 | getColours(); 19 | } 20 | 21 | public SelectableRoundedImageView(final Context context, final AttributeSet attrs) { 22 | super(context, attrs); 23 | getColours(); 24 | } 25 | 26 | public SelectableRoundedImageView(final Context context, final AttributeSet attrs, final int defStyle) { 27 | super(context, attrs, defStyle); 28 | getColours(); 29 | } 30 | 31 | private void getColours() { 32 | mSelectedColour = getResources().getColor(android.R.color.holo_blue_light); 33 | mNormalColour = getBorderColor(); 34 | } 35 | 36 | @Override 37 | public void setBorderColor(final int color) { 38 | mNormalColour = getBorderColor(); 39 | super.setBorderColor(color); 40 | } 41 | 42 | @Override 43 | public boolean onTouchEvent(final MotionEvent event) { 44 | 45 | final boolean inBounds = isInEllipseBounds(event.getX(), event.getY()); 46 | 47 | switch (event.getAction()) { 48 | 49 | case MotionEvent.ACTION_DOWN: 50 | if (inBounds) { 51 | super.setBorderColor(mSelectedColour); 52 | } else { 53 | return true; 54 | } 55 | break; 56 | 57 | case MotionEvent.ACTION_MOVE: 58 | if (!inBounds) { 59 | super.setBorderColor(mNormalColour); 60 | return true; 61 | } 62 | break; 63 | 64 | case MotionEvent.ACTION_CANCEL: 65 | case MotionEvent.ACTION_UP: 66 | super.setBorderColor(mNormalColour); 67 | if (!inBounds) { 68 | return true; 69 | } 70 | break; 71 | } 72 | 73 | return super.onTouchEvent(event); 74 | } 75 | 76 | private boolean isInEllipseBounds(final float x, final float y) { 77 | 78 | final float a = getWidth() / 2; 79 | final float b = getHeight() / 2; 80 | final float radius = Math.min(a, b); 81 | 82 | return Math.pow((x - a) / radius, 2) + Math.pow((y - b) / radius, 2) <= 1; 83 | } 84 | 85 | @Override 86 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 87 | mBounds.set(0, 0, MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); 88 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /GithubStatus/src/main/java/com/deange/githubstatus/ui/view/SliceView.java: -------------------------------------------------------------------------------- 1 | package com.deange.githubstatus.ui.view; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Canvas; 7 | import android.graphics.Outline; 8 | import android.graphics.Path; 9 | import android.graphics.Region; 10 | import android.os.Build; 11 | import android.util.AttributeSet; 12 | import android.util.TypedValue; 13 | import android.view.View; 14 | import android.view.ViewOutlineProvider; 15 | import android.widget.RelativeLayout; 16 | 17 | import com.deange.githubstatus.R; 18 | 19 | public class SliceView extends RelativeLayout { 20 | 21 | private static final float DEFAULT_HEIGHT = 100; 22 | private static final float DEFAULT_OFFSET = 0; 23 | 24 | private final Path mPath = new Path(); 25 | 26 | private float mSliceOffset; 27 | private float mSliceHeight; 28 | 29 | public SliceView(final Context context) { 30 | this(context, null); 31 | } 32 | 33 | public SliceView(final Context context, final AttributeSet attrs) { 34 | this(context, attrs, 0); 35 | } 36 | 37 | public SliceView(final Context context, final AttributeSet attrs, final int defStyle) { 38 | super(context, attrs, defStyle); 39 | 40 | final TypedArray a = 41 | getContext().obtainStyledAttributes(attrs, R.styleable.SliceView, defStyle, 0); 42 | if (a != null) { 43 | mSliceHeight = a.getDimensionPixelSize( 44 | R.styleable.SliceView_sliceHeight, (int) DEFAULT_HEIGHT); 45 | mSliceOffset = a.getDimensionPixelSize( 46 | R.styleable.SliceView_sliceOffset, (int) DEFAULT_OFFSET); 47 | a.recycle(); 48 | 49 | } else { 50 | mSliceHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_HEIGHT, 51 | getResources().getDisplayMetrics()); 52 | mSliceOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_OFFSET, 53 | getResources().getDisplayMetrics()); 54 | } 55 | 56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 57 | setOutlineProvider(new ViewOutlineProvider() { 58 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 59 | @Override 60 | public void getOutline(final View view, final Outline outline) { 61 | outline.setConvexPath(mPath); 62 | } 63 | }); 64 | } 65 | 66 | } 67 | 68 | public float getSliceOffset() { 69 | return mSliceOffset; 70 | } 71 | 72 | public void setSliceOffset(final int slicePixelsOffset) { 73 | mSliceOffset = slicePixelsOffset; 74 | requestLayout(); 75 | invalidate(); 76 | } 77 | 78 | public float getSliceHeight() { 79 | return mSliceHeight; 80 | } 81 | 82 | public void setSliceHeight(final int slicePixelsHeight) { 83 | mSliceHeight = slicePixelsHeight; 84 | requestLayout(); 85 | invalidate(); 86 | } 87 | 88 | @Override 89 | protected void onLayout(final boolean changed, 90 | final int l, final int t, final int r, final int b) { 91 | super.onLayout(changed, l, t, r, b); 92 | 93 | mPath.reset(); 94 | mPath.moveTo(0 , mSliceOffset); 95 | mPath.lineTo(getMeasuredWidth(), mSliceOffset + mSliceHeight); 96 | mPath.lineTo(getMeasuredWidth(), getMeasuredHeight()); 97 | mPath.lineTo(0 , getMeasuredHeight()); 98 | mPath.lineTo(0 , mSliceOffset); 99 | 100 | 101 | } 102 | 103 | @Override 104 | public void draw(final Canvas canvas) { 105 | canvas.clipPath(mPath, Region.Op.INTERSECT); 106 | super.draw(canvas); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-hdpi/ic_action_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-hdpi/ic_action_info.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-hdpi/ic_action_overflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-hdpi/ic_action_overflow.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-hdpi/ic_stat_octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-hdpi/ic_stat_octocat.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-mdpi/ic_action_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-mdpi/ic_action_info.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-mdpi/ic_action_overflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-mdpi/ic_action_overflow.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-mdpi/ic_stat_octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-mdpi/ic_stat_octocat.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-nodpi/bkg_menu_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-nodpi/bkg_menu_banner.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-nodpi/octocat_notif_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-nodpi/octocat_notif_green.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-nodpi/octocat_notif_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-nodpi/octocat_notif_red.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-nodpi/octocat_notif_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-nodpi/octocat_notif_yellow.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-nodpi/thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-nodpi/thumbnail.jpg -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-xhdpi/ic_action_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-xhdpi/ic_action_info.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-xhdpi/ic_action_overflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-xhdpi/ic_action_overflow.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-xhdpi/ic_stat_octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-xhdpi/ic_stat_octocat.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-xxhdpi/ic_action_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-xxhdpi/ic_action_info.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-xxhdpi/ic_action_overflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-xxhdpi/ic_action_overflow.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable-xxhdpi/ic_stat_octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/GithubStatus/src/main/res/drawable-xxhdpi/ic_stat_octocat.png -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable/background_tile.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/drawable/message_status_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/layout-land/activity_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/layout-land/fragment_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | 19 | 20 | 26 | 27 | 35 | 36 | 44 | 45 | 46 | 47 | 48 | 55 | 56 | 57 | 69 | 70 | 76 | 77 | 85 | 86 | 95 | 96 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/layout-land/view_flipper_item.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/layout/activity_gcm.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 20 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | 28 | 29 | 34 | 35 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/layout/dialog_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 26 | 27 | 33 | 34 | 41 | 42 | 49 | 50 | 51 | 52 | 53 | 54 | 62 | 63 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/layout/fragment_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 15 | 16 | 20 | 21 | 30 | 31 | 42 | 43 | 56 | 57 | 63 | 64 | 72 | 73 | 82 | 83 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/layout/fragment_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 20 | 21 | 32 | 33 | 42 | 43 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/layout/list_item_message.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 12 | 13 | 22 | 23 | 32 | 33 | 39 | 40 | 41 | 42 | 47 | 48 | 49 | 55 | 56 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/layout/view_flipper_item.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/menu/activity_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 | 8 | 9 | 12 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/menu/options_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 24 | 27 | 28 | 31 | 32 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/values-land-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/values-land/bools.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/values-v19/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/values/bools.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/values/colours.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #FF339966 5 | #FFF29D50 6 | #FFCC3300 7 | 8 | @color/status_good 9 | @color/status_good 10 | 11 | #FF123456 12 | #FFEEEEEE 13 | 14 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 16dp 4 | 5 | 75dp 6 | 7 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GitHub Status 5 | Settings 6 | 7 | Unavailable 8 | Something went wrong. 9 | Loading\u2026 10 | Previous Messages 11 | Please be patient. 12 | No messages found. 13 | 14 | GitHub is currently 15 | Refresh 16 | Settings 17 | Info 18 | © Christian De Angelis 19 | © Christian De Angelis, %s 20 | This app is not affiliated with github.com 21 | 22 | Good 23 | Minor 24 | Major 25 | 26 | Send 27 | Clear 28 | 29 | Roboto-Black.ttf 30 | Roboto-Bold.ttf 31 | Roboto-Light.ttf 32 | Roboto-LightItalic.ttf 33 | Roboto-Thin.ttf 34 | Roboto-ThinItalic.ttf 35 | 36 | 37 | 38 | Register 39 | Unregister 40 | Clear 41 | 42 | Push Notifications 43 | Receive notifications when GitHub\'s status changes. 44 | 45 | 46 | -------------------------------------------------------------------------------- /GithubStatus/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DEPRECATED 2 | ========== 3 | 4 | **GitHub Status is now Hubbub! Check it out – https://github.com/cdeange/hubbub** 5 | 6 |   7 | 8 | # github-status 9 | 10 | Simple app to connect to the GitHub Status API. [Download the app!][1] 11 | 12 | --- 13 | ### What is it? 14 | GitHub-Status keeps you up-to-date with the most recent status of GitHub, built on top of their [Status API][2]. 15 | 16 | --- 17 | ### Dependencies 18 | GitHub Status uses the **Android Support Library** for the provided components, even though it only supports ICS and above. 19 | It also leverages **Gson** and **OkHttp** for network connections and decoding JSON from the Status API. 20 | 21 | GitHub Status supports API level 14 (Ice Cream Sandwich) and above. 22 | 23 | --- 24 | ### Contributing 25 | Just submit a pull request if you think you have a cool idea for a new fix or feature! 26 | If you're planning on checking out the repo, please respect the file structure and work on it using Android Studio. 27 | 28 | --- 29 | ### Developed By 30 | - Christian De Angelis - 31 | 32 | --- 33 | ### License 34 | ``` 35 | Copyright 2018 Christian De Angelis 36 | 37 | Licensed under the Apache License, Version 2.0 (the "License"); 38 | you may not use this file except in compliance with the License. 39 | You may obtain a copy of the License at 40 | 41 | http://www.apache.org/licenses/LICENSE-2.0 42 | 43 | Unless required by applicable law or agreed to in writing, software 44 | distributed under the License is distributed on an "AS IS" BASIS, 45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | See the License for the specific language governing permissions and 47 | limitations under the License. 48 | ``` 49 | 50 | [1]: https://play.google.com/store/apps/details?id=com.deange.githubstatus 51 | [2]: https://status.github.com 52 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/github-status/4d84de3436eff5ab882a7109e0e7074a1a5757e8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jan 11 16:58:17 PST 2015 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-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':GithubStatus' 2 | --------------------------------------------------------------------------------