/*******************************************************************************
* Copyright (C) 2016 Business Factory, s.r.o.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package bf.io.openshop.ux;
import android.animation.Animator;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.Spinner;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.facebook.appevents.AppEventsLogger;
import com.facebook.applinks.AppLinkData;
import java.util.List;
import java.util.Locale;
import bf.io.openshop.CONST;
import bf.io.openshop.MyApplication;
import bf.io.openshop.R;
import bf.io.openshop.SettingsMy;
import bf.io.openshop.api.EndPoints;
import bf.io.openshop.api.GsonRequest;
import bf.io.openshop.entities.Shop;
import bf.io.openshop.entities.ShopResponse;
import bf.io.openshop.testing.EspressoIdlingResource;
import bf.io.openshop.utils.Analytics;
import bf.io.openshop.utils.MsgUtils;
import bf.io.openshop.utils.Utils;
import bf.io.openshop.ux.adapters.ShopSpinnerAdapter;
import bf.io.openshop.ux.dialogs.LoginDialogFragment;
import timber.log.Timber;
/**
* Initial activity. Handle install referrers, notifications and shop selection;
* <p>
* Created by Petr Melicherik.
*/
public class SplashActivity extends AppCompatActivity {
public static final String REFERRER = "referrer";
private static final String TAG = SplashActivity.class.getSimpleName();
private Activity activity;
private ProgressDialog progressDialog;
/**
* Indicates if layout has been already created.
*/
private boolean layoutCreated = false;
/**
* Spinner offering all available shops.
*/
private Spinner shopSelectionSpinner;
/**
* Button allowing selection of shop during fresh start.
*/
private Button continueToShopBtn;
/**
* Indicates that window has been already detached.
*/
private boolean windowDetached = false;
// Possible layouts
private View layoutIntroScreen;
private View layoutContent;
private View layoutContentNoConnection;
private View layoutContentSelectShop;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Timber.tag(TAG);
activity = this;
// init loading dialog
progressDialog = Utils.generateProgressDialog(this, false);
init();
}
/**
* Prepares activity view and handles incoming intent(Notification, utm data).
*/
private void init() {
// Check if data connected.
if (!MyApplication.getInstance().isDataConnected()) {
progressDialog.hide();
Timber.d("No network connection.");
initSplashLayout();
// Skip intro screen.
layoutContent.setVisibility(View.VISIBLE);
layoutIntroScreen.setVisibility(View.GONE);
// Show retry button.
layoutContentNoConnection.setVisibility(View.VISIBLE);
layoutContentSelectShop.setVisibility(View.GONE);
} else {
progressDialog.hide();
// Google Install referrer is handled by CampaignTrackingService and CampaignTrackingReceiver defined in Manifest.
// Referrer is sent with first event.
// Search for analytics data. General GA Campaign, and Facebook app links (if app links implemented on server side too).
Intent intent = this.getIntent();
if (intent != null) {
Uri uri = intent.getData();
if (uri != null && uri.isHierarchical() && (uri.getQueryParameter("utm_source") != null || uri.getQueryParameter(REFERRER) != null)) {
// GA General Campaign & Traffic Source Attribution. Save camping data.
// https://developers.google.com/analytics/devguides/collection/android/v3/campaigns
Timber.d("UTM source detected. - General Campaign & Traffic Source Attribution.");
if (uri.getQueryParameter("utm_source") != null) {
Analytics.setCampaignUriString(uri.toString());
} else if (uri.getQueryParameter(REFERRER) != null) {
Analytics.setCampaignUriString(uri.getQueryParameter(REFERRER));
}
} else if (intent.getExtras() != null) {
// FB app link. For function needs server side implementation also. https://developers.facebook.com/docs/applinks
Timber.d("Extra bundle detected.");
try {
Bundle bundleApplinkData = getIntent().getExtras();
if (bundleApplinkData != null) {
Bundle applinkData = bundleApplinkData.getBundle("al_applink_data");
if (applinkData != null) {
String targetUrl = applinkData.getString("target_url");
if (targetUrl != null && !targetUrl.isEmpty()) {
Timber.d("TargetUrl: %s", targetUrl);
Analytics.setCampaignUriString(targetUrl);
}
}
}
} catch (Exception e) {
Timber.e(e, "Parsing FB deepLink exception");
}
} else {
// FB deferred app link. For function needs server side implementation also. https://developers.facebook.com/docs/applinks
try {
AppLinkData.fetchDeferredAppLinkData(this, new AppLinkData.CompletionHandler() {
@Override
public void onDeferredAppLinkDataFetched(AppLinkData appLinkData) {
try {
if (appLinkData != null) {
String targetUrl = appLinkData.getTargetUri().toString();
if (targetUrl != null && !targetUrl.isEmpty()) {
Timber.e("TargetUrl: %s", targetUrl);
Analytics.setCampaignUriString(targetUrl);
}
}
} catch (Exception e) {
Timber.e(e, "AppLinkData exception");
}
}
});
} catch (Exception e) {
Timber.e(e, "Fetch deferredAppLinkData exception");
}
}
}
// If opened by notification. Try load shop defined by notification data. If error, just start shop with last used shop.
if (this.getIntent() != null && this.getIntent().getExtras() != null && this.getIntent().getExtras().getString(EndPoints.NOTIFICATION_LINK) != null) {
Timber.d("Run by notification.");
String type = this.getIntent().getExtras().getString(EndPoints.NOTIFICATION_LINK, "");
final String title = this.getIntent().getExtras().getString(EndPoints.NOTIFICATION_TITLE, "");
try {
String[] linkParams = type.split(":");
if (linkParams.length != 3) {
Timber.e("Bad notification format. NotifyType: %s", type);
throw new Exception("Bad notification format. NotifyType:" + type);
} else {
final String target = linkParams[1] + ":" + linkParams[2];
int shopId = Integer.parseInt(linkParams[0]);
String url = String.format(EndPoints.SHOPS_SINGLE, shopId);
Analytics.setCampaignUriString(this.getIntent().getExtras().getString(EndPoints.NOTIFICATION_UTM, ""));
progressDialog.show();
GsonRequest<Shop> req = new GsonRequest<>(Request.Method.GET, url, null, Shop.class, new Response.Listener<Shop>() {
@Override
public void onResponse(Shop shop) {
progressDialog.cancel();
Bundle bundle = new Bundle();
bundle.putString(CONST.BUNDLE_PASS_TARGET, target);
bundle.putString(CONST.BUNDLE_PASS_TITLE, title);
// Logout user if shop changed
Shop actualShop = SettingsMy.getActualShop();
if (actualShop != null && shop.getId() != actualShop.getId())
LoginDialogFragment.logoutUser();
setShopInformationAndStartMainActivity(shop, bundle);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
progressDialog.cancel();
MsgUtils.logErrorMessage(error);
startMainActivity(null);
}
});
req.setRetryPolicy(MyApplication.getDefaultRetryPolice());
req.setShouldCache(false);
MyApplication.getInstance().addToRequestQueue(req, CONST.SPLASH_REQUESTS_TAG);
}
} catch (Exception e) {
Timber.e(e, "Skip Splash activity after notification error.");
startMainActivity(null);
}
} else {
// Nothing special. try continue to MainActivity.
Timber.d("Nothing special.");
startMainActivity(null);
}
}
}
/**
* Save selected/received shop, and try continue to MainActivity.
*
* @param shop selected shop for persist.
* @param bundle notification specific data.
*/
private void setShopInformationAndStartMainActivity(Shop shop, Bundle bundle) {
// Save selected shop
SettingsMy.setActualShop(shop);
startMainActivity(bundle);
}
/**
* SetContentView to activity and prepare layout views.
*/
private void initSplashLayout() {
if (!layoutCreated) {
setContentView(R.layout.activity_splash);
layoutContent = findViewById(R.id.splash_content);
layoutIntroScreen = findViewById(R.id.splash_intro_screen);
layoutContentNoConnection = findViewById(R.id.splash_content_no_connection);
layoutContentSelectShop = findViewById(R.id.splash_content_select_shop);
shopSelectionSpinner = (Spinner) findViewById(R.id.splash_shop_selection_spinner);
continueToShopBtn = (Button) findViewById(R.id.splash_continue_to_shop_btn);
continueToShopBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Shop selectedShop = (Shop) shopSelectionSpinner.getSelectedItem();
if (selectedShop != null && selectedShop.getId() != CONST.DEFAULT_EMPTY_ID)
setShopInformationAndStartMainActivity(selectedShop, null);
else
Timber.e("Cannot continue. Shop is not selected or is null.");
}
});
Button reRunButton = (Button) findViewById(R.id.splash_re_run_btn);
if (reRunButton != null) {
reRunButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
progressDialog.show();
(new Handler()).postDelayed(new Runnable() {
@Override
public void run() {
init();
}
}, 600);
}
});
} else {
Timber.e(new RuntimeException(), "ReRunButton didn't found");
}
layoutCreated = true;
} else {
Timber.d("%s screen is already created.", this.getClass().getSimpleName());
}
}
/**
* Check if shop is selected. If so then start {@link MainActivity}. If no then show form with selection.
*
* @param bundle notification specific data.
*/
private void startMainActivity(Bundle bundle) {
if (SettingsMy.getActualShop() == null) {
// First run, allow user choose desired shop.
Timber.d("Missing active shop. Show shop selection.");
initSplashLayout();
layoutContentNoConnection.setVisibility(View.GONE);
layoutContentSelectShop.setVisibility(View.VISIBLE);
requestShops();
} else {
Intent mainIntent = new Intent(SplashActivity.this, MainActivity.class);
if (bundle != null) {
Timber.d("Pass bundle to main activity");
mainIntent.putExtras(bundle);
}
startActivity(mainIntent);
finish();
}
}
/**
* Load available shops from server.
*/
private void requestShops() {
if (layoutIntroScreen.getVisibility() != View.VISIBLE)
progressDialog.show();
GsonRequest<ShopResponse> getShopsRequest = new GsonRequest<>(Request.Method.GET, EndPoints.SHOPS, null, ShopResponse.class,
new Response.Listener<ShopResponse>() {
@Override
public void onResponse(@NonNull ShopResponse response) {
Timber.d("Get shops response: %s", response.toString());
setSpinShops(response.getShopList());
if (progressDialog != null) progressDialog.cancel();
animateContentVisible();
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
if (progressDialog != null) progressDialog.cancel();
MsgUtils.logAndShowErrorMessage(activity, error);
finish();
}
});
getShopsRequest.setRetryPolicy(MyApplication.getDefaultRetryPolice());
getShopsRequest.setShouldCache(false);
MyApplication.getInstance().addToRequestQueue(getShopsRequest, CONST.SPLASH_REQUESTS_TAG);
}
/**
* Prepare spinner with shops and pre-select the most appropriate.
*
* @param shopList list of shops received from server.
*/
private void setSpinShops(List<Shop> shopList) {
if (shopList != null && !shopList.isEmpty()) {
// preset shop selection title.
Shop defaultEmptyValue = new Shop();
defaultEmptyValue.setId(CONST.DEFAULT_EMPTY_ID);
defaultEmptyValue.setName(getString(R.string.Select_shop));
shopList.add(0, defaultEmptyValue);
ShopSpinnerAdapter shopSpinnerAdapter = new ShopSpinnerAdapter(this, shopList, true);
shopSelectionSpinner.setAdapter(shopSpinnerAdapter);
shopSelectionSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Shop item = (Shop) parent.getItemAtPosition(position);
if (item.getId() == CONST.DEFAULT_EMPTY_ID)
continueToShopBtn.setVisibility(View.INVISIBLE);
else {
continueToShopBtn.setVisibility(View.VISIBLE);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
Timber.d("No shop selected.");
}
});
// pre-select shop if only 1 exist
if (shopList.size() == 2) {
shopSelectionSpinner.setSelection(1);
Timber.d("Only one shop exist.");
} else {
// pre-select shop based on language
String defaultLanguage = Locale.getDefault().getLanguage();
Timber.d("Default language: %s", defaultLanguage);
long tempShopId = 0; // DEFAULT no language
// Find corresponding shop and language
for (int i = 0; i < shopList.size(); i++) {
if (shopList.get(i).getLanguage() != null && shopList.get(i).getLanguage().equalsIgnoreCase(defaultLanguage)) {
tempShopId = shopList.get(i).getId();
break;
}
}
// Select founded shop
for (int i = 0; i < shopList.size(); i++) {
if (shopList.get(i).getId() == tempShopId) {
Timber.d("Preselect language position: %s", i);
shopSelectionSpinner.setSelection(i);
break;
}
}
}
} else {
Timber.e(new RuntimeException(), "Trying to set empty shops array.");
}
}
/**
* Hide intro screen and display content layout with animation.
*/
private void animateContentVisible() {
if (layoutIntroScreen != null && layoutContent != null && layoutIntroScreen.getVisibility() == View.VISIBLE) {
EspressoIdlingResource.increment();
runOnUiThread(new Runnable() {
@Override
public void run() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (windowDetached) {
if (layoutContent != null) layoutContent.setVisibility(View.VISIBLE);
} else {
// // If lollipop use reveal animation. On older phones use fade animation.
if (android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
Timber.d("Circular animation.");
// get the center for the animation circle
final int cx = (layoutContent.getLeft() + layoutContent.getRight()) / 2;
final int cy = (layoutContent.getTop() + layoutContent.getBottom()) / 2;
// get the final radius for the animation circle
int dx = Math.max(cx, layoutContent.getWidth() - cx);
int dy = Math.max(cy, layoutContent.getHeight() - cy);
float finalRadius = (float) Math.hypot(dx, dy);
Animator animator = ViewAnimationUtils.createCircularReveal(layoutContent, cx, cy, 0, finalRadius);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setDuration(1250);
layoutContent.setVisibility(View.VISIBLE);
animator.start();
} else {
Timber.d("Alpha animation.");
layoutContent.setAlpha(0f);
layoutContent.setVisibility(View.VISIBLE);
layoutContent.animate()
.alpha(1f)
.setDuration(1000)
.setListener(null);
}
}
EspressoIdlingResource.decrement();
}
}, 330);
}
});
}
}
@Override
protected void onResume() {
super.onResume();
// Logs 'install' and 'app activate' App Events.
AppEventsLogger.activateApp(this);
}
@Override
protected void onPause() {
super.onPause();
// Logs 'app deactivate' App Event.
AppEventsLogger.deactivateApp(this);
}
@Override
protected void onStop() {
if (progressDialog != null) progressDialog.cancel();
if (layoutIntroScreen != null) layoutIntroScreen.setVisibility(View.GONE);
if (layoutContent != null) layoutContent.setVisibility(View.VISIBLE);
MyApplication.getInstance().cancelPendingRequests(CONST.SPLASH_REQUESTS_TAG);
super.onStop();
}
@Override
public void onAttachedToWindow() {
windowDetached = false;
super.onAttachedToWindow();
}
@Override
public void onDetachedFromWindow() {
windowDetached = true;
super.onDetachedFromWindow();
}
}