Update app icon, use AppAuth library for Oauth (instead of Play Services), fix vibrate/silent feature on pre-6.0
parent
5b73d71e86
commit
840a5fd039
@ -0,0 +1,173 @@
|
||||
package sh.ftp.rocketninelabs.meditationassistant;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import net.openid.appauth.connectivity.ConnectionBuilder;
|
||||
import net.openid.appauth.connectivity.DefaultConnectionBuilder;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Reads and validates the demo app configuration from `res/raw/auth_config.json`. Configuration
|
||||
* changes are detected by comparing the hash of the last known configuration to the read
|
||||
* configuration. When a configuration change is detected, the app state is reset.
|
||||
*/
|
||||
public final class AuthConfiguration {
|
||||
|
||||
private static final String TAG = "Configuration";
|
||||
|
||||
private static final String PREFS_NAME = "config";
|
||||
private static final String KEY_LAST_HASH = "lastHash";
|
||||
|
||||
private static WeakReference<AuthConfiguration> sInstance = new WeakReference<>(null);
|
||||
|
||||
private final Context mContext;
|
||||
private final SharedPreferences mPrefs;
|
||||
private final Resources mResources;
|
||||
|
||||
private JSONObject mConfigJson;
|
||||
private String mConfigError;
|
||||
|
||||
private String mClientId;
|
||||
private String mScope;
|
||||
private Uri mRedirectUri;
|
||||
private Uri mDiscoveryUri;
|
||||
private Uri mAuthEndpointUri;
|
||||
private Uri mTokenEndpointUri;
|
||||
private Uri mRegistrationEndpointUri;
|
||||
|
||||
public static AuthConfiguration getInstance(Context context) {
|
||||
AuthConfiguration config = sInstance.get();
|
||||
if (config == null) {
|
||||
config = new AuthConfiguration(context);
|
||||
sInstance = new WeakReference<AuthConfiguration>(config);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public AuthConfiguration(Context context) {
|
||||
mContext = context;
|
||||
mPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
mResources = context.getResources();
|
||||
|
||||
try {
|
||||
readConfiguration();
|
||||
} catch (InvalidConfigurationException ex) {
|
||||
mConfigError = ex.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the current configuration is valid.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return mConfigError == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a description of the configuration error, if the configuration is invalid.
|
||||
*/
|
||||
@Nullable
|
||||
public String getConfigurationError() {
|
||||
return mConfigError;
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
public String getClientId() {
|
||||
return mClientId;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getScope() {
|
||||
return mScope;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Uri getRedirectUri() {
|
||||
return mRedirectUri;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getDiscoveryUri() {
|
||||
return mDiscoveryUri;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getAuthEndpointUri() {
|
||||
return mAuthEndpointUri;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getTokenEndpointUri() {
|
||||
return mTokenEndpointUri;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getRegistrationEndpointUri() {
|
||||
return mRegistrationEndpointUri;
|
||||
}
|
||||
|
||||
public ConnectionBuilder getConnectionBuilder() {
|
||||
return DefaultConnectionBuilder.INSTANCE;
|
||||
}
|
||||
|
||||
private void readConfiguration() throws InvalidConfigurationException {
|
||||
mClientId = ""; // TODO: blank?
|
||||
mScope = "openid email profile";
|
||||
mRedirectUri = Uri.parse("https://medinet.rocketnine.space/oauth");
|
||||
|
||||
if (!isRedirectUriRegistered()) {
|
||||
throw new InvalidConfigurationException(
|
||||
"redirect_uri is not handled by any activity in this app! "
|
||||
+ "Ensure that the appAuthRedirectScheme in your build.gradle file "
|
||||
+ "is correctly configured, or that an appropriate intent filter "
|
||||
+ "exists in your app manifest.");
|
||||
}
|
||||
|
||||
/*if (getConfigString("discovery_uri") == null) {
|
||||
mAuthEndpointUri = getRequiredConfigWebUri("authorization_endpoint_uri");
|
||||
|
||||
mTokenEndpointUri = getRequiredConfigWebUri("token_endpoint_uri");
|
||||
|
||||
if (mClientId == null) {
|
||||
mRegistrationEndpointUri = getRequiredConfigWebUri("registration_endpoint_uri");
|
||||
}
|
||||
} else {
|
||||
mDiscoveryUri = getRequiredConfigWebUri("discovery_uri");
|
||||
}*/
|
||||
mDiscoveryUri = Uri.parse("");
|
||||
}
|
||||
|
||||
private boolean isRedirectUriRegistered() {
|
||||
// ensure that the redirect URI declared in the configuration is handled by some activity
|
||||
// in the app, by querying the package manager speculatively
|
||||
Intent redirectIntent = new Intent();
|
||||
redirectIntent.setPackage(mContext.getPackageName());
|
||||
redirectIntent.setAction(Intent.ACTION_VIEW);
|
||||
redirectIntent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
redirectIntent.setData(mRedirectUri);
|
||||
|
||||
return !mContext.getPackageManager().queryIntentActivities(redirectIntent, 0).isEmpty();
|
||||
}
|
||||
|
||||
public static final class InvalidConfigurationException extends Exception {
|
||||
InvalidConfigurationException(String reason) {
|
||||
super(reason);
|
||||
}
|
||||
|
||||
InvalidConfigurationException(String reason, Throwable cause) {
|
||||
super(reason, cause);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
package sh.ftp.rocketninelabs.meditationassistant;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.MainThread;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import net.openid.appauth.AppAuthConfiguration;
|
||||
import net.openid.appauth.AuthState;
|
||||
import net.openid.appauth.AuthorizationException;
|
||||
import net.openid.appauth.AuthorizationResponse;
|
||||
import net.openid.appauth.AuthorizationService;
|
||||
import net.openid.appauth.AuthorizationServiceDiscovery;
|
||||
import net.openid.appauth.ClientAuthentication;
|
||||
import net.openid.appauth.TokenRequest;
|
||||
import net.openid.appauth.TokenResponse;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import okio.Okio;
|
||||
|
||||
/**
|
||||
* Displays the authorized state of the user. This activity is provided with the outcome of the
|
||||
* authorization flow, which it uses to negotiate the final authorized state,
|
||||
* by performing an authorization code exchange if necessary. After this, the activity provides
|
||||
* additional post-authorization operations if available, such as fetching user info and refreshing
|
||||
* access tokens.
|
||||
*/
|
||||
public class AuthResultActivity extends Activity {
|
||||
MeditationAssistant ma = null;
|
||||
|
||||
private static final String KEY_USER_INFO = "userInfo";
|
||||
|
||||
private AuthorizationService mAuthService;
|
||||
private AuthStateManager mStateManager;
|
||||
private final AtomicReference<JSONObject> mUserInfoJson = new AtomicReference<>();
|
||||
private ExecutorService mExecutor;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mStateManager = AuthStateManager.getInstance(this);
|
||||
mExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
AuthConfiguration config = AuthConfiguration.getInstance(this);
|
||||
|
||||
mAuthService = new AuthorizationService(
|
||||
this,
|
||||
new AppAuthConfiguration.Builder()
|
||||
.setConnectionBuilder(config.getConnectionBuilder())
|
||||
.build());
|
||||
|
||||
displayLoading("Restoring state...");
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
try {
|
||||
mUserInfoJson.set(new JSONObject(savedInstanceState.getString(KEY_USER_INFO)));
|
||||
} catch (JSONException ex) {
|
||||
Log.e("MA", "Failed to parse saved user info JSON, discarding", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
if (mExecutor.isShutdown()) {
|
||||
mExecutor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
if (mStateManager.getCurrent().isAuthorized()) {
|
||||
updateState();
|
||||
return;
|
||||
}
|
||||
|
||||
// the stored AuthState is incomplete, so check if we are currently receiving the result of
|
||||
// the authorization flow from the browser.
|
||||
AuthorizationResponse response = AuthorizationResponse.fromIntent(getIntent());
|
||||
AuthorizationException ex = AuthorizationException.fromIntent(getIntent());
|
||||
|
||||
if (response != null || ex != null) {
|
||||
mStateManager.updateAfterAuthorization(response, ex);
|
||||
}
|
||||
|
||||
if (response != null && response.authorizationCode != null) {
|
||||
// authorization code exchange is required
|
||||
mStateManager.updateAfterAuthorization(response, ex);
|
||||
exchangeAuthorizationCode(response);
|
||||
} else if (ex != null) {
|
||||
// TODO: handle failure "Authorization flow failed: " + ex.getMessage());
|
||||
} else {
|
||||
// TODO: handle failure No authorization state retained - reauthorization required");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle state) {
|
||||
// user info is retained to survive activity restarts, such as when rotating the
|
||||
// device or switching apps. This isn't essential, but it helps provide a less
|
||||
// jarring UX when these events occur - data does not just disappear from the view.
|
||||
if (mUserInfoJson.get() != null) {
|
||||
state.putString(KEY_USER_INFO, mUserInfoJson.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
mAuthService.dispose();
|
||||
mExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void displayLoading(String message) {
|
||||
Log.d("MA", "Auth: " + message);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void updateState() {
|
||||
AuthState state = mStateManager.getCurrent();
|
||||
|
||||
if (state.getAccessToken() == null) {
|
||||
Log.d("MA", "Access token was null: ");
|
||||
// TODO: Handle auth failure, prompt to retry
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
Long expiresAt = state.getAccessTokenExpirationTime();
|
||||
if (expiresAt != null && expiresAt < System.currentTimeMillis()) {
|
||||
refreshAccessToken();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d("MA", "Got token");
|
||||
getMeditationAssistant().getMediNET().signInWithAuthToken(state.getAccessToken());
|
||||
finish();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void refreshAccessToken() {
|
||||
displayLoading("Refreshing access token");
|
||||
performTokenRequest(
|
||||
mStateManager.getCurrent().createTokenRefreshRequest(),
|
||||
this::handleAccessTokenResponse);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void exchangeAuthorizationCode(AuthorizationResponse authorizationResponse) {
|
||||
displayLoading("Exchanging authorization code");
|
||||
performTokenRequest(
|
||||
authorizationResponse.createTokenExchangeRequest(),
|
||||
this::handleCodeExchangeResponse);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void performTokenRequest(
|
||||
TokenRequest request,
|
||||
AuthorizationService.TokenResponseCallback callback) {
|
||||
ClientAuthentication clientAuthentication;
|
||||
try {
|
||||
clientAuthentication = mStateManager.getCurrent().getClientAuthentication();
|
||||
} catch (ClientAuthentication.UnsupportedAuthenticationMethod ex) {
|
||||
Log.d("MA", "Token request cannot be made, client authentication for the token "
|
||||
+ "endpoint could not be constructed (%s)", ex);
|
||||
// TODO handle failure Client authentication method is unsupported");
|
||||
return;
|
||||
}
|
||||
|
||||
mAuthService.performTokenRequest(
|
||||
request,
|
||||
clientAuthentication,
|
||||
callback);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void handleAccessTokenResponse(
|
||||
@Nullable TokenResponse tokenResponse,
|
||||
@Nullable AuthorizationException authException) {
|
||||
mStateManager.updateAfterTokenResponse(tokenResponse, authException);
|
||||
runOnUiThread(this::updateState);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void handleCodeExchangeResponse(
|
||||
@Nullable TokenResponse tokenResponse,
|
||||
@Nullable AuthorizationException authException) {
|
||||
|
||||
mStateManager.updateAfterTokenResponse(tokenResponse, authException);
|
||||
if (!mStateManager.getCurrent().isAuthorized()) {
|
||||
final String message = "Authorization Code exchange failed"
|
||||
+ ((authException != null) ? authException.error : "");
|
||||
|
||||
// WrongThread inference is incorrect for lambdas
|
||||
//noinspection WrongThread
|
||||
// TODO: Handle not authorized, show error
|
||||
} else {
|
||||
runOnUiThread(this::updateState);
|
||||
}
|
||||
}
|
||||
public MeditationAssistant getMeditationAssistant() {
|
||||
if (ma == null) {
|
||||
ma = (MeditationAssistant) this.getApplication();
|
||||
}
|
||||
return ma;
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
package sh.ftp.rocketninelabs.meditationassistant;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.AnyThread;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import net.openid.appauth.AuthState;
|
||||
import net.openid.appauth.AuthorizationException;
|
||||
import net.openid.appauth.AuthorizationResponse;
|
||||
import net.openid.appauth.RegistrationResponse;
|
||||
import net.openid.appauth.TokenResponse;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* An example persistence mechanism for an {@link AuthState} instance.
|
||||
* This stores the instance in a shared preferences file, and provides thread-safe access and
|
||||
* mutation.
|
||||
*/
|
||||
public class AuthStateManager {
|
||||
|
||||
private static final AtomicReference<WeakReference<AuthStateManager>> INSTANCE_REF =
|
||||
new AtomicReference<>(new WeakReference<AuthStateManager>(null));
|
||||
|
||||
private static final String TAG = "AuthStateManager";
|
||||
|
||||
private static final String STORE_NAME = "AuthState";
|
||||
private static final String KEY_STATE = "state";
|
||||
|
||||
private final SharedPreferences mPrefs;
|
||||
private final ReentrantLock mPrefsLock;
|
||||
private final AtomicReference<AuthState> mCurrentAuthState;
|
||||
|
||||
@AnyThread
|
||||
public static AuthStateManager getInstance(@NonNull Context context) {
|
||||
AuthStateManager manager = INSTANCE_REF.get().get();
|
||||
if (manager == null) {
|
||||
manager = new AuthStateManager(context.getApplicationContext());
|
||||
INSTANCE_REF.set(new WeakReference<>(manager));
|
||||
}
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
private AuthStateManager(Context context) {
|
||||
mPrefs = context.getSharedPreferences(STORE_NAME, Context.MODE_PRIVATE);
|
||||
mPrefsLock = new ReentrantLock();
|
||||
mCurrentAuthState = new AtomicReference<>();
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@NonNull
|
||||
public AuthState getCurrent() {
|
||||
if (mCurrentAuthState.get() != null) {
|
||||
return mCurrentAuthState.get();
|
||||
}
|
||||
|
||||
AuthState state = readState();
|
||||
if (mCurrentAuthState.compareAndSet(null, state)) {
|
||||
return state;
|
||||
} else {
|
||||
return mCurrentAuthState.get();
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@NonNull
|
||||
public AuthState replace(@NonNull AuthState state) {
|
||||
writeState(state);
|
||||
mCurrentAuthState.set(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@NonNull
|
||||
public AuthState updateAfterAuthorization(
|
||||
@Nullable AuthorizationResponse response,
|
||||
@Nullable AuthorizationException ex) {
|
||||
AuthState current = getCurrent();
|
||||
current.update(response, ex);
|
||||
return replace(current);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@NonNull
|
||||
public AuthState updateAfterTokenResponse(
|
||||
@Nullable TokenResponse response,
|
||||
@Nullable AuthorizationException ex) {
|
||||
AuthState current = getCurrent();
|
||||
current.update(response, ex);
|
||||
return replace(current);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@NonNull
|
||||
public AuthState updateAfterRegistration(
|
||||
RegistrationResponse response,
|
||||
AuthorizationException ex) {
|
||||
AuthState current = getCurrent();
|
||||
if (ex != null) {
|
||||
return current;
|
||||
}
|
||||
|
||||
current.update(response);
|
||||
return replace(current);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@NonNull
|
||||
private AuthState readState() {
|
||||
mPrefsLock.lock();
|
||||
try {
|
||||
String currentState = mPrefs.getString(KEY_STATE, null);
|
||||
if (currentState == null) {
|
||||
return new AuthState();
|
||||
}
|
||||
|
||||
try {
|
||||
return AuthState.jsonDeserialize(currentState);
|
||||
} catch (JSONException ex) {
|
||||
Log.w(TAG, "Failed to deserialize stored auth state - discarding");
|
||||
return new AuthState();
|
||||
}
|
||||
} finally {
|
||||
mPrefsLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
private void writeState(@Nullable AuthState state) {
|
||||
mPrefsLock.lock();
|
||||
try {
|
||||
SharedPreferences.Editor editor = mPrefs.edit();
|
||||
if (state == null) {
|
||||
editor.remove(KEY_STATE);
|
||||
} else {
|
||||
editor.putString(KEY_STATE, state.jsonSerializeString());
|
||||
}
|
||||
|
||||
if (!editor.commit()) {
|
||||
throw new IllegalStateException("Failed to write state to shared prefs");
|
||||
}
|
||||
} finally {
|
||||
mPrefsLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|