diff --git a/.gitignore b/.gitignore index f487361a..334e15a9 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,4 @@ manifest-merger-release-report.txt # Android Studio heap captures captures/ +app/google-services.json diff --git a/app/build.gradle b/app/build.gradle index fcdd59a5..b3caee18 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,6 +7,10 @@ android { useLibrary 'org.apache.http.legacy' + configurations { + compile.exclude group: "org.apache.httpcomponents", module: "httpclient" + } + defaultConfig { applicationId 'fi.aalto.legroup.achso' minSdkVersion 16 @@ -84,10 +88,13 @@ dependencies { compile 'org.florescu.android.rangeseekbar:rangeseekbar-library:0.3.0' // Google Play Services APIs - compile 'com.google.android.gms:play-services-base:8.1.0' - compile 'com.google.android.gms:play-services-maps:8.1.0' - compile 'com.google.android.gms:play-services-location:8.1.0' - compile 'com.google.android.gms:play-services-analytics:8.1.0' + compile 'com.google.android.gms:play-services-base:10.2.1' + compile 'com.google.android.gms:play-services-maps:10.2.1' + compile 'com.google.android.gms:play-services-location:10.2.1' + compile 'com.google.android.gms:play-services-analytics:10.2.1' + + // Push notifications + compile 'com.google.firebase:firebase-messaging:10.2.1' // OAuth2 library for OpenID Connect compile('com.google.oauth-client:google-oauth-client:1.19.0') { @@ -129,3 +136,5 @@ dependencies { // Decrypting the Layers Box URL compile 'fi.aalto.legroup:cryptohelper:0.1.0' } + +apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c5d66253..f6ac48b2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -138,7 +138,24 @@ android:label="@string/choose_account" android:parentActivityName=".browsing.BrowserActivity" /> - + + + + + + + + + + + + diff --git a/app/src/main/java/fi/aalto/legroup/achso/app/App.java b/app/src/main/java/fi/aalto/legroup/achso/app/App.java index 88deeff0..bf021cda 100644 --- a/app/src/main/java/fi/aalto/legroup/achso/app/App.java +++ b/app/src/main/java/fi/aalto/legroup/achso/app/App.java @@ -1,5 +1,6 @@ package fi.aalto.legroup.achso.app; +import android.accounts.Account; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -7,23 +8,34 @@ import android.net.NetworkInfo; import android.net.Uri; import android.os.Environment; +import android.os.Handler; import android.preference.PreferenceManager; import android.support.multidex.MultiDexApplication; +import android.util.Log; import android.widget.Toast; import com.google.android.gms.analytics.GoogleAnalytics; +import com.google.firebase.iid.FirebaseInstanceId; import com.rollbar.android.Rollbar; import com.squareup.okhttp.OkHttpClient; import com.squareup.otto.Bus; +import com.squareup.otto.Subscribe; + +import org.json.JSONException; import java.io.File; +import java.io.IOException; import java.security.GeneralSecurityException; +import java.util.Timer; +import java.util.TimerTask; import fi.aalto.legroup.achso.BuildConfig; import fi.aalto.legroup.achso.R; +import fi.aalto.legroup.achso.authentication.AccountLoggedOutEvent; import fi.aalto.legroup.achso.authentication.AuthenticatedHttpClient; import fi.aalto.legroup.achso.authentication.LoginManager; import fi.aalto.legroup.achso.authentication.LoginRequestEvent; +import fi.aalto.legroup.achso.authentication.LoginStateEvent; import fi.aalto.legroup.achso.authentication.OIDCConfig; import fi.aalto.legroup.achso.authoring.ExportHelper; import fi.aalto.legroup.achso.authoring.LocationManager; @@ -79,6 +91,7 @@ public void onCreate() { setupPreferences(); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + layersBoxUrl = readLayersBoxUrl(); usePublicLayersBox = preferences.getBoolean(AppPreferences.USE_PUBLIC_LAYERS_BOX, false); publicLayersBoxUrl = Uri.parse(getString(R.string.publicLayersBoxUrl)); @@ -139,6 +152,7 @@ public void onCreate() { updateOIDCTokens(this); + bus.register(this); bus.post(new LoginRequestEvent(LoginRequestEvent.Type.LOGIN)); // Trim the caches asynchronously @@ -172,6 +186,64 @@ public void tokensRetrieved() { } } + public static void tokenUpdated(String notificationToken) { + if (loginManager.isLoggedIn()) { + App.registerToken(notificationToken); + } + } + + @Subscribe + public static void onLoginStateEvent(final LoginStateEvent event) { + new Timer().schedule(new TimerTask() { + @Override + public void run() { + // this code will be executed after 2 seconds + String token = FirebaseInstanceId.getInstance().getToken(); + + if (token == null) return; + + if (event.getState() == LoginManager.LoginState.LOGGED_IN) { + App.registerToken(token); + } + } + }, 4000); + } + + @Subscribe + public static void onAccountLoggedOutEvent(final AccountLoggedOutEvent event) { + new Timer().schedule(new TimerTask() { + @Override + public void run() { + // this code will be executed after 2 seconds + String token = FirebaseInstanceId.getInstance().getToken(); + + if (token == null) return; + + App.removeTokenFromAccount(event.getAccount(), token); + } + }, 10); + } + + private static void registerToken(final String notificationToken) { + try { + achRails.registerToken(notificationToken); + } catch (JSONException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void removeTokenFromAccount(Account account, String notificationToken) { + try { + achRails.unregisterToken(account, notificationToken); + } catch (JSONException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + public static Uri getLayersBoxUrl() { if (usePublicLayersBox) { return publicLayersBoxUrl; diff --git a/app/src/main/java/fi/aalto/legroup/achso/authentication/AccountLoggedOutEvent.java b/app/src/main/java/fi/aalto/legroup/achso/authentication/AccountLoggedOutEvent.java new file mode 100644 index 00000000..3ca88c4f --- /dev/null +++ b/app/src/main/java/fi/aalto/legroup/achso/authentication/AccountLoggedOutEvent.java @@ -0,0 +1,19 @@ +package fi.aalto.legroup.achso.authentication; + +import android.accounts.Account; + +/** + * Created by mat on 19/04/2017. + */ + +public class AccountLoggedOutEvent { + Account account; + + public AccountLoggedOutEvent(Account account) { + this.account = account; + } + + public Account getAccount() { + return account; + } +} diff --git a/app/src/main/java/fi/aalto/legroup/achso/authentication/AuthenticatedHttpClient.java b/app/src/main/java/fi/aalto/legroup/achso/authentication/AuthenticatedHttpClient.java index e85f88be..d173751b 100644 --- a/app/src/main/java/fi/aalto/legroup/achso/authentication/AuthenticatedHttpClient.java +++ b/app/src/main/java/fi/aalto/legroup/achso/authentication/AuthenticatedHttpClient.java @@ -5,6 +5,7 @@ import android.content.Context; import android.util.Log; +import com.squareup.okhttp.Callback; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; @@ -67,6 +68,16 @@ public Response execute(Request request, Account account, boolean doRetry) throw return response; } + public void enqueue(Request request, Account account, Callback cb) { + AccountManager accountManager = AccountManager.get(context); + + String token = getBearerToken(account); + + request = request.newBuilder().header("Authorization", "Bearer " + token).build(); + + httpClient.newCall(request).enqueue(cb); + } + public boolean accessDenied(Response response) { int code = response.code(); diff --git a/app/src/main/java/fi/aalto/legroup/achso/authentication/LoginManager.java b/app/src/main/java/fi/aalto/legroup/achso/authentication/LoginManager.java index 59ed3644..d2c7f483 100644 --- a/app/src/main/java/fi/aalto/legroup/achso/authentication/LoginManager.java +++ b/app/src/main/java/fi/aalto/legroup/achso/authentication/LoginManager.java @@ -88,10 +88,16 @@ public void login(Account account) { new LoginTask().execute(account); } + private void notifyAcccountLoggedOut(Account account) { + this.bus.post(new AccountLoggedOutEvent(account)); + } + /** * Logs out from the account and disables auto-login. Use this if the user manually logs out. */ public void logoutExplicitly() { + setState(LoginState.LOGGING_OUT, false); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs.edit() @@ -106,7 +112,12 @@ public void logoutExplicitly() { * automatic (e.g. connectivity lost) and not initiated by the user. */ public void logout() { + if (getState() != LoginState.LOGGING_OUT) { + setState(LoginState.LOGGING_OUT, false); + } + setState(LoginState.LOGGED_OUT, true); + notifyAcccountLoggedOut(account); account = null; user = null; } @@ -259,7 +270,5 @@ protected void onPostExecute(String error) { setState(LoginState.LOGGED_IN, true); } - } - } diff --git a/app/src/main/java/fi/aalto/legroup/achso/browsing/DetailActivity.java b/app/src/main/java/fi/aalto/legroup/achso/browsing/DetailActivity.java index d2429494..0817d1b0 100644 --- a/app/src/main/java/fi/aalto/legroup/achso/browsing/DetailActivity.java +++ b/app/src/main/java/fi/aalto/legroup/achso/browsing/DetailActivity.java @@ -25,6 +25,7 @@ import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.CircleOptions; @@ -143,27 +144,30 @@ public void onCreate(Bundle savedInstanceState) { SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.mapFragment); - Location location = video.getLocation(); + final Location location = video.getLocation(); if (location != null) { - LatLng position = new LatLng(location.getLatitude(), location.getLongitude()); - - GoogleMap map = mapFragment.getMap(); + mapFragment.getMapAsync(new OnMapReadyCallback() { + @Override + public void onMapReady(GoogleMap googleMap) { + LatLng position = new LatLng(location.getLatitude(), location.getLongitude()); - map.addCircle(new CircleOptions() - .center(position) - .radius(location.getAccuracy()) - .strokeWidth(3.0f) - .strokeColor(Color.WHITE) - .fillColor(Color.parseColor("#80ffffff"))); + googleMap.addCircle(new CircleOptions() + .center(position) + .radius(location.getAccuracy()) + .strokeWidth(3.0f) + .strokeColor(Color.WHITE) + .fillColor(Color.parseColor("#80ffffff"))); - map.addMarker(new MarkerOptions() - .position(position) - .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))); + googleMap.addMarker(new MarkerOptions() + .position(position) + .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))); - map.moveCamera(CameraUpdateFactory.newLatLngZoom(position, 14.5f)); + googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(position, 14.5f)); - findViewById(R.id.unknownLocationText).setVisibility(View.GONE); + findViewById(R.id.unknownLocationText).setVisibility(View.GONE); + } + }); } initializeAddQRButton(); diff --git a/app/src/main/java/fi/aalto/legroup/achso/storage/remote/VideoHost.java b/app/src/main/java/fi/aalto/legroup/achso/storage/remote/VideoHost.java index 3d0da60c..75f2a9ba 100644 --- a/app/src/main/java/fi/aalto/legroup/achso/storage/remote/VideoHost.java +++ b/app/src/main/java/fi/aalto/legroup/achso/storage/remote/VideoHost.java @@ -1,5 +1,6 @@ package fi.aalto.legroup.achso.storage.remote; +import android.accounts.Account; import android.net.Uri; import org.json.JSONException; @@ -57,4 +58,8 @@ public interface VideoHost { * Finds a video by the video source uri. */ public Video findVideoByVideoUri(Uri videoUri) throws IOException; + + public void registerToken(String notificationToken) throws JSONException, IOException; + + public void unregisterToken(Account account, String notificationToken) throws JSONException, IOException; } diff --git a/app/src/main/java/fi/aalto/legroup/achso/storage/remote/strategies/AchRailsStrategy.java b/app/src/main/java/fi/aalto/legroup/achso/storage/remote/strategies/AchRailsStrategy.java index f523f328..faddede8 100644 --- a/app/src/main/java/fi/aalto/legroup/achso/storage/remote/strategies/AchRailsStrategy.java +++ b/app/src/main/java/fi/aalto/legroup/achso/storage/remote/strategies/AchRailsStrategy.java @@ -3,6 +3,7 @@ import android.accounts.Account; import android.net.Uri; +import com.squareup.okhttp.Callback; import com.squareup.okhttp.FormEncodingBuilder; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.Request; @@ -27,6 +28,7 @@ import fi.aalto.legroup.achso.entities.serialization.json.JsonSerializable; import fi.aalto.legroup.achso.entities.serialization.json.JsonSerializer; import fi.aalto.legroup.achso.storage.remote.VideoHost; +import fi.aalto.legroup.achso.utilities.EmptyCallback; import okio.BufferedSink; import okio.Okio; @@ -60,6 +62,7 @@ Request.Builder buildVideosRequest() { return new Request.Builder() .url(endpointUrl.buildUpon().appendPath("videos.json").toString()); } + Request.Builder buildVideosRequest(UUID id) { return new Request.Builder() .url(endpointUrl.buildUpon() @@ -101,6 +104,16 @@ private Response validateResponse(Response response) throws IOException { return response; } + private Response executeRequestWithAccount(Account account, Request request) { + try { + return App.authenticatedHttpClient.execute(request, account); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + private Response executeRequest(Request request) throws IOException { return validateResponse(executeRequestNoFail(request)); } @@ -161,6 +174,41 @@ public void makeVideoPrivate(UUID videoId) throws IOException, JSONException { Response response = executeRequest(request); } + + @Override + public void registerToken(String notificationToken) throws JSONException, IOException { + JSONObject obj = new JSONObject(); + obj.put("registration_token", notificationToken); + + RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), obj.toString()); + + Request request = new Request.Builder() + .url(endpointUrl.buildUpon() + .appendPath("notifications") + .appendPath("register_token") + .toString()) + .put(body).build(); + + executeRequest(request); + } + + @Override + public void unregisterToken(Account account, String notificationToken) throws JSONException, IOException { + JSONObject obj = new JSONObject(); + obj.put("registration_token", notificationToken); + + RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), obj.toString()); + + Request request = new Request.Builder() + .url(endpointUrl.buildUpon() + .appendPath("notifications") + .appendPath("unregister_token") + .toString()) + .put(body).build(); + + executeRequestWithAccount(account, request); + } + @Override public Video downloadVideoManifest(UUID id) throws IOException { diff --git a/app/src/main/java/fi/aalto/legroup/achso/utilities/AchsoFirebaseInstanceIdService.java b/app/src/main/java/fi/aalto/legroup/achso/utilities/AchsoFirebaseInstanceIdService.java new file mode 100644 index 00000000..fdfe4e04 --- /dev/null +++ b/app/src/main/java/fi/aalto/legroup/achso/utilities/AchsoFirebaseInstanceIdService.java @@ -0,0 +1,17 @@ +package fi.aalto.legroup.achso.utilities; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.FirebaseInstanceIdService; + +import fi.aalto.legroup.achso.app.App; + +public class AchsoFirebaseInstanceIdService extends FirebaseInstanceIdService { + + @Override + public void onTokenRefresh() { + String refreshedToken = FirebaseInstanceId.getInstance().getToken(); + + App.tokenUpdated(refreshedToken); + + super.onTokenRefresh(); + } +} diff --git a/app/src/main/java/fi/aalto/legroup/achso/utilities/AchsoFirebaseMessagingService.java b/app/src/main/java/fi/aalto/legroup/achso/utilities/AchsoFirebaseMessagingService.java new file mode 100644 index 00000000..c159f025 --- /dev/null +++ b/app/src/main/java/fi/aalto/legroup/achso/utilities/AchsoFirebaseMessagingService.java @@ -0,0 +1,57 @@ +package fi.aalto.legroup.achso.utilities; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.media.RingtoneManager; +import android.net.Uri; +import android.support.v4.app.NotificationCompat; + +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; + +import fi.aalto.legroup.achso.R; +import fi.aalto.legroup.achso.browsing.BrowserActivity; + +/** + * Created by mat on 18/04/2017. + */ + +public class AchsoFirebaseMessagingService extends FirebaseMessagingService { + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + super.onMessageReceived(remoteMessage); + + if (remoteMessage.getNotification() != null) { + RemoteMessage.Notification notification = remoteMessage.getNotification(); + String title = notification.getTitle(); + String body = notification.getBody(); + + sendNotification(body, title); + } + } + + private void sendNotification(String messageBody, String messageTitle) { + Intent intent = new Intent(this, BrowserActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent, + PendingIntent.FLAG_ONE_SHOT); + + + Uri defaultSoundUri= RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.ic_launcher) + .setContentTitle(messageTitle) + .setContentText(messageBody) + .setAutoCancel(true) + .setSound(defaultSoundUri) + .setContentIntent(pendingIntent); + + NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + notificationManager.notify(0 /* ID of notification */, notificationBuilder.build()); + } +} diff --git a/app/src/main/java/fi/aalto/legroup/achso/utilities/EmptyCallback.java b/app/src/main/java/fi/aalto/legroup/achso/utilities/EmptyCallback.java new file mode 100644 index 00000000..44ba585d --- /dev/null +++ b/app/src/main/java/fi/aalto/legroup/achso/utilities/EmptyCallback.java @@ -0,0 +1,23 @@ +package fi.aalto.legroup.achso.utilities; + +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import java.io.IOException; + +/** + * Created by mat on 09/04/2017. + */ + +public class EmptyCallback implements Callback { + @Override + public void onFailure(Request request, IOException e) { + System.out.println(request); + } + + @Override + public void onResponse(Response response) throws IOException { + System.out.println(response); + } +} diff --git a/build.gradle b/build.gradle index c6f2f500..e118f6a3 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,8 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:2.3.1' + classpath 'com.google.gms:google-services:3.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files