Java tutorial
// Copyright 2015 The Vanadium Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package io.v.android.security; import android.app.Activity; import android.app.Fragment; import android.app.FragmentTransaction; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Looper; import android.preference.PreferenceManager; import android.util.Log; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import java.security.interfaces.ECPublicKey; import java.util.UUID; import io.v.android.impl.google.services.blessing.BlessingActivity; import io.v.android.v23.V; import io.v.v23.VFutures; import io.v.v23.context.VContext; import io.v.v23.security.Blessings; import io.v.v23.security.Constants; import io.v.v23.security.VPrincipal; import io.v.v23.security.VSecurity; import io.v.v23.verror.VException; import io.v.v23.vom.VomUtil; /** * Manages {@link Blessings} for a given Android application, persisting them in its * shared preferences. * <p> * This class is thread-safe. */ public class BlessingsManager extends Fragment { private static String TAG = "BlessingsManager"; private static final int REQUEST_CODE_MINT_BLESSINGS = 1000; private static final String STATE_SAVED = "STATE_SAVED"; private static SettableFuture<Blessings> mintFuture; private VContext mBaseContext; private boolean mWasDestroyed = false; private String mPrefKey; // may be null if wasDestroyed == true private String mGoogleAccount; // may be null if wasDestroyed == true /** * Returns a new {@link ListenableFuture} whose result are the {@link Blessings} found in * {@link SharedPreferences} under the given key. * <p> * If no {@link Blessings} are found, mints a new set of {@link Blessings} and stores them * in {@link SharedPreferences} under the provided key. * <p> * This method may start an activity to handle the creation of new blessings, if needed. * Hence, you should be prepared that your activity may be stopped and re-started. * <p> * This method is re-entrant: if blessings need to be minted, multiple concurrent invocations * of this method will result in only the last invocation's future ever being invoked. This * means that it's safe to call this method from any of the android's lifecycle methods * (e.g., onCreate(), onStart(), onResume()). * <p> * This method must be invoked on the UI thread. * * @param context Vanadium context * @param activity android {@link Activity} requesting blessings * @param key a key under which the blessings are stored * @param setAsDefault if true, the returned {@link Blessings} will be set as default * blessings for the app * @return a new {@link ListenableFuture} whose result are the blessings * persisted under the given key */ public static ListenableFuture<Blessings> getBlessings(VContext context, final Activity activity, String key, boolean setAsDefault) { if (Looper.myLooper() != Looper.getMainLooper()) { return Futures .immediateFailedFuture(new VException("getBlessings() must be invoked " + "on the UI thread")); } try { Blessings blessings = readBlessings(context, activity, key, setAsDefault); if (blessings != null) { return Futures.immediateFuture(blessings); } } catch (VException e) { Log.e(TAG, "Malformed blessings in SharedPreferences. Minting new blessings: " + e.getMessage()); } return mintBlessings(context, activity, key, setAsDefault); } /** * Returns {@link Blessings} found in {@link SharedPreferences} under the given key. * <p> * Unlike {@link #getBlessings}, if no {@link Blessings} are found this method won't mint a new * set of {@link Blessings}; instead, {@code null} value is returned. * * @param context Vanadium context * @param androidContext android {@link Context} requesting blessings * @param key a key under which the blessings are stored * @param setAsDefault if true, the returned {@link Blessings}, if non-{@code null}, will be * set as default blessings for the app * @return {@link Blessings} found in {@link SharedPreferences} under the given * key or {@code null} if no blessings are found * @throws VException if the blessings are found in {@link SharedPreferences} but they * are invalid */ public static Blessings readBlessings(VContext context, Context androidContext, String key, boolean setAsDefault) throws VException { String blessingsVom = PreferenceManager.getDefaultSharedPreferences(androidContext).getString(key, ""); if (blessingsVom == null || blessingsVom.isEmpty()) { return null; } Blessings blessings = (Blessings) VomUtil.decodeFromString(blessingsVom, Blessings.class); if (blessings == null) { throw new VException("Couldn't decode blessings: got null blessings"); } // TODO(spetrovic): validate the blessings and fail if they aren't valid return setAsDefault ? VFutures.sync(wrapWithSetAsDefault(context, androidContext, Futures.immediateFuture(blessings))) : blessings; } /** * Mints a new set of {@link Blessings} that are persisted in {@link SharedPreferences} under * the provided key. * <p> * If {@code googleAccount} is non-{@code null} and non-empty, mints the blessings using * that account; otherwise, prompts the user to pick one of the installed Google accounts * (if there is more than one installed). * <p> * This method will start an activity to handle the creation of new blessings. Hence, you * should be prepared that your activity will be stopped and re-started, at the minimum. * <p> * This method is re-entrant: if invoked the 2nd time while the 1st invocation is still * pending, the future associated with the 2nd invocation will overwrite the 1st future: the * 1st future will never be invoked. * <p> * This method must be invoked on the UI thread. * * @param activity android {@link Activity} requesting blessings * @param key a key in {@link SharedPreferences} under which the newly minted * blessings are persisted * @param googleAccount a Google account to use to mint the blessings; if {@code null} or * empty, user will be prompted to pick one of the installed Google * accounts, if there is more than one installed * @param setAsDefault if true, the returned {@link Blessings} will be set as default * blessings for the app * @return a new {@link ListenableFuture} whose result are the newly minted * {@link Blessings} */ public static ListenableFuture<Blessings> mintBlessings(VContext ctx, final Activity activity, String key, String googleAccount, boolean setAsDefault) { if (Looper.myLooper() != Looper.getMainLooper()) { return Futures .immediateFailedFuture(new VException("mintBlessings() must be invoked " + "on the UI thread")); } if (mintFuture != null) { // Mint already in progress, which means that the invoking activity has been // destroyed and then recreated. Register the new future to be invoked on completion // of that mint. Note that it is safe and desirable to override the old future // as it's invocation would be handled by a destroyed activity. mintFuture = SettableFuture.create(); return setAsDefault ? wrapWithSetAsDefault(ctx, activity, mintFuture) : mintFuture; } mintFuture = SettableFuture.create(); FragmentTransaction transaction = activity.getFragmentManager().beginTransaction(); BlessingsManager fragment = new BlessingsManager(); fragment.mPrefKey = key; fragment.mGoogleAccount = googleAccount; transaction.add(fragment, UUID.randomUUID().toString()); transaction.commit(); // this will invoke the fragment's onCreate() immediately. return setAsDefault ? wrapWithSetAsDefault(ctx, activity, mintFuture) : mintFuture; } /** * A shortcut for {@link #mintBlessings(VContext, Activity, String, String, boolean)}} with * empty Google account, causing the user to be prompted to pick one of the installed Google * accounts (if there is more than one installed). */ public static ListenableFuture<Blessings> mintBlessings(VContext ctx, Activity activity, final String key, boolean setAsDefault) { return mintBlessings(ctx, activity, key, "", setAsDefault); } public BlessingsManager() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mBaseContext = V.init(getActivity()); // onCreate() being called with non-null savedInstanceState is an indicator that the // fragment (and the containing activity) have been destroyed since originally // created, as onCreate() wouldn't be called again, otherwise. mWasDestroyed = savedInstanceState != null; if (!mWasDestroyed) { // Start the intent to fetch the blessings. ECPublicKey pubKey = V.getPrincipal(mBaseContext).publicKey(); Intent intent = new Intent(getActivity(), BlessingActivity.class); intent.putExtra(BlessingActivity.EXTRA_PUBLIC_KEY, pubKey); if (mGoogleAccount != null && !mGoogleAccount.isEmpty()) { intent.putExtra(BlessingActivity.EXTRA_GOOGLE_ACCOUNT, mGoogleAccount); } intent.putExtra(BlessingActivity.EXTRA_PREF_KEY, mPrefKey); startActivityForResult(intent, REQUEST_CODE_MINT_BLESSINGS); } } @Override public void onDestroy() { super.onDestroy(); mBaseContext.cancel(); } @Override public void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); // Just write something into the bundle (see onCreate() above). savedInstanceState.putBoolean(STATE_SAVED, true); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CODE_MINT_BLESSINGS: { if (mintFuture == null) { // shouldn't really happen break; } SettableFuture<Blessings> future = mintFuture; mintFuture = null; // Extract VOM-encoded blessings. if (data == null) { future.setException(new VException("NULL blessing response")); break; } if (resultCode != Activity.RESULT_OK) { future.setException(new VException( "Error getting blessing: " + data.getStringExtra(BlessingActivity.EXTRA_ERROR))); break; } byte[] blessingsVom = data.getByteArrayExtra(BlessingActivity.EXTRA_REPLY); if (blessingsVom == null || blessingsVom.length <= 0) { future.setException(new VException("Got null blessings.")); break; } // VOM-Decode blessings. try { Blessings blessings = (Blessings) VomUtil.decode(blessingsVom, Blessings.class); future.set(blessings); } catch (VException e) { future.setException(e); } break; } } // Remove this fragment from the invoking activity. FragmentTransaction transaction = getActivity().getFragmentManager().beginTransaction(); transaction.remove(this); transaction.commit(); super.onActivityResult(requestCode, resultCode, data); } private static ListenableFuture<Blessings> wrapWithSetAsDefault(final VContext ctx, final Context context, ListenableFuture<Blessings> future) { return Futures.transform(future, new AsyncFunction<Blessings, Blessings>() { @Override public ListenableFuture<Blessings> apply(Blessings blessings) throws Exception { if (ctx.isCanceled()) { return Futures.immediateFailedFuture(new VException("Vanadium context canceled")); } // Update local state with the new blessings. try { VPrincipal p = V.getPrincipal(ctx); p.blessingStore().setDefaultBlessings(blessings); p.blessingStore().set(blessings, Constants.ALL_PRINCIPALS); VSecurity.addToRoots(p, blessings); return Futures.immediateFuture(blessings); } catch (VException e) { return Futures.immediateFailedFuture(e); } } }); } }