diff --git a/app/build.gradle b/app/build.gradle index a811191..d979b2a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,7 +90,7 @@ android { defaultConfig { applicationId "com.eveningoutpost.dexdrip" - minSdkVersion 19 + minSdkVersion 23 // increasing target SDK version can cause compatibility issues with Android 7+ //noinspection ExpiredTargetSdkVersion targetSdkVersion 23 @@ -141,6 +141,8 @@ android { exclude 'META-INF/INDEX.LIST' exclude 'META-INF/NOTICE.md' exclude 'META-INF/LICENSE.md' + pickFirst 'org/bouncycastle/x509/CertPathReviewerMessages.properties' + pickFirst 'org/bouncycastle/x509/CertPathReviewerMessages_de.properties' } compileOptions { @@ -289,6 +291,7 @@ dependencies { //implementation 'com.google.dagger:dagger-android-support:2.x' // if you use the support libraries annotationProcessor 'com.google.dagger:dagger-compiler:2.25.4' implementation 'net.danlew:android.joda:2.10.6.1' + implementation 'org.bouncycastle:bcpkix-jdk15to18:1.68' testImplementation 'joda-time:joda-time:2.10.7' testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowDownloader.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowDownloader.java index 272a255..764c379 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowDownloader.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowDownloader.java @@ -2,18 +2,24 @@ import android.os.PowerManager; +import com.eveningoutpost.dexdrip.R; +import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkAuthenticator; +import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkCredentialStore; import com.eveningoutpost.dexdrip.Models.JoH; import com.eveningoutpost.dexdrip.Models.UserError; import com.eveningoutpost.dexdrip.UtilityModels.CollectionServiceStarter; import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; -import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkAuthenticator; -import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkCredentialStore; -import com.eveningoutpost.dexdrip.cgm.carelinkfollow.client.*; +import com.eveningoutpost.dexdrip.cgm.carelinkfollow.client.CareLinkClient; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.message.RecentData; -import com.eveningoutpost.dexdrip.cgm.carelinkfollow.utils.Logger; +import com.eveningoutpost.dexdrip.xdrip; import static com.eveningoutpost.dexdrip.Models.JoH.emptyString; +/** + * CareLink Downloader + * - download data from CareLink + * - execute data conversion and update xDrip data + */ public class CareLinkFollowDownloader { private static final String TAG = "CareLinkFollowDL"; @@ -24,7 +30,7 @@ public class CareLinkFollowDownloader { private String carelinkCountry; private String carelinkPatient; - private CareLinkClient carelinkClient; + private CareLinkClient careLinkClient; private boolean loginDataLooksOkay; @@ -38,10 +44,6 @@ public String getStatus() { return status; } - public int getLastResponseCode() { - return lastResponseCode; - } - CareLinkFollowDownloader(String carelinkUsername, String carelinkPassword, String carelinkCountry, String carelinkPatient) { this.carelinkUsername = carelinkUsername; this.carelinkPassword = carelinkPassword; @@ -56,72 +58,7 @@ public static void resetInstance() { CollectionServiceStarter.restartCollectionServiceBackground(); } - public boolean doEverything() { - msg("Start download"); - if (D) UserError.Log.e(TAG, "doEverything called"); - //if (loginDataLooksOkay) { - if (CareLinkCredentialStore.getInstance().getAuthStatus() == CareLinkCredentialStore.AUTHENTICATED) { - //Refresh token if expiration - if (CareLinkCredentialStore.getInstance().getExpiresIn() < 6 * 60_000) { - UserError.Log.e(TAG, "Token is about to expire, trying to renew it."); - try { - if (!(new CareLinkAuthenticator(CareLinkCredentialStore.getInstance().getCredential().country, CareLinkCredentialStore.getInstance()).refreshToken())) { - UserError.Log.e(TAG, "Error renewing token!"); - return false; - } else { - UserError.Log.e(TAG, "Token renewed!"); - } - } catch (Exception e) { - UserError.Log.e(TAG, "Exception in renewing token! " + e.getMessage()); - return false; - } - } - try { - if (getCareLinkClient() != null) { - extendWakeLock(30_000); - backgroundProcessConnectData(); - } else { - UserError.Log.d(TAG, "Cannot get data as CareLinkClient is null"); - return false; - } - return true; - } catch (Exception e) { - UserError.Log.e(TAG, "Got exception in getData() " + e); - releaseWakeLock(); - return false; - } - } else { - if (CareLinkCredentialStore.getInstance().getAuthStatus() == CareLinkCredentialStore.NOT_AUTHENTICATED) { - UserError.Log.e(TAG, "Not authenticated! Please login!"); - msg("Not authenticated!"); - } - if (CareLinkCredentialStore.getInstance().getAuthStatus() == CareLinkCredentialStore.TOKEN_EXPIRED) { - UserError.Log.e(TAG, "Token expired!"); - msg("Token expired!"); - } - /* - final String invalid = "Invalid CareLink login data!"; - msg(invalid); - UserError.Log.e(TAG, invalid); - if(emptyString(carelinkUsername)){ - UserError.Log.e(TAG, "CareLink Username empty!"); - } - if(emptyString(carelinkPassword)){ - UserError.Log.e(TAG, "CareLink Password empty!"); - } - if(carelinkCountry == null){ - UserError.Log.e(TAG, "CareLink Country empty!"); - }else if(!CountryUtils.isSupportedCountry(carelinkCountry)){ - UserError.Log.e(TAG, "CareLink Country not supported!"); - } - */ - return false; - } - - } - public void doEverything(boolean refreshToken, boolean downloadData) { - Logger.Log(TAG, "doEverything"); if (refreshToken) this.refreshToken(); if (downloadData) @@ -129,57 +66,57 @@ public void doEverything(boolean refreshToken, boolean downloadData) { } private void downloadData() { - Logger.Log(TAG, "downloadData"); - msg("Start download"); - if (checkCredentials()) { + msg(xdrip.gs(R.string.carelink_download_start)); + if (checkCredentials(true, true, true)) { try { if (getCareLinkClient() != null) { extendWakeLock(30_000); backgroundProcessConnectData(); } else { UserError.Log.d(TAG, "Cannot get data as CareLinkClient is null"); - msg("Download data failed!"); + msg(xdrip.gs(R.string.carelink_download_failed)); } } catch (Exception e) { UserError.Log.e(TAG, "Got exception in getData() " + e); releaseWakeLock(); - msg("Download data failed!"); + msg(xdrip.gs(R.string.carelink_download_failed)); } } } private void refreshToken() { - Logger.Log(TAG, "refreshToken"); - msg("Start refreshing token"); - if (checkCredentials()) { + msg(xdrip.gs(R.string.carelink_refresh_token_start)); + if (checkCredentials(true, false, true)) { try { if (new CareLinkAuthenticator(CareLinkCredentialStore.getInstance().getCredential().country, CareLinkCredentialStore.getInstance()).refreshToken()) { - UserError.Log.e(TAG, "Login token renewed!"); + UserError.Log.d(TAG, "Access renewed!"); msg(null); } else { - UserError.Log.e(TAG, "Error renewing login token!"); - msg("Login refresh failed! Will try again!"); + UserError.Log.e(TAG, "Error renewing access token!"); + msg(xdrip.gs(R.string.carelink_refresh_token_failed)); } } catch (Exception e) { - UserError.Log.e(TAG, "Error renewing login token: " + e.getMessage()); - msg("Login refresh failed! Will try again!"); + UserError.Log.e(TAG, "Error renewing access token: " + e.getMessage()); + msg(xdrip.gs(R.string.carelink_refresh_token_failed)); } } } - private boolean checkCredentials() { + private boolean checkCredentials(boolean checkAuthenticated, boolean checkAccessExpired, boolean checkRefreshExpired) { // Not authenticated - if (CareLinkCredentialStore.getInstance().getAuthStatus() != CareLinkCredentialStore.AUTHENTICATED) { - msg("Not logged in! Please log in!"); + if (checkAuthenticated && CareLinkCredentialStore.getInstance().getAuthStatus() != CareLinkCredentialStore.AUTHENTICATED) { + msg(xdrip.gs(R.string.carelink_credential_status_not_authenticated)); return false; - // Token expired - } else if (CareLinkCredentialStore.getInstance().getExpiresIn() <= 0) { - msg("Login refresh expired! Please log in!"); + } + if (checkAccessExpired && CareLinkCredentialStore.getInstance().getAccessExpiresIn() <= 0) { + msg(xdrip.gs(R.string.carelink_credential_status_access_expired)); return false; - // Credentials are all ok! - } else { - return true; } + if (checkRefreshExpired && CareLinkCredentialStore.getInstance().getRefreshExpiresIn() <= 0) { + msg(xdrip.gs(R.string.carelink_credential_status_refresh_expired)); + return false; + } + return true; } private void msg(final String msg) { @@ -188,85 +125,86 @@ private void msg(final String msg) { } public void invalidateSession() { - this.carelinkClient = null; + this.careLinkClient = null; } private void backgroundProcessConnectData() { - Inevitable.task("proc-carelink-follow", 100, this::processCareLinkData); + Inevitable.task("proc-carelink-follow", 100, this::processConnectData); releaseWakeLock(); // handover to inevitable } // don't call this directly unless you are also handling the wakelock release - private void processCareLinkData() { + private void processConnectData() { - Logger.Log(TAG, "processCareLinkData"); RecentData recentData = null; CareLinkClient carelinkClient = null; + //Get client carelinkClient = getCareLinkClient(); + //Get RecentData from CareLink client if (carelinkClient != null) { - //Get data + //Get data + try { + if (JoH.emptyString(this.carelinkPatient)) + recentData = getCareLinkClient().getRecentData(); + else + recentData = getCareLinkClient().getRecentData(this.carelinkPatient); + lastResponseCode = carelinkClient.getLastResponseCode(); + } catch (Exception e) { + UserError.Log.e(TAG, "Exception in CareLink data download: " + e); + } + + //Process data + if (recentData != null) { + UserError.Log.d(TAG, "Success get data!"); try { - if (JoH.emptyString(this.carelinkPatient)) - recentData = getCareLinkClient().getRecentData(); - else - recentData = getCareLinkClient().getRecentData(this.carelinkPatient); - lastResponseCode = carelinkClient.getLastResponseCode(); + UserError.Log.d(TAG, "Start process data"); + //Process CareLink data (conversion and update xDrip data) + CareLinkDataProcessor.processRecentData(recentData, true); + UserError.Log.d(TAG, "ProcessData finished!"); + //Update Service status + CareLinkFollowService.updateBgReceiveDelay(); + msg(null); } catch (Exception e) { - UserError.Log.e(TAG, "Exception in CareLink data download: " + e); + UserError.Log.e(TAG, "Exception in data processing: " + e); + msg("Data processing error!"); } - //Process data - if (recentData != null) { - UserError.Log.d(TAG, "Success get data!"); - //Process data - try { - //Process CareLink data (conversion and update xDrip data) - CareLinkDataProcessor.processRecentData(recentData, true); - //Update Service status - CareLinkFollowService.updateBgReceiveDelay(); - msg(null); - } catch (Exception e) { - UserError.Log.e(TAG, "Exception in data processing: " + e); - msg("Data processing error!"); - } //Data receive error + } else { + if (carelinkClient.getLastResponseCode() == 401) { + UserError.Log.e(TAG, "CareLink login error! Response code: " + carelinkClient.getLastResponseCode()); + msg("Login error!"); + //login error } else { - if (carelinkClient.getLastResponseCode() == 401) { - UserError.Log.e(TAG, "CareLink login error! Response code: " + carelinkClient.getLastResponseCode()); - msg("Login error!"); - //login error - } else { - UserError.Log.e(TAG, "CareLink download error! Response code: " + carelinkClient.getLastResponseCode()); - UserError.Log.e(TAG, "Error message: " + getCareLinkClient().getLastErrorMessage()); - msg("Download data failed!"); - } + UserError.Log.e(TAG, "CareLink download error! Response code: " + carelinkClient.getLastResponseCode()); + UserError.Log.e(TAG, "Error message: " + getCareLinkClient().getLastErrorMessage()); + msg("Download data failed!"); } - + } } } private CareLinkClient getCareLinkClient() { - if (carelinkClient == null) { + if (careLinkClient == null) { try { UserError.Log.d(TAG, "Creating CareLinkClient"); if (CareLinkCredentialStore.getInstance().getAuthStatus() == CareLinkCredentialStore.AUTHENTICATED) - carelinkClient = new CareLinkClient(CareLinkCredentialStore.getInstance()); - //carelinkClient = new CareLinkClient(carelinkUsername, carelinkPassword, carelinkCountry); + careLinkClient = new CareLinkClient(CareLinkCredentialStore.getInstance()); } catch (Exception e) { - UserError.Log.e(TAG, "Error creating CareLinkClient: " + e.getMessage()); + UserError.Log.e(TAG, "Error creating CareLinkClient", e); } } - return carelinkClient; + return careLinkClient; } private static synchronized void extendWakeLock(final long ms) { if (wl == null) { if (D) UserError.Log.d(TAG, "Creating wakelock"); - wl = JoH.getWakeLock("CareLinkFollow-download", (int) ms); + wl = JoH.getWakeLock("CLFollow-download", (int) ms); } else { JoH.releaseWakeLock(wl); // lets not get too messy wl.acquire(ms); diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowService.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowService.java index e9f33c1..dc95ece 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowService.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/CareLinkFollowService.java @@ -6,6 +6,7 @@ import android.support.annotation.Nullable; import android.text.SpannableString; +import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkCredentialStore; import com.eveningoutpost.dexdrip.Models.BgReading; import com.eveningoutpost.dexdrip.Models.JoH; import com.eveningoutpost.dexdrip.Models.UserError; @@ -13,8 +14,6 @@ import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; import com.eveningoutpost.dexdrip.UtilityModels.Pref; import com.eveningoutpost.dexdrip.UtilityModels.StatusItem; -import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkCredentialStore; -import com.eveningoutpost.dexdrip.cgm.carelinkfollow.utils.Logger; import com.eveningoutpost.dexdrip.utils.DexCollectionType; import com.eveningoutpost.dexdrip.utils.framework.BuggySamsung; import com.eveningoutpost.dexdrip.utils.framework.ForegroundService; @@ -28,7 +27,12 @@ import static com.eveningoutpost.dexdrip.Models.JoH.msSince; import static com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder.DEXCOM_PERIOD; -import org.apache.commons.math3.complex.Quaternion; +/** + * CareLink Follow Service + * - main service class for managing CareLink Connect data retrieval + * - start/stop data retrieval + * - provide status infos of follower service + */ public class CareLinkFollowService extends ForegroundService { @@ -50,16 +54,13 @@ public class CareLinkFollowService extends ForegroundService { private static CareLinkFollowDownloader downloader; private static volatile int gracePeriod = 0; - private static volatile int pollInterval = 0; + private static volatile int missedPollInterval = 0; private static volatile int renewBefore = 0; private static volatile int renewInterval = 0; - private static final long WAKE_UP_GRACE_SECOND = 60; - @Override public void onCreate() { - Logger.Log(TAG, "onCreate"); super.onCreate(); resetInstance(); // manage static reference life cycle } @@ -69,7 +70,6 @@ public void onCreate() { * Update observedDelay if new bg reading is available */ static void updateBgReceiveDelay() { - Logger.Log(TAG, "updateBgReceiveDelay"); lastBg = BgReading.lastNoSenssor(); if (lastBg != null && lastBgTime != lastBg.timestamp) { bgReceiveDelay = JoH.msSince(lastBg.timestamp); @@ -78,7 +78,6 @@ static void updateBgReceiveDelay() { } public synchronized static void resetInstanceAndInvalidateSession() { - Logger.Log(TAG, "resetInstanceAndInvalidateSession"); try { if (downloader != null) { downloader.invalidateSession(); @@ -92,7 +91,7 @@ public synchronized static void resetInstanceAndInvalidateSession() { public static void resetInstance() { downloader = null; gracePeriod = 0; - pollInterval = 0; + missedPollInterval = 0; renewBefore = 0; renewInterval = 0; } @@ -102,10 +101,16 @@ private static boolean shouldServiceRun() { } private static long getGraceMillis() { - //return Constants.SECOND_IN_MS * WAKE_UP_GRACE_SECOND; return Constants.SECOND_IN_MS * gracePeriod; } + private static long getMissedIntervalMillis() { + if (missedPollInterval == 0) + return SAMPLE_PERIOD; + else + return Constants.MINUTE_IN_MS * missedPollInterval; + } + private static long getRenewBeforeMillis() { return Constants.MINUTE_IN_MS * renewBefore; } @@ -114,62 +119,37 @@ private static long getRenewIntervalMillis() { return Constants.MINUTE_IN_MS * renewInterval; } - private static long getIntervalMillis() { - return Constants.MINUTE_IN_MS * pollInterval; - } - - private static CareLinkFollowDownloader getDownloader(){ - if (downloader == null) { - downloader = new CareLinkFollowDownloader( - Pref.getString("clfollow_user", ""), - Pref.getString("clfollow_pass", ""), - Pref.getString("clfollow_country", "").toLowerCase(), - Pref.getString("clfollow_patient", "") - ); - } - return downloader; - } - static void scheduleWakeUp() { - Logger.Log(TAG, "scheduleWakeUp"); String scheduleReason; long next; final BgReading lastBg = BgReading.lastNoSenssor(); final long last = lastBg != null ? lastBg.timestamp : 0; - final long nextTokenRefresh = anticipateNextTokenRefresh(JoH.tsl(), CareLinkCredentialStore.getInstance().getExpiresOn(), getRenewBeforeMillis(), getRenewIntervalMillis()); - final long nextDataPoll = anticipateNextDataPoll(JoH.tsl(), last, SAMPLE_PERIOD, getGraceMillis(), getIntervalMillis()); + final long nextTokenRefresh = anticipateNextTokenRefresh(JoH.tsl(), CareLinkCredentialStore.getInstance().getAccessExpiresOn(), getRenewBeforeMillis(), getRenewIntervalMillis()); + final long nextDataPoll = anticipateNextDataPoll(JoH.tsl(), last, SAMPLE_PERIOD, getGraceMillis(), getMissedIntervalMillis()); // Token needs to refreshed sooner - if(nextTokenRefresh <= nextDataPoll){ + if (nextTokenRefresh <= nextDataPoll) { next = nextTokenRefresh; - scheduleReason = " as login expires: "; - // Data is required sooner + scheduleReason = " as access expires: "; + // Data is required sooner } else { next = nextDataPoll; scheduleReason = " as last BG timestamp: "; } - if(JoH.msTill(next) < (RATE_LIMIT_SECONDS * Constants.SECOND_IN_MS)) + if (JoH.msTill(next) < (RATE_LIMIT_SECONDS * Constants.SECOND_IN_MS)) next = JoH.tsl() + (RATE_LIMIT_SECONDS * Constants.SECOND_IN_MS); + wakeup_time = next; UserError.Log.d(TAG, "Anticipate next: " + JoH.dateTimeText(next) + scheduleReason + JoH.dateTimeText(last)); - wakeup_time = next; JoH.wakeUpIntent(xdrip.getAppContext(), JoH.msTill(next), WakeLockTrampoline.getPendingIntent(CareLinkFollowService.class, Constants.CARELINKFOLLOW_SERVICE_FAILOVER_ID)); } - public static long anticipateNextWakeUp(long now, final long last, final long period, final long grace, final long interval) { - - long nextDataPoll = anticipateNextDataPoll(now, last, period, grace, interval); - - return nextDataPoll; - - } - - private static long anticipateNextTokenRefresh(long now, final long expiry, final long before, final long interval){ + private static long anticipateNextTokenRefresh(long now, final long expiry, final long before, final long interval) { long next; @@ -184,7 +164,7 @@ private static long anticipateNextTokenRefresh(long now, final long expiry, fina } - private static long anticipateNextDataPoll(long now, final long last, final long period, final long grace, final long interval) { + public static long anticipateNextDataPoll(long now, final long last, final long period, final long grace, final long missedInterval) { long next; @@ -196,9 +176,9 @@ private static long anticipateNextDataPoll(long now, final long last, final long else { //last expected next = now + ((last - now) % period); - //add poll interval until next time is reached + //add missed poll interval until future time is reached while (next <= now) { - next += interval; + next += missedInterval; } //add grace next += grace; @@ -208,14 +188,23 @@ private static long anticipateNextDataPoll(long now, final long last, final long } + private static CareLinkFollowDownloader getDownloader() { + if (downloader == null) { + downloader = new CareLinkFollowDownloader( + Pref.getString("clfollow_user", ""), + Pref.getString("clfollow_pass", ""), + Pref.getString("clfollow_country", "").toLowerCase(), + Pref.getString("clfollow_patient", "") + ); + } + return downloader; + } @Override public int onStartCommand(Intent intent, int flags, int startId) { - - final PowerManager.WakeLock wl = JoH.getWakeLock("ConnectFollow-osc", 60000); + final PowerManager.WakeLock wl = JoH.getWakeLock("CareLinkFollow-osc", 60000); try { - Logger.Log(TAG, "onStartCommand"); UserError.Log.d(TAG, "WAKE UP WAKE UP WAKE UP"); // Check service should be running @@ -225,25 +214,25 @@ public int onStartCommand(Intent intent, int flags, int startId) { stopSelf(); return START_NOT_STICKY; } - buggySamsungCheck(); last_wakeup = JoH.tsl(); + // Check current if (gracePeriod == 0) gracePeriod = Pref.getStringToInt("clfollow_grace_period", 30); - if (pollInterval == 0) - pollInterval = Pref.getStringToInt("clfollow_poll_interval", 5); - if(renewBefore == 0) + if (missedPollInterval == 0) + missedPollInterval = Pref.getStringToInt("clfollow_missed_poll_interval", 5); + if (renewBefore == 0) renewBefore = 10; - if(renewInterval == 0) + if (renewInterval == 0) renewInterval = 1; lastBg = BgReading.lastNoSenssor(); if (lastBg != null) { lastBgTime = lastBg.timestamp; } // Check if downloader needs to be started (last BG old or token needs to be renewed) - final boolean refreshToken = (JoH.msTill(CareLinkCredentialStore.getInstance().getExpiresOn())< getRenewBeforeMillis()) ? true : false; + final boolean refreshToken = (JoH.msTill(CareLinkCredentialStore.getInstance().getAccessExpiresOn()) < getRenewBeforeMillis()) ? true : false; final boolean downloadData = (lastBg == null || msSince(lastBg.timestamp) > SAMPLE_PERIOD + getGraceMillis()) ? true : false; if (refreshToken || downloadData) { //Only start if rate limit is not exceeded @@ -268,12 +257,12 @@ public int onStartCommand(Intent intent, int flags, int startId) { } /** - * MegaStatus for CareLink Follower + * MegaStatus for Connect Follower */ public static List megaStatus() { final BgReading lastBg = BgReading.lastNoSenssor(); - long hightlightGrace = Constants.SECOND_IN_MS * getGraceMillis() * 2 + Constants.SECOND_IN_MS * 10; // 2 times garce + 20 seconds for processing + long hightlightGrace = Constants.SECOND_IN_MS * getGraceMillis() + Constants.SECOND_IN_MS * 10; //garce + 20 seconds for processing // Status for BG receive delay (time from bg was recorded till received in xdrip) String ageOfBgLastPoll = "n/a"; @@ -299,19 +288,9 @@ public static List megaStatus() { } } - // Last response code - int lastResponseCode = downloader != null ? downloader.getLastResponseCode() : 0; - - StatusItem.Highlight authHighlight = StatusItem.Highlight.NORMAL; - - if (lastBg != null) { - long age = JoH.msSince(lastBg.timestamp); - ageLastBg = JoH.niceTimeScalar(age); - if (age > SAMPLE_PERIOD + hightlightGrace) { - bgAgeHighlight = StatusItem.Highlight.BAD; - } - } + // Status of authentication String authStatus = null; + StatusItem.Highlight authHighlight = null; switch (CareLinkCredentialStore.getInstance().getAuthStatus()) { case CareLinkCredentialStore.NOT_AUTHENTICATED: authStatus = "NOT AUTHENTICATED"; @@ -321,22 +300,42 @@ public static List megaStatus() { authHighlight = StatusItem.Highlight.GOOD; authStatus = "AUTHENTICATED"; break; - case CareLinkCredentialStore.TOKEN_EXPIRED: + case CareLinkCredentialStore.ACCESS_EXPIRED: + authHighlight = StatusItem.Highlight.NOTICE; + authStatus = "ACCESS EXPIRED"; + break; + case CareLinkCredentialStore.REFRESH_EXPIRED: authHighlight = StatusItem.Highlight.BAD; - authStatus = "TOKEN EXPIRED"; + authStatus = "REFRESH EXPIRED"; break; } - //Create status screeen + //Client type + String clientType = "Unkown"; + try { + switch (CareLinkCredentialStore.getInstance().getCredential().authType) { + case Browser: + clientType = "Browser"; + break; + case MobileApp: + clientType = "CarePartner app"; + break; + } + } catch (Exception ex) { + } + + //Build status screeen List megaStatus = new ArrayList<>(); + megaStatus.add(new StatusItem("Client type", clientType)); megaStatus.add(new StatusItem("Authentication status", authStatus, authHighlight)); - megaStatus.add(new StatusItem("Login expires in", JoH.niceTimeScalar(CareLinkCredentialStore.getInstance().getExpiresIn()))); + megaStatus.add(new StatusItem("Access expires in", JoH.niceTimeScalar(CareLinkCredentialStore.getInstance().getAccessExpiresIn()))); + megaStatus.add(new StatusItem("Login expires in", JoH.niceTimeScalar(CareLinkCredentialStore.getInstance().getRefreshExpiresIn()))); megaStatus.add(new StatusItem()); megaStatus.add(new StatusItem("Latest BG", ageLastBg + (lastBg != null ? " ago" : ""), bgAgeHighlight)); megaStatus.add(new StatusItem("BG receive delay", ageOfBgLastPoll, ageOfLastBgPollHighlight)); megaStatus.add(new StatusItem("Data period:", JoH.niceTimeScalar(SAMPLE_PERIOD))); megaStatus.add(new StatusItem("Grace period:", JoH.niceTimeScalar(getGraceMillis()))); - megaStatus.add(new StatusItem("Missed poll interval:", JoH.niceTimeScalar(getIntervalMillis()))); + megaStatus.add(new StatusItem("Missed poll interval:", JoH.niceTimeScalar(getMissedIntervalMillis()))); megaStatus.add(new StatusItem()); megaStatus.add(new StatusItem("Last poll", lastPoll > 0 ? JoH.niceTimeScalar(JoH.msSince(lastPoll)) + " ago" : "n/a")); megaStatus.add(new StatusItem("Last wakeup", last_wakeup > 0 ? JoH.niceTimeScalar(JoH.msSince(last_wakeup)) + " ago" : "n/a")); @@ -347,9 +346,7 @@ public static List megaStatus() { megaStatus.add(new StatusItem("Last poll time", lastPoll > 0 ? JoH.dateTimeText(lastPoll) : "n/a")); megaStatus.add(new StatusItem("Next poll time", JoH.dateTimeText(wakeup_time))); megaStatus.add(new StatusItem()); - megaStatus.add(new StatusItem("Last response code", lastResponseCode)); - megaStatus.add(new StatusItem()); - megaStatus.add(new StatusItem("Buggy Samsung", JoH.buggy_samsung ? "Yes" : "No")); + megaStatus.add(new StatusItem("Buggy handset", JoH.buggy_samsung ? "Yes" : "No")); return megaStatus; } @@ -362,7 +359,6 @@ public IBinder onBind(Intent intent) { @Override public void onDestroy() { - Logger.Log(TAG, "onDestroy"); super.onDestroy(); resetInstance(); // manage static reference life cycle } @@ -396,5 +392,4 @@ public static SpannableString nanoStatus() { return emptyString(current_state) ? null : new SpannableString(current_state); } - } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthType.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthType.java new file mode 100644 index 0000000..9c446f0 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthType.java @@ -0,0 +1,6 @@ +package com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth; + +public enum CareLinkAuthType { + Browser, + MobileApp +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthentication.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthentication.java new file mode 100644 index 0000000..d7d87e1 --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthentication.java @@ -0,0 +1,20 @@ +package com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth; + +import okhttp3.Headers; + +public class CareLinkAuthentication { + + public CareLinkAuthType authType; + private Headers.Builder builder; + + public CareLinkAuthentication(Headers headers, CareLinkAuthType authType) { + this.builder = new Headers.Builder(); + this.builder.addAll(headers); + this.authType = authType; + } + + public Headers getHeaders() { + return builder.build(); + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthenticator.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthenticator.java index b83ed17..ce082dd 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthenticator.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkAuthenticator.java @@ -2,25 +2,65 @@ import android.app.Activity; import android.app.Dialog; +import android.app.AlertDialog; import android.content.DialogInterface; import android.graphics.Bitmap; +import android.net.UrlQuerySanitizer; import android.os.Handler; import android.os.Looper; import android.support.v7.widget.LinearLayoutCompat; import android.view.ViewGroup; import android.webkit.CookieManager; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; +import com.eveningoutpost.dexdrip.R; +import com.eveningoutpost.dexdrip.Models.UserError; +import com.eveningoutpost.dexdrip.xdrip; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.KeyPair; +import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.util.ArrayList; + +import android.util.Base64; +import android.widget.LinearLayout; +import android.widget.ProgressBar; + +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; + +import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.UUID; import java.util.concurrent.Semaphore; +import javax.security.auth.x500.X500Principal; + +import okhttp3.ConnectionPool; import okhttp3.Cookie; +import okhttp3.FormBody; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -29,12 +69,22 @@ public class CareLinkAuthenticator { + private static final String TAG = "CareLinkAuthenticator"; + + protected static final String CAREPARTNER_APP_DISCO_URL = "https://clcloud.minimed.com/connect/carepartner/v6/discover/android/3.1"; protected static final String CARELINK_CONNECT_SERVER_EU = "carelink.minimed.eu"; protected static final String CARELINK_CONNECT_SERVER_US = "carelink.minimed.com"; protected static final String CARELINK_LANGUAGE_EN = "en"; protected static final String CARELINK_AUTH_TOKEN_COOKIE_NAME = "auth_tmp_token"; protected static final String CARELINK_TOKEN_VALIDTO_COOKIE_NAME = "c_token_valid_to"; + protected static final String[] ANDROID_MODELS = { + "SM-G973F", + "SM-G988U1", + "SM-G981W", + "SM-G9600" + }; + protected static final SimpleDateFormat[] VALIDTO_DATE_FORMATS = { new SimpleDateFormat("EEE MMM d HH:mm:ss zzz yyyy", Locale.ENGLISH), new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ENGLISH), @@ -52,10 +102,19 @@ public class CareLinkAuthenticator { //new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX") }; + private static final int PKCE_BASE64_ENCODE_SETTINGS = + Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE; + private final Semaphore available = new Semaphore(0, true); + private AlertDialog progressDialog; private String carelinkCountry; private CareLinkCredentialStore credentialStore; + private CarePartnerAppConfig carepartnerAppConfig; + private String authCode = null; + private OkHttpClient httpClient = null; + private boolean authWebViewCancelled = false; + private boolean carelinkCommunicationError = false; public CareLinkAuthenticator(String carelinkCountry, CareLinkCredentialStore credentialStore) { @@ -63,53 +122,426 @@ public CareLinkAuthenticator(String carelinkCountry, CareLinkCredentialStore cre this.credentialStore = credentialStore; } - /* - public synchronized CareLinkCredential getCreditential() throws InterruptedException { - if(Looper.myLooper() == Looper.getMainLooper()) - throw new RuntimeException("don't call getAccessToken() from the main thread."); + public boolean authenticate(Activity context, CareLinkAuthType authType) throws InterruptedException { - switch (credentialStore.getAuthStatus()) { - case CareLinkCredentialStore.NOT_AUTHENTICATED: - authenticate(); - available.acquire(); - break; - case CareLinkCredentialStore.TOKEN_EXPIRED: - refreshToken(); - available.acquire(); + if (Looper.myLooper() == Looper.getMainLooper()) + throw new RuntimeException("don't call authenticate() from the main thread."); + + //Execute authentication method of authentication type + switch (authType) { + case Browser: + this.authenticateAsBrowser(context); break; - case CareLinkCredentialStore.AUTHENTICATED: + case MobileApp: + this.authenticateAsCpApp(context); break; } - return credentialStore.getCredential(); + //Return: is authenticated + return (credentialStore.getAuthStatus() == CareLinkCredentialStore.AUTHENTICATED); } - */ - public boolean authenticate(Activity context) throws InterruptedException { - if (Looper.myLooper() == Looper.getMainLooper()) - throw new RuntimeException("don't call authenticate() from the main thread."); + public boolean refreshToken() { + //Have credential, authType is known, already authenticated + if (credentialStore.getCredential() != null && credentialStore.getCredential().authType != null && credentialStore.getAuthStatus() != CareLinkCredentialStore.NOT_AUTHENTICATED) { + switch (credentialStore.getCredential().authType) { + case Browser: + return this.refreshBrowserToken(); + case MobileApp: + return this.refreshCpAppToken(); + default: + return false; + } + } else { + return false; + } + } + + private void authenticateAsCpApp(Activity context) { + + String deviceId; + String androidModel; + String clientId; + String clientSecret; + String magIdentifier; + JsonObject clientCredential; + String codeVerifier; + String authUrl; + String idToken; + String idTokenType; + + try { + + carelinkCommunicationError = false; - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(new Runnable() { + //Show progress dialog while preparing for login page + this.showProgressDialog(context); + + //Generate ID, models + deviceId = generateDeviceId(); + androidModel = this.generateAndroidModel(); + + //Load application config + this.loadAppConfig(); + + //Create client credential + clientCredential = this.createClientCredential(deviceId); + clientId = clientCredential.get("client_id").getAsString(); + clientSecret = clientCredential.get("client_secret").getAsString(); + + //Prepare authentication + UserError.Log.d(TAG, "Prepare authentication"); + codeVerifier = generateRandomDataBase64url(32); + authUrl = this.prepareAuth(clientId, codeVerifier); + + //Hide progress dialog + this.hideProgressDialog(); + + //Authenticate in browser + UserError.Log.d(TAG, "Start browser login"); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + CareLinkAuthenticator.this.showCpAppAuthPage(context, authUrl); + } + }); + available.acquire(); + + //Continue if not cancelled and no error + if (!this.authWebViewCancelled && !carelinkCommunicationError) { + //Show progress dialog while completing authentication + this.showProgressDialog(context); + + //Register device + UserError.Log.d(TAG, "Register device"); + Response registerResp = this.registerDevice(deviceId, androidModel, clientId, clientSecret, authCode, codeVerifier); + magIdentifier = registerResp.header("mag-identifier"); + idToken = registerResp.header("id-token"); + idTokenType = registerResp.header("id-token-type"); + + //Get access token + UserError.Log.d(TAG, "Get access token"); + JsonObject tokenObject = this.getAccessToken(clientId, clientSecret, magIdentifier, idToken, idTokenType); + + //Store credentials + UserError.Log.d(TAG, "Store credentials"); + this.credentialStore.setMobileAppCredential(this.carelinkCountry, + deviceId, androidModel, clientId, clientSecret, magIdentifier, + tokenObject.get("access_token").getAsString(), tokenObject.get("refresh_token").getAsString(), + //new Date(Calendar.getInstance().getTime().getTime() + 15 * 60000), + //new Date(Calendar.getInstance().getTime().getTime() + 30 * 60000)); + new Date(Calendar.getInstance().getTime().getTime() + (tokenObject.get("expires_in").getAsInt() * 1000)), + new Date(Calendar.getInstance().getTime().getTime() + (this.carepartnerAppConfig.getRefreshLifetimeSec() * 1000))); + + //Hide progress dialog + this.hideProgressDialog(); + } + + } catch (Exception ex) { + UserError.Log.e(TAG, "Error authenticating as CpApp. Details: \r\n " + ex.getMessage()); + carelinkCommunicationError = true; + this.hideProgressDialog(); + } + + //Show communication error + if (carelinkCommunicationError) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + new AlertDialog.Builder(context) + .setTitle("Communication error!") + .setMessage("Error communicating with CareLink Server! Please try again later!") + .setCancelable(true) + .setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + dialogInterface.cancel(); + } + }) + .show(); + } + }); + } + + } + + private void authenticateAsBrowser(Activity context) throws InterruptedException { + + //Authenticate in browser + new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { - showDialog(context); + CareLinkAuthenticator.this.showBrowserAuthPage(context, ""); } }); available.acquire(); - return (credentialStore.getAuthStatus() == CareLinkCredentialStore.AUTHENTICATED); } - public boolean refreshToken() { + private OkHttpClient getHttpClient() { + if (this.httpClient == null) + this.httpClient = new OkHttpClient(); + return this.httpClient; + } + + private boolean loadAppConfig() throws IOException { + if (carepartnerAppConfig == null) { + carepartnerAppConfig = new CarePartnerAppConfig(); + UserError.Log.d(TAG, "Get region config"); + carepartnerAppConfig.regionConfig = this.getCpAppRegionConfig(); + UserError.Log.d(TAG, "Get SSO config"); + carepartnerAppConfig.ssoConfig = this.getCpAppSSOConfig(carepartnerAppConfig.getSSOConfigUrl()); + } + return true; + } + + private JsonObject getAccessToken(String clientId, String clientSecret, String magIdentifier, String idToken, String idTokenType) throws IOException { + return this.getToken(clientId, clientSecret, magIdentifier, idToken, idTokenType, null); + } + + private JsonObject refreshToken(String clientId, String clientSecret, String magIdentifier, String refreshToken) throws IOException { + return this.getToken(clientId, clientSecret, magIdentifier, null, null, refreshToken); + } + + private JsonObject getToken(String clientId, String clientSecret, String magIdentifier, String idToken, String idTokenType, String refreshToken) throws IOException { + + Request.Builder requestBuilder; + FormBody.Builder form; + + //Common token request params + form = new FormBody.Builder() + .add("client_id", clientId) + .add("client_secret", clientSecret); + //Authentication token request params + if (idToken != null) { + form.add("assertion", idToken) + .add("grant_type", idTokenType) + .add("scope", this.carepartnerAppConfig.getOAuthScope()); + //Refresh token request params + } else { + form.add("refresh_token", refreshToken) + .add("grant_type", "refresh_token"); + } + + requestBuilder = new Request.Builder() + .post(form.build()) + .addHeader("mag-identifier", magIdentifier) + .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + + return this.callSsoRestApi(requestBuilder, carepartnerAppConfig.getOAuthTokenEndpoint(), null); + + } + + private Response registerDevice(String deviceId, String androidModel, String clientId, String clientSecret, String authCode, String codeVerifier) throws IOException { + + String trimmedCsr = null; + + //Create RSA2048 keypair and CSR + try { + KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA"); + keygen.initialize(2048); + KeyPair keypair = keygen.genKeyPair(); + trimmedCsr = createTrimmedCsr(keypair, "SHA256withRSA", "socialLogin", deviceId, androidModel, "Medtronic"); + } catch (Exception e) { + UserError.Log.e(TAG, "Error creating CSR! Details: \r\n" + e.getMessage()); + } + + //Register device and get certificate for CSR + RequestBody body; + Request.Builder requestBuilder; + + body = RequestBody.create(null, trimmedCsr); + + requestBuilder = new Request.Builder() + .post(body) + .addHeader("device-id", Base64.encodeToString(deviceId.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP)) + .addHeader("device-name", Base64.encodeToString(androidModel.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP)) + .addHeader("authorization", "Bearer " + authCode) + .addHeader("client-authorization", "Basic " + Base64.encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP)) + .addHeader("cert-format", "pem") + .addHeader("create-session", "true") + .addHeader("code-verifier", codeVerifier) + .addHeader("redirect-uri", carepartnerAppConfig.getOAuthRedirectUri()); + + return this.callSsoApi(requestBuilder, carepartnerAppConfig.getMagDeviceRegisterEndpoint(), null); + + } + + private String createTrimmedCsr(KeyPair keypair, String signAlgo, String cn, String ou, String dc, String o) throws IOException, OperatorCreationException { + + PKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder( + new X500Principal( + "CN=" + cn + + ", OU=" + ou + + ", DC=" + dc + + ", O=" + o), keypair.getPublic()); + JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(signAlgo); + ContentSigner signer = csBuilder.build(keypair.getPrivate()); + PKCS10CertificationRequest csr = p10Builder.build(signer); + StringWriter writer = new StringWriter(); + JcaPEMWriter jcaPEMWriter = new JcaPEMWriter(writer); + jcaPEMWriter.writeObject(csr); + jcaPEMWriter.close(); + + return writer.toString().replaceAll("-----.*-----", "").replaceAll("\\r", "").replaceAll("\\n", ""); + + } + + private String prepareAuth(String clientId, String codeVerifier) throws IOException { + + Request.Builder requestBuilder; + Map queryParams; + String codeChallenge = null; + JsonObject providers; + + //Generate SHA-256 code challenge + try { + codeChallenge = Base64.encodeToString( + MessageDigest.getInstance("SHA-256").digest(codeVerifier.getBytes("ISO_8859_1")), + PKCE_BASE64_ENCODE_SETTINGS); + } catch (Exception ex) { + } + + //Set params + queryParams = new HashMap(); + queryParams.put("client_id", clientId); + queryParams.put("response_type", "code"); + queryParams.put("display", "social_login"); + queryParams.put("scope", this.carepartnerAppConfig.getOAuthScope()); + queryParams.put("code_challenge", codeChallenge); + queryParams.put("code_challenge_method", "S256"); + queryParams.put("redirect_uri", this.carepartnerAppConfig.getOAuthRedirectUri()); + queryParams.put("state", generateRandomDataBase64url(32)); + + requestBuilder = new Request.Builder() + .get(); + + providers = this.callSsoRestApi(requestBuilder, carepartnerAppConfig.getOAuthAuthEndpoint(), queryParams); + + //Get auth url of enterprise login provider + for (JsonElement provider : providers.get("providers").getAsJsonArray()) { + if (provider.getAsJsonObject().get("provider").getAsJsonObject().get("id").getAsString().contentEquals("enterprise")) + return (provider.getAsJsonObject().get("provider").getAsJsonObject().get("auth_url").getAsString()); + } + + return null; + + } + + private JsonObject createClientCredential(String deviceId) throws IOException { + + RequestBody form; + Request.Builder requestBuilder; + + form = new FormBody.Builder() + .add("client_id", carepartnerAppConfig.getClientId()) + .add("nonce", UUID.randomUUID().toString()) + .build(); + + requestBuilder = new Request.Builder() + .post(form) + .addHeader("device-id", Base64.encodeToString(deviceId.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP | Base64.URL_SAFE)); + + return this.callSsoRestApi(requestBuilder, carepartnerAppConfig.getMagCredentialInitEndpoint(), null); + + } + + private String generateDeviceId() { + + try { + byte[] bytes = MessageDigest.getInstance("SHA-256").digest(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); + StringBuilder stringBuilder = new StringBuilder(bytes.length); + for (byte byteChar : bytes) + stringBuilder.append(String.format("%02x", byteChar)); + return stringBuilder.toString(); + } catch (NoSuchAlgorithmException e) { + UserError.Log.e(TAG, "Error generating deviceId! Details: \r\n" + e.getMessage()); + } + + return null; + + } + + private String generateAndroidModel() { + return ANDROID_MODELS[new Random().nextInt(ANDROID_MODELS.length)]; + } + + private String generateRandomDataBase64url(int length) { + SecureRandom secureRandom = new SecureRandom(); + byte[] codeVerifier = new byte[length]; + secureRandom.nextBytes(codeVerifier); + return Base64.encodeToString(codeVerifier, PKCE_BASE64_ENCODE_SETTINGS); + } + + private JsonObject callSsoRestApi(Request.Builder requestBuilder, String endpoint, Map queryParams) throws IOException { + + Response response = this.callSsoApi(requestBuilder, endpoint, queryParams); + if (response.isSuccessful()) { + return JsonParser.parseString(response.body().string()).getAsJsonObject(); + } else { + return null; + } + + } + + private Response callSsoApi(Request.Builder requestBuilder, String endpoint, Map queryParams) throws IOException { + + HttpUrl.Builder url = null; + + //Build URL + url = new HttpUrl.Builder() + .scheme("https") + .host(carepartnerAppConfig.getSSOServerHost()) + .addPathSegments(carepartnerAppConfig.getSSOServerPrefix()) + .addPathSegments(endpoint); + //Add query params + if (queryParams != null) { + for (Map.Entry param : queryParams.entrySet()) { + url.addQueryParameter(param.getKey(), param.getValue()); + } + } + requestBuilder.url(url.build()); + //Send request + return this.getHttpClient().newCall(requestBuilder.build()).execute(); + + } + + private boolean refreshCpAppToken() { + JsonObject tokenRefreshResult; + + try { + //Get config + this.loadAppConfig(); + //Refresh token + tokenRefreshResult = this.refreshToken( + credentialStore.getCredential().clientId, credentialStore.getCredential().clientSecret, + credentialStore.getCredential().magIdentifier, credentialStore.getCredential().refreshToken); + //Save token + credentialStore.updateMobileAppCredential( + tokenRefreshResult.get("access_token").getAsString(), + //new Date(Calendar.getInstance().getTime().getTime() + 15 * 60000), + //new Date(Calendar.getInstance().getTime().getTime() + 30 * 60000), + new Date(Calendar.getInstance().getTime().getTime() + (tokenRefreshResult.get("expires_in").getAsInt() * 1000)), + new Date(Calendar.getInstance().getTime().getTime() + (this.carepartnerAppConfig.getRefreshLifetimeSec() * 1000)), + tokenRefreshResult.get("refresh_token").getAsString()); + //Completed successfully + return true; + } catch (Exception ex) { + UserError.Log.e(TAG, "Error refreshing CpApp token! Details: \r\n" + ex.getMessage()); + return false; + } + } + + private boolean refreshBrowserToken() { + //If not authenticated => unable to refresh if (credentialStore.getAuthStatus() == CareLinkCredentialStore.NOT_AUTHENTICATED) return false; - HttpUrl url = null; - OkHttpClient httpClient = null; - Request.Builder requestBuilder = null; - Response response = null; - EditableCookieJar cookieJar = null; + HttpUrl url; + OkHttpClient httpClient; + Request.Builder requestBuilder; + Response response; + EditableCookieJar cookieJar; //Build client with cookies from CredentialStore @@ -143,15 +575,14 @@ public boolean refreshToken() { //New authentication cookies found if (cookieJar.contains(CARELINK_AUTH_TOKEN_COOKIE_NAME) && cookieJar.contains(CARELINK_TOKEN_VALIDTO_COOKIE_NAME)) { //Update credentials - this.credentialStore.setCredential( - this.carelinkCountry, + this.credentialStore.updateBrowserCredential( cookieJar.getCookies(CARELINK_AUTH_TOKEN_COOKIE_NAME).get(0).value(), this.parseValidTo(cookieJar.getCookies(CARELINK_TOKEN_VALIDTO_COOKIE_NAME).get(0).value()), + this.parseValidTo(cookieJar.getCookies(CARELINK_TOKEN_VALIDTO_COOKIE_NAME).get(0).value()), cookieJar.getAllCookies().toArray(new Cookie[0])); } else { return false; } - } //error in response else { @@ -165,13 +596,81 @@ public boolean refreshToken() { } return (credentialStore.getAuthStatus() == CareLinkCredentialStore.AUTHENTICATED); + } + + private void showProgressDialog(Activity context) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + CareLinkAuthenticator.this.getProgressDialog(context).show(); + } + }); + } + + private void hideProgressDialog() { + if (this.progressDialog != null && this.progressDialog.isShowing()) { + this.progressDialog.dismiss(); + } + } + private AlertDialog getProgressDialog(Activity context) { + if (this.progressDialog == null) { + AlertDialog.Builder builder; + builder = new AlertDialog.Builder(context); + builder.setTitle(xdrip.gs(R.string.carelink_auth_login_in_progress)); + final ProgressBar progressBar = new ProgressBar(context); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + progressBar.setLayoutParams(lp); + builder.setView(progressBar); + this.progressDialog = builder.create(); + } + //return builder; + return this.progressDialog; } - private void showDialog(Activity context) { + private void showBrowserAuthPage(Activity context, String url) { + final Dialog authDialog = new Dialog(context); + this.showAuthWebView(authDialog, url, new WebViewClient() { + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + if (CareLinkAuthenticator.this.extractBrowserLoginCookies(url)) + authDialog.dismiss(); + } + }); + } + + private void showCpAppAuthPage(Activity context, String url) { + + //CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build(); + //customTabsIntent.launchUrl( + // context, Uri.parse(url)); - //Create dialog final Dialog authDialog = new Dialog(context); + this.showAuthWebView(authDialog, url, new WebViewClient() { + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + if (CareLinkAuthenticator.this.extractCpAppAuthCode(url)) + //close browser dialog + authDialog.dismiss(); + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + //Connection error + if (error.getErrorCode() == WebViewClient.ERROR_CONNECT) { + carelinkCommunicationError = true; + authDialog.dismiss(); + } + } + }); + } + + private void showAuthWebView(Dialog authDialog, String url, WebViewClient webViewClient) { + + this.authWebViewCancelled = false; + LinearLayoutCompat layout = new LinearLayoutCompat(authDialog.getContext()); WebView webView = new WebView(authDialog.getContext()); webView.setLayoutParams(new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); @@ -184,42 +683,86 @@ public void onDismiss(DialogInterface dialog) { unlock(); } }); + authDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + authWebViewCancelled = true; + } + }); //Configure Webview webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setUserAgentString("Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36"); - HashMap headers = new HashMap<>(); - headers.put("Accept-Language", "en;q=0.9, *;q=0.8"); - headers.put("Sec-Ch-Ua", "\"Google Chrome\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\""); - webView.loadUrl(this.getLoginUrl(), headers); - webView.setWebViewClient(new WebViewClient() { + webView.loadUrl(url); + webView.setWebViewClient(webViewClient); - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - if (CareLinkAuthenticator.this.extractCookies(url)) - authDialog.dismiss(); + //Set dialog display params and show it + authDialog.setCancelable(true); + authDialog.getWindow().setLayout(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.MATCH_PARENT); + authDialog.show(); + + } + + private boolean extractCpAppAuthCode(String url) { + + //Redirect url => extract code, completed + if (url.contains(this.carepartnerAppConfig.getOAuthRedirectUri())) { + try { + UrlQuerySanitizer sanitizer = new UrlQuerySanitizer(); + sanitizer.setAllowUnregisteredParamaters(true); + sanitizer.parseUrl(url); + authCode = sanitizer.getValue("code"); + } catch (Exception ex) { + UserError.Log.e(TAG, "Error extracting authCode! Details: \r\n" + ex.getMessage()); } + return true; + //Other url => authentication not completed yet + } else + return false; - @Override - public void onPageFinished(WebView view, String url) { - if (CareLinkAuthenticator.this.extractCookies(url)) - authDialog.dismiss(); + } + + private JsonObject getCpAppRegionConfig() throws IOException { + + + //Get CarePartner app discover + JsonObject endpointConfig = this.getConfigJson(CAREPARTNER_APP_DISCO_URL); + //Extract region config of selected country + JsonArray countries = endpointConfig.getAsJsonArray("supportedCountries"); + JsonArray regions = endpointConfig.getAsJsonArray("CP"); + for (JsonElement country : countries) { + if (country.getAsJsonObject().has(this.carelinkCountry.toUpperCase(Locale.ROOT))) { + String regionCode = country.getAsJsonObject().get(this.carelinkCountry.toUpperCase(Locale.ROOT)).getAsJsonObject().get("region").getAsString(); + for (JsonElement region : regions) { + if (region.getAsJsonObject().get("region").getAsString().contentEquals(regionCode)) { + return region.getAsJsonObject(); + } + } } - }); + } + return null; - //Set dialog display infos and show it - authDialog.setCancelable(true); - /* - authDialog.getWindow().setLayout( - (int) (context.getResources().getDisplayMetrics().widthPixels), - (int) (context.getResources().getDisplayMetrics().heightPixels)); - */ - authDialog.getWindow().setLayout(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.MATCH_PARENT); - authDialog.show(); } - protected String getLoginUrl() { + private JsonObject getCpAppSSOConfig(String url) throws IOException { + return this.getConfigJson(url); + } + + private JsonObject getConfigJson(String url) throws IOException { + + Request request; + + request = new Request.Builder() + .url(url) + .get() + .build(); + Response response = this.getHttpClient().newCall(request).execute(); + return JsonParser.parseString(response.body().string()).getAsJsonObject(); + + } + + private String getWebAppLoginUrl() { HttpUrl url = null; @@ -235,14 +778,15 @@ protected String getLoginUrl() { } - protected String careLinkServer() { + private String careLinkServer() { if (this.carelinkCountry.equals("us")) return CARELINK_CONNECT_SERVER_US; else return CARELINK_CONNECT_SERVER_EU; } - protected Boolean extractCookies(String url) { + private Boolean extractBrowserLoginCookies(String url) { + String cookies = null; String authToken = null; String host = null; @@ -288,20 +832,22 @@ protected Boolean extractCookies(String url) { return false; //Update credentials - this.credentialStore.setCredential(this.carelinkCountry, authToken, validToDate, cookieList.toArray(new Cookie[0])); + this.credentialStore.setBrowserCredential(this.carelinkCountry, authToken, validToDate, validToDate, cookieList.toArray(new Cookie[0])); + //success return true; } else //error return false; } - protected void unlock() { + private void unlock() { if (available.availablePermits() <= 0) available.release(); } - protected Date parseValidTo(String validToDateString) { + private Date parseValidTo(String validToDateString) { for (SimpleDateFormat zonedFormat : VALIDTO_DATE_FORMATS) { + //try until translate is successful try { return zonedFormat.parse(validToDateString); } catch (Exception ex) { @@ -310,5 +856,4 @@ protected Date parseValidTo(String validToDateString) { return null; } - } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredential.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredential.java index 639b3f9..89705d1 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredential.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredential.java @@ -4,13 +4,39 @@ import java.util.Date; import okhttp3.Cookie; +import okhttp3.Headers; public class CareLinkCredential { + public String country = null; public String accessToken = null; public Cookie[] cookies = null; - public Date tokenValidTo = null; + public Date accessValidTo = null; + public Date refreshValidTo = null; + public CareLinkAuthType authType = null; + public String androidModel = null; + public String deviceId = null; + public String clientId = null; + public String clientSecret = null; + public String magIdentifier = null; + public String refreshToken = null; + + + public CareLinkAuthentication getAuthentication() { + + //Not authenticated + if (this.authType == null || this.getAuthorizationFieldValue() == null) + return null; + + //Build authentication + Headers.Builder headers = new Headers.Builder(); + headers.add("Authorization", this.getAuthorizationFieldValue()); + if (this.authType == CareLinkAuthType.MobileApp) + headers.add("mag-identifier", this.magIdentifier); + return new CareLinkAuthentication(headers.build(), this.authType); + + } public String getToken() { return accessToken; @@ -23,12 +49,11 @@ public String getAuthorizationFieldValue() { return "Bearer " + this.getToken(); } - public long getExpiresIn() { - if (this.tokenValidTo == null) + public long getAccessExpiresIn() { + if (this.accessValidTo == null) return -1; else - return this.tokenValidTo.getTime() - Calendar.getInstance().getTime().getTime(); + return this.accessValidTo.getTime() - Calendar.getInstance().getTime().getTime(); } } - diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredentialStore.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredentialStore.java index 12a35a2..ef41bbb 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredentialStore.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CareLinkCredentialStore.java @@ -15,8 +15,9 @@ public class CareLinkCredentialStore { private static final String TAG = "CareLinkCredentialStore"; public final static int NOT_AUTHENTICATED = 0; - public final static int TOKEN_EXPIRED = 1; + public final static int ACCESS_EXPIRED = 1; public final static int AUTHENTICATED = 2; + public final static int REFRESH_EXPIRED = 3; private CareLinkCredential credential = null; private static CareLinkCredentialStore instance = null; @@ -37,7 +38,8 @@ public static CareLinkCredentialStore getInstance() { if (!credJson.equals("")) { try { CareLinkCredential savedCred = new GsonBuilder().create().fromJson(credJson, CareLinkCredential.class); - instance.setCredential(savedCred.country, savedCred.accessToken, savedCred.tokenValidTo, savedCred.cookies, false); + instance.setCredential(savedCred.country, savedCred.authType, savedCred.accessToken, savedCred.accessValidTo, savedCred.refreshValidTo, savedCred.cookies, + savedCred.androidModel, savedCred.deviceId, savedCred.clientId, savedCred.clientSecret, savedCred.magIdentifier, savedCred.refreshToken, false); } catch (Exception e) { UserError.Log.d(TAG, "Error when restoring saved Credential: " + e.getMessage()); } @@ -49,17 +51,38 @@ public static CareLinkCredentialStore getInstance() { return instance; } - synchronized void setCredential(String country, String accessToken, Date tokenValidTo, Cookie[] cookies) { - this.setCredential(country, accessToken, tokenValidTo, cookies, true); + synchronized void setMobileAppCredential(String country, String deviceId, String androidModel, String clientId, String clientSecret, String magIdentifier, String accessToken, String refreshToken, Date accessValidTo, Date refreshValidTo) { + this.setCredential(country, CareLinkAuthType.MobileApp, accessToken, accessValidTo, refreshValidTo, null, androidModel, deviceId, clientId, clientSecret, magIdentifier, refreshToken, true); } - protected synchronized void setCredential(String country, String accessToken, Date tokenValidTo, Cookie[] cookies, boolean save) { + synchronized void updateMobileAppCredential(String accessToken, Date accessValidTo, Date refreshValidTo, String refreshToken) { + this.setCredential(credential.country, CareLinkAuthType.MobileApp, accessToken, accessValidTo, refreshValidTo, null, credential.androidModel, credential.deviceId, credential.clientId, credential.clientSecret, credential.magIdentifier, refreshToken, true); + } + + synchronized void updateBrowserCredential(String accessToken, Date accessValidTo, Date refreshValidTo, Cookie[] cookies) { + this.setCredential(credential.country, CareLinkAuthType.Browser, accessToken, accessValidTo, refreshValidTo, cookies, null, null, null, null, null, null, true); + } + + synchronized void setBrowserCredential(String country, String accessToken, Date accessValidTo, Date refreshValidTo, Cookie[] cookies) { + this.setCredential(country, CareLinkAuthType.Browser, accessToken, accessValidTo, refreshValidTo, cookies, null, null, null, null, null, null, true); + } + + protected synchronized void setCredential(String country, CareLinkAuthType authType, String accessToken, Date accessValidTo, Date refreshValidTo, Cookie[] cookies, String androidModel, String deviceId, String clientId, String clientSecret, String magIdentifier, String refreshToken, boolean save) { + credential = new CareLinkCredential(); + credential.authType = authType; credential.country = country; credential.accessToken = accessToken; + credential.accessValidTo = accessValidTo; credential.cookies = cookies; - credential.tokenValidTo = tokenValidTo; - if (credential.accessToken == null || credential.tokenValidTo == null) + credential.androidModel = androidModel; + credential.deviceId = deviceId; + credential.clientId = clientId; + credential.clientSecret = clientSecret; + credential.magIdentifier = magIdentifier; + credential.refreshToken = refreshToken; + credential.refreshValidTo = refreshValidTo; + if (credential.accessToken == null || credential.accessValidTo == null) authStatus = NOT_AUTHENTICATED; else evaluateExpiration(); @@ -82,18 +105,32 @@ public int getAuthStatus() { return authStatus; } - public long getExpiresIn() { - if (credential == null || credential.tokenValidTo == null) + public long getAccessExpiresIn() { + if (credential == null || credential.accessValidTo == null) + return -1; + else + return credential.accessValidTo.getTime() - Calendar.getInstance().getTime().getTime(); + } + + public long getAccessExpiresOn() { + if (credential == null || credential.accessValidTo == null) + return -1; + else + return credential.accessValidTo.getTime(); + } + + public long getRefreshExpiresIn() { + if (credential == null || credential.refreshValidTo == null) return -1; else - return credential.tokenValidTo.getTime() - Calendar.getInstance().getTime().getTime(); + return credential.refreshValidTo.getTime() - Calendar.getInstance().getTime().getTime(); } - public long getExpiresOn() { - if (credential == null || credential.tokenValidTo == null) + public long getRefreshExpiresOn() { + if (credential == null || credential.refreshValidTo == null) return -1; else - return credential.tokenValidTo.getTime(); + return credential.refreshValidTo.getTime(); } synchronized void clear() { @@ -104,11 +141,12 @@ synchronized void clear() { } protected void evaluateExpiration() { - if (this.getExpiresIn() < 0) - authStatus = TOKEN_EXPIRED; + if (this.getRefreshExpiresIn() < 0) + authStatus = REFRESH_EXPIRED; + else if (this.getAccessExpiresIn() < 0) + authStatus = ACCESS_EXPIRED; else authStatus = AUTHENTICATED; } - } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CarePartnerAppConfig.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CarePartnerAppConfig.java new file mode 100644 index 0000000..939594d --- /dev/null +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/auth/CarePartnerAppConfig.java @@ -0,0 +1,92 @@ +package com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +public class CarePartnerAppConfig { + + public JsonObject regionConfig = null; + public JsonObject ssoConfig = null; + + public String getRegion() { + return regionConfig.get("region").getAsString(); + } + + public String getSSOConfigUrl() { + return regionConfig.get("SSOConfiguration").getAsString(); + } + + public String getCloudBaseUrl() { + return regionConfig.get("baseUrlCumulus").getAsString(); + } + + public String getCareLinkBaseUrl() { + return regionConfig.get("baseUrlCareLink").getAsString(); + } + + public String getSSOServerHost() { + return this.getChildJsonString(ssoConfig, "server.hostname"); + } + + public String getSSOServerPrefix() { + return this.getChildJsonString(ssoConfig, "server.prefix"); + } + + public int getSSOServerPort() { + return this.getChildJsonElement(ssoConfig, "server.port").getAsInt(); + } + + public String getOAuthAuthEndpoint() { + return this.getChildJsonString(ssoConfig, "oauth.system_endpoints.authorization_endpoint_path").substring(1); + } + + public String getOAuthTokenEndpoint() { + return this.getChildJsonString(ssoConfig, "oauth.system_endpoints.token_endpoint_path").substring(1); + } + + public String getMagCredentialInitEndpoint() { + return this.getChildJsonString(ssoConfig, "mag.system_endpoints.client_credential_init_endpoint_path").substring(1); + } + + public String getMagDeviceRegisterEndpoint() { + return this.getChildJsonString(ssoConfig, "mag.system_endpoints.device_register_endpoint_path").substring(1); + } + + public String getClientId() { + return getClientMemberString("client_id"); + } + + public String getOAuthScope() { + return getClientMemberString("scope"); + } + + public String getOAuthRedirectUri() { + return getClientMemberString("redirect_uri"); + } + + public int getRefreshLifetimeSec() { + return Integer.parseInt(getClientMemberString("client_key_custom.lifetimes.oauth2_refresh_token_lifetime_sec")); + } + + private String getClientMemberString(String clientMember) { + return this.getChildJsonString(this.getChildJsonElement(ssoConfig, "oauth.client.client_ids").getAsJsonArray().get(0) + .getAsJsonObject(), clientMember); + } + + private String getChildJsonString(JsonObject parent, String path) { + return getChildJsonElement(parent, path).getAsString(); + } + + private JsonElement getChildJsonElement(JsonObject parent, String path) { + + JsonElement obj = parent; + + for (String member : path.split("\\.")) { + obj = obj.getAsJsonObject().get(member); + } + + return obj; + + } + +} diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/client/CareLinkClient.java b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/client/CareLinkClient.java index c7a423d..ba1fdb8 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/client/CareLinkClient.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/cgm/carelinkfollow/client/CareLinkClient.java @@ -1,12 +1,15 @@ package com.eveningoutpost.dexdrip.cgm.carelinkfollow.client; import com.eveningoutpost.dexdrip.Models.JoH; +import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkAuthType; +import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkAuthentication; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkCredentialStore; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.EditableCookieJar; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.message.*; + import okhttp3.*; import java.io.IOException; @@ -23,7 +26,6 @@ public class CareLinkClient { protected static final String CARELINK_CONNECT_SERVER_EU = "carelink.minimed.eu"; protected static final String CARELINK_CONNECT_SERVER_US = "carelink.minimed.com"; protected static final String CARELINK_LANGUAGE_EN = "en"; - protected static final String CARELINK_LOCALE_EN = "en"; protected static final String CARELINK_AUTH_TOKEN_COOKIE_NAME = "auth_tmp_token"; protected static final String CARELINK_TOKEN_VALIDTO_COOKIE_NAME = "c_token_valid_to"; protected static final int AUTH_EXPIRE_DEADLINE_MINUTES = 1; @@ -44,36 +46,51 @@ public class CareLinkClient { protected CareLinkCredentialStore credentialStore; //Session info - protected boolean sessionInfosLoaded = false; + protected boolean sessionInfosLoaded = false; protected User sessionUser; + public User getSessionUser() { return sessionUser; } + protected Profile sessionProfile; + public Profile getSessionProfile() { return sessionProfile; } + protected CountrySettings sessionCountrySettings; + public CountrySettings getSessionCountrySettings() { return sessionCountrySettings; } + protected RecentUploads sessionRecentUploads; + public RecentUploads getSessionRecentUploads() { return sessionRecentUploads; } + protected Boolean sessionDeviceIsBle; + public Boolean getSessionDeviceIsBle() { return sessionDeviceIsBle; } + protected Boolean sessionM2MEnabled; - public boolean getSessionM2MEnabled(){ + + public boolean getSessionM2MEnabled() { return sessionM2MEnabled; } + protected Patient[] sessionPatients; - public Patient[] getSessionPatients(){ + + public Patient[] getSessionPatients() { return sessionPatients; } + protected MonitorData sessionMonitorData; + public MonitorData getSessionMonitorData() { return sessionMonitorData; } @@ -83,22 +100,31 @@ public MonitorData getSessionMonitorData() { protected boolean loginInProcess = false; protected boolean collectingSessionInfos = false; protected int lastResponseCode; + public int getLastResponseCode() { return lastResponseCode; } + protected boolean lastLoginSuccess; + public boolean getLastLoginSuccess() { return lastLoginSuccess; } + protected boolean lastDataSuccess; + public boolean getLastDataSuccess() { return lastDataSuccess; } + protected String lastErrorMessage = ""; + public String getLastErrorMessage() { return lastErrorMessage; } + protected String lastStackTraceString; + public String getLastStackTraceString() { return lastStackTraceString; } @@ -131,7 +157,10 @@ private void createHttpClient(){ EditableCookieJar cookieJar = null; cookieJar = new EditableCookieJar(); - cookieJar.AddCookies(this.credentialStore.getCredential().cookies); + //Add cookies if there are any + if(this.credentialStore.getCredential().cookies != null && this.credentialStore.getCredential().cookies.length > 0) { + cookieJar.AddCookies(this.credentialStore.getCredential().cookies); + } this.httpClient = new OkHttpClient.Builder() .cookieJar(cookieJar) @@ -142,17 +171,19 @@ private void createHttpClient(){ /* * WRAPPER DATA RETRIEVAL METHODS */ + //Wrapper for common request of recent data (last 24 hours) public RecentData getRecentData() { - //Use default patient username + //Use default patient username if not provided return this.getRecentData(this.getDefaultPatientUsername()); } + //Get recent data of patient public RecentData getRecentData(String patientUsername) { // Force login to get basic info - if (getAuthorizationToken() == null) + if (getAuthentication() == null) return null; // 7xxG @@ -168,10 +199,11 @@ else if (this.sessionM2MEnabled) } + //Determine default patient public String getDefaultPatientUsername() { // Force login to get basic info - if (getAuthorizationToken() == null) + if (getAuthentication() == null) return null; // Care Partner + multi follow => first patient @@ -180,7 +212,7 @@ public String getDefaultPatientUsername() { return this.sessionPatients[0].username; else return null; - // Not care partner or no multi follow => username from session profile + // Not care partner or no multi follow => username from session profile else if (this.sessionProfile.username != null) return this.sessionProfile.username; else @@ -196,10 +228,10 @@ public boolean isBleDevice(String patientUsername){ return sessionDeviceIsBle; // Force login to get basic info - if(getAuthorizationToken() == null) + if(getAuthentication() == null) return false; - // Determine session device by recent uploads + // Patient: device from recent uploads if possible if(!this.sessionUser.isCarePartner()){ recentUploadBle = this.isRecentUploadBle(); if(recentUploadBle != null){ @@ -208,6 +240,7 @@ public boolean isBleDevice(String patientUsername){ } } + // Care partner (+M2M): device from patient list if(this.sessionM2MEnabled && this.sessionUser.isCarePartner()) if(patientUsername == null || this.sessionPatients == null) return false; @@ -218,6 +251,7 @@ public boolean isBleDevice(String patientUsername){ } return false; } + // Other: classic method (session monitor data) else return this.sessionMonitorData.isBle(); @@ -276,7 +310,7 @@ protected boolean executeLoginProcedure() { consentResponse.close(); // Get required sessions infos - if(this.getSessionInfos()) + if (this.getSessionInfos()) lastLoginSuccess = true; } catch (Exception e) { @@ -365,7 +399,7 @@ protected Response getLoginSession() throws IOException { requestBuilder = new Request.Builder().url(url); - this.addHttpHeaders(requestBuilder, RequestType.HtmlGet); + this.addHttpHeaders(requestBuilder, RequestType.HtmlGet, true); return this.httpClient.newCall(requestBuilder.build()).execute(); @@ -399,7 +433,7 @@ protected Response doLogin(Response loginSessionResponse) throws IOException { .url(url) .post(form); - this.addHttpHeaders(requestBuilder, RequestType.HtmlGet); + this.addHttpHeaders(requestBuilder, RequestType.HtmlGet, true); return this.httpClient.newCall(requestBuilder.build()).execute(); @@ -427,14 +461,15 @@ protected Response doConsent(Response doLoginResponse) throws IOException { requestBuilder = new Request.Builder().url(consentUrl).post(form); - this.addHttpHeaders(requestBuilder, RequestType.HtmlPost); + this.addHttpHeaders(requestBuilder, RequestType.HtmlPost, true); return this.httpClient.newCall(requestBuilder.build()).execute(); } - protected String getAuthorizationToken() { + protected CareLinkAuthentication getAuthentication() { + // CredentialStore is used if(this.credentialStore != null){ if(!this.sessionInfosLoaded && this.credentialStore.getAuthStatus() == CareLinkCredentialStore.AUTHENTICATED && !this.collectingSessionInfos) { @@ -443,13 +478,11 @@ protected String getAuthorizationToken() { if(!this.collectingSessionInfos && !this.sessionInfosLoaded) return null; else - return this.credentialStore.getCredential().getAuthorizationFieldValue(); - } else { - + return this.credentialStore.getCredential().getAuthentication(); // New token is needed: // a) no token or about to expire => execute authentication // b) last response 401 - // c) last login failed after login process completed + } else { if (!((SimpleOkHttpCookieJar) httpClient.cookieJar()).contains(CARELINK_AUTH_TOKEN_COOKIE_NAME) || !((SimpleOkHttpCookieJar) httpClient.cookieJar()).contains(CARELINK_TOKEN_VALIDTO_COOKIE_NAME) || !((new Date(Date.parse(((SimpleOkHttpCookieJar) httpClient.cookieJar()) @@ -464,10 +497,14 @@ protected String getAuthorizationToken() { return null; } - // there can be only one - return "Bearer" + " " + ((SimpleOkHttpCookieJar) httpClient.cookieJar()).getCookies(CARELINK_AUTH_TOKEN_COOKIE_NAME).get(0).value(); + //there can be only one auth cookie + return new CareLinkAuthentication( + new Headers.Builder().add("Authorization", "Bearer" + " " + ((SimpleOkHttpCookieJar) httpClient.cookieJar()).getCookies(CARELINK_AUTH_TOKEN_COOKIE_NAME).get(0).value()).build(), + CareLinkAuthType.Browser); + //return "Bearer" + " " + ((SimpleOkHttpCookieJar) httpClient.cookieJar()).getCookies(CARELINK_AUTH_TOKEN_COOKIE_NAME).get(0).value(); } + } /* @@ -539,7 +576,7 @@ public RecentData getLast24Hours() { recentData = this.getData(this.careLinkServer(), "patient/connect/data", queryParams, null, RecentData.class); if (recentData != null) correctTimeInRecentData(recentData); - }catch (Exception e){ + } catch (Exception e) { lastErrorMessage = e.getClass().getSimpleName() + ":" + e.getMessage(); } @@ -559,7 +596,7 @@ public RecentData getConnectDisplayMessage(String username, String role, String userJson = new JsonObject(); userJson.addProperty("username", username); userJson.addProperty("role", role); - if(!JoH.emptyString(patientUsername)) + if (!JoH.emptyString(patientUsername)) userJson.addProperty("patientId", patientUsername); gson = new GsonBuilder().create(); @@ -570,7 +607,7 @@ public RecentData getConnectDisplayMessage(String username, String role, String recentData = this.getData(HttpUrl.parse(endpointUrl), requestBody, RecentData.class); if (recentData != null) correctTimeInRecentData(recentData); - }catch (Exception e){ + } catch (Exception e) { lastErrorMessage = e.getClass().getSimpleName() + ":" + e.getMessage(); } return recentData; @@ -583,7 +620,7 @@ public RecentData getM2MPatientData(String patientUsername) { Map queryParams = null; //Patient username is mandantory! - if(patientUsername == null || patientUsername.isEmpty()) + if (patientUsername == null || patientUsername.isEmpty()) return null; queryParams = new HashMap(); @@ -615,17 +652,16 @@ protected String extractResponseData(String respBody, String groupRegex, int gro } // Http header builder for requests - protected void addHttpHeaders(Request.Builder requestBuilder, RequestType type) { + protected void addHttpHeaders(Request.Builder requestBuilder, RequestType type, boolean isBrowserClient) { //Add common browser headers - requestBuilder - .addHeader("Accept-Language", "en;q=0.9, *;q=0.8") - .addHeader("Connection", "keep-alive") - //.addHeader("Sec-Ch-Ua", "\"Not/A)Brand\";v=\"99\", \"Google Chrome\";v=\"115\", \"Chromium\";v=\"115\"") - .addHeader("Sec-Ch-Ua", "\"Google Chrome\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\"") - .addHeader("User-Agent", "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36") - //.addHeader("Connection", "keep-alive"); - ; + if(isBrowserClient) { + requestBuilder + .addHeader("Accept-Language", "en;q=0.9, *;q=0.8") + .addHeader("Connection", "keep-alive") + .addHeader("Sec-Ch-Ua", "\"Google Chrome\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\"") + .addHeader("User-Agent", "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36"); + } //Set media type based on request type switch (type) { @@ -634,45 +670,44 @@ protected void addHttpHeaders(Request.Builder requestBuilder, RequestType type) requestBuilder.addHeader("Content-Type", "application/json; charset=utf-8"); break; case HtmlGet: - requestBuilder.addHeader("Accept", - "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"); - break; + requestBuilder.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"); case HtmlPost: - requestBuilder.addHeader("Accept", - "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"); + requestBuilder.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"); requestBuilder.addHeader("Content-Type", "application/x-www-form-urlencoded"); break; } } - // Data request for API calls + // General data request for API calls protected T getData(HttpUrl url, RequestBody requestBody, Class dataClass) { Request.Builder requestBuilder = null; HttpUrl.Builder urlBuilder = null; - String authToken = null; + CareLinkAuthentication authentication = null; String responseString = null; Response response = null; Object data = null; + boolean isBrowserClient = true; this.lastDataSuccess = false; this.lastErrorMessage = ""; - // Get auth token - authToken = this.getAuthorizationToken(); + // Get authentication + authentication = this.getAuthentication(); - if (authToken != null) { + if (authentication != null) { // Create request for URL with authToken - requestBuilder = new Request.Builder().url(url).addHeader("Authorization", authToken); + //requestBuilder = new Request.Builder().url(url).addHeader("Authorization", authToken); + requestBuilder = new Request.Builder().url(url).headers(authentication.getHeaders()); - // Add header + // Add additional headers if (requestBody == null) { - this.addHttpHeaders(requestBuilder, RequestType.Json); + this.addHttpHeaders(requestBuilder, RequestType.Json, authentication.authType == CareLinkAuthType.Browser); } else { requestBuilder.post(requestBody); - this.addHttpHeaders(requestBuilder, RequestType.HtmlPost); + this.addHttpHeaders(requestBuilder, RequestType.HtmlPost, authentication.authType == CareLinkAuthType.Browser); } // Send request @@ -684,7 +719,7 @@ protected T getData(HttpUrl url, RequestBody requestBody, Class dataClass responseString = response.body().string(); data = new GsonBuilder().create().fromJson(responseString, dataClass); this.lastDataSuccess = true; - } catch (Exception e){ + } catch (Exception e) { lastErrorMessage = e.getClass().getSimpleName() + ":" + e.getMessage(); } } @@ -696,7 +731,7 @@ protected T getData(HttpUrl url, RequestBody requestBody, Class dataClass } //Return result - if(data != null) + if (data != null) return dataClass.cast(data); else return null; @@ -723,27 +758,27 @@ protected T getData(String host, String path, Map queryParam } - protected void correctTimeInRecentData(RecentData recentData){ + protected void correctTimeInRecentData(RecentData recentData) { boolean timezoneMissing = false; String offsetString = null; - if(recentData.sMedicalDeviceTime != null && !recentData.sMedicalDeviceTime.isEmpty() && recentData.lastMedicalDeviceDataUpdateServerTime > 1) { + if (recentData.sMedicalDeviceTime != null && !recentData.sMedicalDeviceTime.isEmpty() && recentData.lastMedicalDeviceDataUpdateServerTime > 1) { //MedicalDeviceTime string has no timezone information - if(parseDateString(recentData.sMedicalDeviceTime) == null) { + if (parseDateString(recentData.sMedicalDeviceTime) == null) { timezoneMissing = true; //offset = this.getZonedDate(recentData.lastSG.datetime).getOffset(); //Try get TZ offset string: lastSG or lastAlarm - if(recentData.lastSG != null && recentData.lastSG.datetime != null) + if (recentData.lastSG != null && recentData.lastSG.datetime != null) offsetString = this.getZoneOffset(recentData.lastSG.datetime); else offsetString = this.getZoneOffset(recentData.lastAlarm.datetime); - if(recentData.lastAlarm != null && recentData.lastAlarm.datetime != null) + if (recentData.lastAlarm != null && recentData.lastAlarm.datetime != null) recentData.lastAlarm.datetimeAsDate = parseDateString(recentData.lastAlarm.datetime); //Build correct dates with timezone recentData.sMedicalDeviceTime = recentData.sMedicalDeviceTime + offsetString; @@ -769,7 +804,7 @@ protected void correctTimeInRecentData(RecentData recentData){ } //Timezone was present => check if time needs correction - if(!timezoneMissing) { + if (!timezoneMissing) { //Calc time diff between event time and actual local time int diffInHour = (int) Math.round(((recentData.lastMedicalDeviceDataUpdateServerTime - recentData.dMedicalDeviceTime.getTime()) / 3600000D)); @@ -828,41 +863,42 @@ protected void correctTimeInRecentData(RecentData recentData){ } - protected String getZoneOffset(String dateString){ + protected String getZoneOffset(String dateString) { Matcher offsetDataMatcher = Pattern.compile(("(.*)([\\+|-].*)")).matcher(dateString); - if(offsetDataMatcher.find()) + if (offsetDataMatcher.find()) return offsetDataMatcher.group(2); else - return null; + return null; } - protected Date parseDateString(String dateString){ - for(SimpleDateFormat zonedFormat : ZONED_DATE_FORMATS){ + protected Date parseDateString(String dateString) { + for (SimpleDateFormat zonedFormat : ZONED_DATE_FORMATS) { try { return zonedFormat.parse(dateString); - }catch (Exception ex){} + } catch (Exception ex) { + } } - return null; + return null; } - protected Date shiftDateByHours(Date date, int hours){ - if(date != null) { + protected Date shiftDateByHours(Date date, int hours) { + if (date != null) { Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.add(Calendar.HOUR_OF_DAY, hours); return calendar.getTime(); } else { - return null; + return null; } } //Calculate DateTime using graph index (1 index = 5 minute) - protected static Date calcTimeByIndex(Date lastSensorTime, int index, boolean round){ - if(lastSensorTime == null) + protected static Date calcTimeByIndex(Date lastSensorTime, int index, boolean round) { + if (lastSensorTime == null) return null; - else if(round) + else if (round) //round to 10 minutes - return new Date((Math.round((calcTimeByIndex(lastSensorTime,index,false).getTime()) / 600_000D) * 600_000L)); + return new Date((Math.round((calcTimeByIndex(lastSensorTime, index, false).getTime()) / 600_000D) * 600_000L)); else return new Date((lastSensorTime.getTime() - ((287 - index) * 300_000L))); } diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java b/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java index 4ef8898..ac46c19 100644 --- a/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java +++ b/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java @@ -87,6 +87,7 @@ import com.eveningoutpost.dexdrip.WidgetUpdateService; import com.eveningoutpost.dexdrip.calibrations.PluggableCalibration; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.CareLinkFollowService; +import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkAuthType; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkAuthenticator; import com.eveningoutpost.dexdrip.cgm.carelinkfollow.auth.CareLinkCredentialStore; import com.eveningoutpost.dexdrip.cgm.nsfollow.NightscoutFollow; @@ -1196,6 +1197,7 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { final Preference carelinkFollowDownloadBoluses = findPreference("clfollow_download_boluses"); final Preference carelinkFollowDownloadMeals = findPreference("clfollow_download_meals"); final Preference carelinkFollowDownloadNotifications = findPreference("clfollow_download_notifications"); + //final Preference carelinkFollowDownloadBasals = findPreference("clfollow_download_basals"); if (collectionType == DexCollectionType.CLFollow) { //Add CL prefs //collectionCategory.addPreference(carelinkFollowUser); @@ -1209,6 +1211,7 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { collectionCategory.addPreference(carelinkFollowDownloadBoluses); collectionCategory.addPreference(carelinkFollowDownloadMeals); collectionCategory.addPreference(carelinkFollowDownloadNotifications); + //collectionCategory.addPreference(carelinkFollowDownloadBasals); //Create prefChange handler final Preference.OnPreferenceChangeListener carelinkFollowListener = new Preference.OnPreferenceChangeListener() { @Override @@ -1218,6 +1221,7 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { return true; } }; + //Pref click handler for Login final Preference.OnPreferenceClickListener carelinkLoginListener = new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { @@ -1225,17 +1229,18 @@ public boolean onPreferenceClick(Preference preference) { public void run() { try { String country = Pref.getString("clfollow_country", "").toLowerCase(); - if (country.equals("")) - JoH.static_toast(preference.getContext(), "Country is required!", Toast.LENGTH_LONG); - else { + if (country.equals("")) { + JoH.static_toast(preference.getContext(), xdrip.gs(R.string.carelink_auth_country_required), Toast.LENGTH_LONG); + } else { CareLinkAuthenticator authenticator = new CareLinkAuthenticator(country, CareLinkCredentialStore.getInstance()); - if (authenticator.authenticate(getActivity())) { - JoH.static_toast(preference.getContext(), "Authenticated!", Toast.LENGTH_LONG); + if (authenticator.authenticate(getActivity(), CareLinkAuthType.MobileApp)) { + JoH.static_toast(preference.getContext(), xdrip.gs(R.string.carelink_auth_status_authenticated), Toast.LENGTH_LONG); CareLinkFollowService.resetInstanceAndInvalidateSession(); CollectionServiceStarter.restartCollectionServiceBackground(); } - else - JoH.static_toast(preference.getContext(), "Not authenticated!", Toast.LENGTH_LONG); + else { + JoH.static_toast(preference.getContext(), xdrip.gs(R.string.carelink_auth_status_not_authenticated), Toast.LENGTH_LONG); + } } } catch (InterruptedException e) { @@ -1276,6 +1281,7 @@ public void run() { collectionCategory.removePreference(carelinkFollowDownloadBoluses); collectionCategory.removePreference(carelinkFollowDownloadMeals); collectionCategory.removePreference(carelinkFollowDownloadNotifications); + //collectionCategory.removePreference(carelinkFollowDownloadBasals); } catch (Exception e) { // } @@ -1615,6 +1621,7 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { collectionCategory.removePreference(carelinkFollowDownloadBoluses); collectionCategory.removePreference(carelinkFollowDownloadMeals); collectionCategory.removePreference(carelinkFollowDownloadNotifications); + //collectionCategory.removePreference(carelinkFollowDownloadBasals); } catch (Exception e) { // } diff --git a/app/src/main/res/values-hu/strings-hu.xml b/app/src/main/res/values-hu/strings-hu.xml index d9456e6..d214683 100644 --- a/app/src/main/res/values-hu/strings-hu.xml +++ b/app/src/main/res/values-hu/strings-hu.xml @@ -1269,6 +1269,34 @@ Nightscout követő URL A kezeléseket is töltse le Nightscout követőként Kezelések letöltése + + CareLink ország + Válassza ki az országát + Követett beteg felhasználóneve + Követett betegfiók felhasználóneve (opcionális) + + Bejelentkezés + Bejelentkezés böngészőben + Türelmi idő + Várakozási idő adatfeltöltésre (s) + Várakozási idő adatkimaradnásnál + Adatlekérdezés-ismétlések közötti várakozási idő adatkimaradásnál + VC mérés vérből + Ujjbegy VC értékek letöltése CareLink-ből + Bólusok + Bólus adatok letöltése CareLink-ből + Étkezések + Étkezés adatok letöltése CareLink-ből + Értesítések + Értesítések letöltése CareLink-ből OB1 G5/G6 kollektor beállítások Teljesen újraír kód. Működnie kell a 4.4 - 9-es Android verziókon. Támogatja a natív módot és még sok mást is OB1 kollektor használata @@ -1631,4 +1659,15 @@ Biztonsági mentés Biztonsági mentés kész Biztonsági mentés meghiúsult + Ország kötelező! + Azonosított állapot! + Nincs azonosítva! + Belépés folyamatban... + Hozzáférés megújítása + Hozzáférés megújítása sikertelen! Majd újrapróbálja! + Adatletöltés indítása + Adatletöltés sikertelen! + Nincs belépve! Jelentkezzen be! + Hozzáférés lejárt! Meg fogja újítani! + Belépés lejárt! Jelentkezzen be! \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fb7b5d..a22ad0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1634,4 +1634,15 @@ Don\'t ask me that again Connecting libre sensor to bluetooth Do you want to connect xDrip with this sensor? This will stop libre reader and libre link from receiving alerts. Press yes and scan again to connect (choose don\'t connect if you want to run xDrip with the patched app) + Country is required! + Authenticated status! + Not authenticated status! + Login in progress... + Start access renewal + Access renewal failed! Will try again! + Start download + Download data failed! + Not logged in! Please log in! + Access expired! Will refresh! + Login expired! Please log in! diff --git a/wear/build.gradle b/wear/build.gradle index 1d04249..439c1c8 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -9,7 +9,7 @@ android { // abortOnError false } defaultConfig { - applicationId "com.eveningoutpost.dexdripva4" + applicationId "com.eveningoutpost.dexdrip" minSdkVersion 21 //noinspection ExpiredTargetSdkVersion targetSdkVersion 23