// Copyright 2015 The Project Buendia Authors
//
// 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: http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distrib-
// uted 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
// specific language governing permissions and limitations under the License.
package org.projectbuendia.client.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.provider.Settings;
import android.support.annotation.StringRes;
import android.support.v4.app.FragmentActivity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import org.projectbuendia.client.App;
import org.projectbuendia.client.R;
import org.projectbuendia.client.diagnostics.HealthIssue;
import org.projectbuendia.client.diagnostics.TroubleshootingAction;
import org.projectbuendia.client.events.diagnostics.TroubleshootingActionsChangedEvent;
import org.projectbuendia.client.updater.AvailableUpdateInfo;
import org.projectbuendia.client.updater.DownloadedUpdateInfo;
import org.projectbuendia.client.utils.Logger;
import org.projectbuendia.client.utils.Utils;
import java.util.HashMap;
import java.util.Map;
import de.greenrobot.event.EventBus;
/**
* An abstract {@link FragmentActivity} that is the base for all activities, providing a "content
* view" that can be populated by implementing classes and a "status view" that can be used for
* troubleshooting and status messages.
*/
public abstract class BaseActivity extends FragmentActivity {
private static final Logger LOG = Logger.create();
private static final double PHI = (Math.sqrt(5) + 1)/2; // golden ratio
private static final double STEP_FACTOR = Math.sqrt(PHI); // each step up/down scales this much
private static final long MIN_STEP = -2;
private static final long MAX_STEP = 2;
// TODO: Store sScaleStep in an app preference.
private static long sScaleStep = 0; // app-wide scale step, selected by user
private Long pausedScaleStep = null; // this activity's scale step when last paused
private LinearLayout mWrapperView;
private FrameLayout mInnerContent;
private SnackBar snackBar;
@Override public boolean dispatchKeyEvent(KeyEvent event) {
int action = event.getAction();
int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
if (action == KeyEvent.ACTION_DOWN) {
adjustFontScale(1);
}
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:
if (action == KeyEvent.ACTION_DOWN) {
adjustFontScale(-1);
}
return true;
default:
return super.dispatchKeyEvent(event);
}
}
public void adjustFontScale(int delta) {
long newScaleStep = Math.max(MIN_STEP, Math.min(MAX_STEP, sScaleStep + delta));
if (newScaleStep != sScaleStep) {
restartWithFontScale(newScaleStep);
}
}
public void restartWithFontScale(long newScaleStep) {
Configuration config = getResources().getConfiguration();
config.fontScale = (float) Math.pow(STEP_FACTOR, newScaleStep);
sScaleStep = newScaleStep;
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
finish();
startActivity(getIntent());
}
@Override public void setContentView(int layoutResId) {
initializeWrapperView();
mInnerContent.removeAllViews();
getLayoutInflater().inflate(layoutResId, mInnerContent);
}
private void initializeWrapperView() {
if (mWrapperView != null) return;
mWrapperView =
(LinearLayout) getLayoutInflater().inflate(R.layout.view_status_wrapper, null);
super.setContentView(mWrapperView);
mInnerContent =
(FrameLayout) mWrapperView.findViewById(R.id.status_wrapper_inner_content);
}
private void initializeSnackBar() {
if ((mWrapperView != null) && (snackBar == null)) {
snackBar = new SnackBar(mWrapperView);
}
}
/**
* Adds a message to the SnackBar. Priority defaults to 999.
* @see "SnackBar Documentation." {@link SnackBar#message(int)}
*/
public void snackBar(@StringRes int message) {
snackBar.message(message);
}
/**
* Adds a message to the SnackBar with informed priority.
* @see "SnackBar Documentation." {@link SnackBar#message(int, int)}
*/
public void snackBar(@StringRes int message, int priority) {
snackBar.message(message, priority);
}
/**
* Adds a message to the SnackBar. Priority defaults to 999.
* @see "SnackBar Documentation." {@link SnackBar#message(int, int, View.OnClickListener, int)}
*/
public void snackBar(@StringRes int message, @StringRes int actionMessage, View
.OnClickListener listener) {
snackBar.message(message, actionMessage, listener, 999);
}
/**
* Adds a message to the SnackBar with informed priority.
* @see "SnackBar Documentation." {@link SnackBar#message(int, int, View.OnClickListener, int)}
*/
public void snackBar(@StringRes int message, @StringRes int actionMessage, View
.OnClickListener listener, int priority) {
snackBar.message(message, actionMessage, listener, priority);
}
/**
* Adds a message to the SnackBar with all parameters except for secondsToTimeout.
* @see "SnackBar Documentation."
* {@link SnackBar#message(int, int, View.OnClickListener, int, boolean, int)}
*/
public void snackBar(@StringRes int message, @StringRes int actionMessage, View
.OnClickListener actionOnClick, int priority, boolean isDismissible) {
snackBar.message(message, actionMessage, actionOnClick, priority, isDismissible, 0);
}
/**
* Adds a message to the SnackBar with all parameters.
* @see "SnackBar Documentation."
* {@link SnackBar#message(int, int, View.OnClickListener, int, boolean, int)}
*/
public void snackBar(@StringRes int message, @StringRes int actionMessage, View
.OnClickListener actionOnClick, int priority, boolean isDismissible, int secondsToTimeOut) {
snackBar.message(message, actionMessage, actionOnClick, priority, isDismissible,
secondsToTimeOut);
}
/**
* Use it to programmatically dismiss a SnackBar message.
* @param id The @StringRes for the message.
*/
public void snackBarDismiss(@StringRes int id) {
snackBar.dismiss(id);
}
/**
* Programmatically dismiss multiple messages at once
* @param id a @StringRes message Array
*/
public void snackBarDismiss(@StringRes int[] id) {
snackBar.dismiss(id);
}
@Override public void setContentView(View view) {
initializeWrapperView();
mInnerContent.removeAllViews();
mInnerContent.addView(view);
}
@Override public void setContentView(View view, ViewGroup.LayoutParams params) {
initializeWrapperView();
mInnerContent.removeAllViews();
mInnerContent.addView(view, params);
}
/** Called when the set of troubleshooting actions changes. */
public void onEventMainThread(TroubleshootingActionsChangedEvent event) {
if (event.solvedIssue != null) {
displayProblemSolvedMessage(event.solvedIssue);
}
if (event.actions.isEmpty()) {
return;
}
for (TroubleshootingAction troubleshootingAction: event.actions) {
switch (troubleshootingAction) {
case ENABLE_WIFI:
snackBar(R.string.troubleshoot_wifi_disabled,
R.string.troubleshoot_wifi_disabled_action_enable,
new View.OnClickListener() {
@Override public void onClick(View view) {
((WifiManager) getSystemService(Context.WIFI_SERVICE)).setWifiEnabled
(true);
}
}, 995, false);
break;
case CONNECT_WIFI:
snackBar(R.string.troubleshoot_wifi_disconnected,
R.string.troubleshoot_wifi_disconnected_action_connect,
new View.OnClickListener() {
@Override public void onClick(View view) {
startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS));
}
}, 996, false);
break;
case CHECK_SERVER_AUTH:
snackBar(R.string.troubleshoot_server_auth,
R.string.troubleshoot_server_auth_action_check,
new View.OnClickListener() {
@Override public void onClick(View view) {
SettingsActivity.start(BaseActivity.this);
}
}, 999, false);
break;
case CHECK_SERVER_CONFIGURATION:
snackBar(R.string.troubleshoot_server_address,
R.string.troubleshoot_server_address_action_check,
new View.OnClickListener() {
@Override public void onClick(View view) {
SettingsActivity.start(BaseActivity.this);
}
}, 999, false);
break;
case CHECK_SERVER_REACHABILITY:
snackBar(R.string.troubleshoot_server_unreachable,
R.string.troubleshoot_action_more_info,
new View.OnClickListener() {
@Override public void onClick(View view) {
// TODO: Display the actual server URL that couldn't be reached in
// this message. This will require that injection be hooked up
// through to
// this inner class, which may be complicated.
showMoreInfoDialog(
getString(R.string.troubleshoot_server_unreachable),
getString(R.string.troubleshoot_server_unreachable_details),
true);
}
}, 997, false);
break;
case CHECK_SERVER_SETUP:
snackBar(R.string.troubleshoot_server_unstable,
R.string.troubleshoot_action_more_info,
new View.OnClickListener() {
@Override public void onClick(View view) {
// TODO: Display the actual server URL that couldn't be reached in
// this message. This will require that injection be hooked up
// through to
// this inner class, which may be complicated.
showMoreInfoDialog(
getString(R.string.troubleshoot_server_unstable),
getString(R.string.troubleshoot_server_unstable_details),
false);
}
}, 999, false);
break;
case CHECK_SERVER_STATUS:
snackBar(R.string.troubleshoot_server_not_responding,
R.string.troubleshoot_action_more_info,
new View.OnClickListener() {
@Override public void onClick(View view) {
// TODO: Display the actual server URL that couldn't be reached in
// this message. This will require that injection be hooked up
// through to
// this inner class, which may be complicated.
showMoreInfoDialog(
getString(R.string.troubleshoot_server_not_responding),
getString(R.string.troubleshoot_server_not_responding_details),
false);
}
}, 999, false);
break;
case CHECK_PACKAGE_SERVER_REACHABILITY:
snackBar(R.string.troubleshoot_package_server_unreachable,
R.string.troubleshoot_action_more_info,
new View.OnClickListener() {
@Override public void onClick(View view) {
showMoreInfoDialog(
getString(R.string.troubleshoot_package_server_unreachable),
getString(R.string.troubleshoot_update_server_unreachable_details),
true);
}
}, 998, false);
break;
case CHECK_PACKAGE_SERVER_CONFIGURATION:
snackBar(R.string.troubleshoot_package_server_misconfigured,
R.string.troubleshoot_action_more_info,
new View.OnClickListener() {
@Override public void onClick(View view) {
showMoreInfoDialog(
getString(R.string.troubleshoot_package_server_misconfigured),
getString(
R.string.troubleshoot_update_server_misconfigured_details),
true);
}
}, 999, false);
break;
default:
LOG.w("Troubleshooting action '%1$s' is unknown.", troubleshootingAction);
return;
}
}
}
private void displayProblemSolvedMessage(HealthIssue solvedIssue) {
// The troubleShootingMessages Map have the issue as the key and the TroubleshootingMessage
// object as it's value.
Map<HealthIssue, TroubleshootingMessage> troubleshootingMessages = new HashMap<>();
troubleshootingMessages.put(HealthIssue.WIFI_DISABLED,
new TroubleshootingMessage(
R.string.troubleshoot_wifi_disabled,
R.string.troubleshoot_wifi_disabled_solved,
10
));
troubleshootingMessages.put(HealthIssue.WIFI_NOT_CONNECTED,
new TroubleshootingMessage(
R.string.troubleshoot_wifi_disconnected,
R.string.troubleshoot_wifi_disconnected_solved,
10
));
troubleshootingMessages.put(HealthIssue.SERVER_AUTHENTICATION_ISSUE,
new TroubleshootingMessage(
R.string.troubleshoot_server_auth,
R.string.troubleshoot_server_auth_solved,
10
));
troubleshootingMessages.put(HealthIssue.SERVER_CONFIGURATION_INVALID,
new TroubleshootingMessage(
R.string.troubleshoot_server_address,
R.string.troubleshoot_server_address_solved,
10
));
troubleshootingMessages.put(HealthIssue.SERVER_HOST_UNREACHABLE,
new TroubleshootingMessage(
R.string.troubleshoot_server_unreachable,
R.string.troubleshoot_server_unreachable_solved,
10
));
troubleshootingMessages.put(HealthIssue.SERVER_INTERNAL_ISSUE,
new TroubleshootingMessage(
R.string.troubleshoot_server_unstable,
R.string.troubleshoot_server_unstable_solved,
10
));
troubleshootingMessages.put(HealthIssue.SERVER_NOT_RESPONDING,
new TroubleshootingMessage(
R.string.troubleshoot_server_not_responding,
R.string.troubleshoot_server_not_responding_solved,
10
));
troubleshootingMessages.put(HealthIssue.PACKAGE_SERVER_HOST_UNREACHABLE,
new TroubleshootingMessage(
R.string.troubleshoot_package_server_unreachable,
R.string.troubleshoot_package_server_unreachable_solved,
5
));
troubleshootingMessages.put(HealthIssue.PACKAGE_SERVER_INDEX_NOT_FOUND,
new TroubleshootingMessage(
R.string.troubleshoot_package_server_misconfigured,
R.string.troubleshoot_package_server_misconfigured_solved,
10
));
TroubleshootingMessage messages = troubleshootingMessages.get(solvedIssue);
if (messages != null) {
SnackBar.Message snackBarMessage = snackBar.getMessage(messages.messageId);
if (snackBarMessage != null) {
snackBar.dismiss(snackBarMessage.key);
snackBar.message(messages.resolvedMessageId, 0, null, 994, true, messages.timeout);
}
}
}
private void showMoreInfoDialog(String title, String message,
boolean includeSettingsButton) {
AlertDialog.Builder builder = new AlertDialog.Builder(BaseActivity.this)
.setIcon(android.R.drawable.ic_dialog_info)
.setTitle(title)
.setMessage(message)
.setNeutralButton(android.R.string.ok, null);
if (includeSettingsButton) {
builder.setPositiveButton(R.string.troubleshoot_action_check_settings,
new DialogInterface.OnClickListener() {
@Override public void onClick(DialogInterface dialog, int which) {
SettingsActivity.start(BaseActivity.this);
}
});
}
builder.show();
}
/** The user has requested a download of the last known available software update. */
public static class DownloadRequestedEvent {
}
/** The user has requested installation of the last downloaded software update. */
public static class InstallationRequestedEvent {
}
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
App.getInstance().inject(this);
}
@Override protected void onResume() {
super.onResume();
initializeSnackBar();
if (pausedScaleStep != null && sScaleStep != pausedScaleStep) {
// If the font scale was changed while this activity was paused, force a refresh.
restartWithFontScale(sScaleStep);
}
EventBus.getDefault().registerSticky(this);
App.getInstance().getHealthMonitor().start();
Utils.logEvent("resumed_activity", "class", this.getClass().getSimpleName());
}
@Override protected void onPause() {
EventBus.getDefault().unregister(this);
App.getInstance().getHealthMonitor().stop();
pausedScaleStep = sScaleStep;
super.onPause();
}
protected class UpdateNotificationUi implements UpdateNotificationController.Ui {
public UpdateNotificationUi() {}
@Override public void showUpdateAvailableForDownload(AvailableUpdateInfo updateInfo) {
snackBar(R.string.snackbar_update_available,
R.string.snackbar_action_download,
new View.OnClickListener() {
@Override public void onClick(View view) {
Utils.logEvent("download_update_button_pressed");
//TODO: programatically dismiss the snackbar message
EventBus.getDefault().post(new DownloadRequestedEvent());
}
});
}
@Override public void showUpdateReadyToInstall(DownloadedUpdateInfo updateInfo) {
snackBar(R.string.snackbar_update_downloaded,
R.string.snackbar_action_install,
new View.OnClickListener() {
@Override public void onClick(View view) {
Utils.logEvent("install_update_button_pressed");
//TODO: programatically dismiss the snackbar message
EventBus.getDefault().post(new InstallationRequestedEvent());
}
});
}
@Override public void hideSoftwareUpdateNotifications() {
}
}
/**
* The TroubleshootingMessage object relates the error message with the solved message and
* for how many seconds the solved message is displayed.
*/
private static class TroubleshootingMessage {
@StringRes public final int messageId;
@StringRes public final int resolvedMessageId;
public final int timeout;
/**
*
* @param messageId The message string id triggered by the issue.
* @param resolvedMessageId The new message string id (solved message).
* @param timeout The timeout count (in seconds) for the solved message.
*/
public TroubleshootingMessage(@StringRes int messageId, @StringRes int resolvedMessageId,
int timeout) {
this.messageId = messageId;
this.resolvedMessageId = resolvedMessageId;
this.timeout = timeout;
}
}
}