diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 4b5f8090eede..7ac23b64f1ee 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -135,7 +135,7 @@ + { + + private static final String ARG_SITE_TITLE = "ARG_SITE_TITLE"; + private static final String ARG_SITE_TAGLINE = "ARG_SITE_TAGLINE"; + private static final String ARG_SITE_SLUG = "ARG_SITE_SLUG"; + private static final String ARG_SITE_THEME = "ARG_SITE_THEME"; + + public enum SiteCreationPhase { + IDLE, + NEW_SITE, + FETCHING_NEW_SITE, + SET_TAGLINE, + SET_THEME, + SUCCESS, + FAILURE + } + + public static class OnSiteCreationStateUpdated { + public final SiteCreationPhase state; + + public OnSiteCreationStateUpdated(SiteCreationPhase state) { + this.state = state; + } + } + + @Inject Dispatcher mDispatcher; + @Inject AccountStore mAccountStore; + @Inject SiteStore mSiteStore; + + private SiteCreationPhase mSiteCreationPhase = SiteCreationPhase.IDLE; + + private String mSiteTagline; + private String mSiteTheme; + private long mNewSiteRemoteId; + + public static void createSite( + Context context, + String siteTitle, + String siteTagline, + String siteSlug, + String siteTheme) { + Intent intent = new Intent(context, SiteCreationService.class); + intent.putExtra(ARG_SITE_TITLE, siteTitle); + intent.putExtra(ARG_SITE_TAGLINE, siteTagline); + intent.putExtra(ARG_SITE_SLUG, siteSlug); + intent.putExtra(ARG_SITE_THEME, siteTheme); + context.startService(intent); + } + + public SiteCreationService() { + super(OnSiteCreationStateUpdated.class); + } + + @Override + protected OnSiteCreationStateUpdated getCurrentStateEvent() { + return new OnSiteCreationStateUpdated(mSiteCreationPhase); + } + + @Override + public boolean isInProgress() { + return mSiteCreationPhase != SiteCreationPhase.IDLE + && mSiteCreationPhase != SiteCreationPhase.SUCCESS + && mSiteCreationPhase != SiteCreationPhase.FAILURE; + } + + @Override + public boolean isError() { + return mSiteCreationPhase == SiteCreationPhase.FAILURE; + } + + @Override + public Notification getNotification() { + switch (mSiteCreationPhase) { + case NEW_SITE: + return getProgressNotification(25, "Site creation in: " + mSiteCreationPhase.name()); + case FETCHING_NEW_SITE: + return getProgressNotification(50, "Site creation in: " + mSiteCreationPhase.name()); + case SET_TAGLINE: + return getProgressNotification(75, "Site creation in: " + mSiteCreationPhase.name()); + case SET_THEME: + return getProgressNotification(100, "Site creation in: " + mSiteCreationPhase.name()); + case SUCCESS: + return getSuccessNotification("Site created!"); + case FAILURE: + return getFailureNotification("Site creation failed :("); + } + + return null; + } + + private void setState(SiteCreationPhase siteCreationPhase) { + mSiteCreationPhase = siteCreationPhase; + notifyState(); + + if (siteCreationPhase == SiteCreationPhase.FAILURE || siteCreationPhase == SiteCreationPhase.SUCCESS) { + stopSelf(); + } + } + + @Override + public void onCreate() { + super.onCreate(); + ((WordPress) getApplication()).component().inject(this); + + AppLog.i(T.MAIN, "SiteCreationService > Created"); + mDispatcher.register(this); + + // TODO: Recover any site creations that were interrupted by the service being stopped? + } + + @Override + public void onDestroy() { + mDispatcher.unregister(this); + AppLog.i(T.MAIN, "SiteCreationService > Destroyed"); + super.onDestroy(); + } + + private Intent getPendingIntent() { + return new Intent(this, NewBlogActivity.class); + } + + private Notification getProgressNotification(int progress, String content) { + return new NotificationCompat.Builder(this) + .setContentTitle(content) + .setSmallIcon(R.drawable.ic_my_sites_24dp) + .setColor(getResources().getColor(R.color.blue_wordpress)) + .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), + R.mipmap.app_icon)) + .setAutoCancel(true) + .setContentIntent(PendingIntent.getActivity(SiteCreationService.this, + AutoForeground.NOTIFICATION_ID_PROGRESS, + getPendingIntent(), + PendingIntent.FLAG_ONE_SHOT)) + .setProgress(100, progress, false) + .build(); + } + + private Notification getSuccessNotification(String content) { + return new NotificationCompat.Builder(this) + .setContentTitle(content) + .setSmallIcon(R.drawable.ic_my_sites_24dp) + .setColor(getResources().getColor(R.color.blue_wordpress)) + .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), + R.mipmap.app_icon)) + .setAutoCancel(true) + .setContentIntent(PendingIntent.getActivity(SiteCreationService.this, + AutoForeground.NOTIFICATION_ID_SUCCESS, + getPendingIntent(), + PendingIntent.FLAG_ONE_SHOT)) + .build(); + } + + private Notification getFailureNotification(String content) { + return new NotificationCompat.Builder(this) + .setContentTitle(content) + .setSmallIcon(R.drawable.ic_my_sites_24dp) + .setColor(getResources().getColor(R.color.blue_wordpress)) + .setLargeIcon(BitmapFactory.decodeResource(getApplicationContext().getResources(), + R.mipmap.app_icon)) + .setAutoCancel(true) + .setContentIntent(PendingIntent.getActivity(SiteCreationService.this, + AutoForeground.NOTIFICATION_ID_FAILURE, + getPendingIntent(), + PendingIntent.FLAG_ONE_SHOT)) + .build(); + } + + @Override + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { + if (intent == null) { + return START_NOT_STICKY; + } + + setState(SiteCreationPhase.NEW_SITE); + + final String siteTitle = intent.getStringExtra(ARG_SITE_TITLE); + final String siteSlug = intent.getStringExtra(ARG_SITE_SLUG); + mSiteTagline = intent.getStringExtra(ARG_SITE_TAGLINE); + mSiteTheme = intent.getStringExtra(ARG_SITE_THEME); + + final String language = LanguageUtils.getPatchedCurrentDeviceLanguage(this); + + SiteStore.NewSitePayload newSitePayload =new SiteStore.NewSitePayload( + siteSlug, + siteTitle, + language, + SiteStore.SiteVisibility.PUBLIC, + false); + mDispatcher.dispatch(SiteActionBuilder.newCreateNewSiteAction(newSitePayload)); + AppLog.i(T.NUX, "User tries to create a new site, title: " + siteTitle + ", SiteName: " + siteSlug); + + return START_REDELIVER_INTENT; + } + + private void activateTheme(final SiteModel site, final String themeId) { + WordPress.getRestClientUtils().setTheme(site.getSiteId(), themeId, new RestRequest.Listener() { + @Override + public void onResponse(JSONObject response) { + ThemeTable.setCurrentTheme(WordPress.wpDB.getDatabase(), String.valueOf(site.getSiteId()), themeId); + + setState(SiteCreationPhase.SUCCESS); + } + }, new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + setState(SiteCreationPhase.FAILURE); + } + }); + } + + // OnChanged events + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onNewSiteCreated(SiteStore.OnNewSiteCreated event) { + AppLog.i(T.NUX, event.toString()); + if (event.isError()) { + setState(SiteCreationPhase.FAILURE); + return; + } + + AnalyticsTracker.track(AnalyticsTracker.Stat.CREATED_SITE); + + setState(SiteCreationPhase.FETCHING_NEW_SITE); + + mNewSiteRemoteId = event.newSiteRemoteId; + + // We can't get all the site informations from the new site endpoint, so we have to fetch the site list. + mDispatcher.dispatch(SiteActionBuilder.newFetchSitesAction()); + } + + @SuppressWarnings("unused") + @Subscribe(threadMode = ThreadMode.MAIN) + public void onSiteChanged(SiteStore.OnSiteChanged event) { + AppLog.i(T.NUX, event.toString()); + if (event.isError()) { + // Site has been created but there was a error while fetching the sites. Can happen if we get + // a response including a broken Jetpack site. We can continue and check if the newly created + // site has been fetched. + AppLog.e(T.NUX, event.error.type.toString()); + } + + final SiteModel site = mSiteStore.getSiteBySiteId(mNewSiteRemoteId); + + if (mSiteCreationPhase == SiteCreationPhase.FETCHING_NEW_SITE) { + Intent intent = new Intent(); + if (site == null) { + setState(SiteCreationPhase.FAILURE); + return; + } + + setState(SiteCreationPhase.SET_TAGLINE); + + SiteSettingsInterface siteSettings = SiteSettingsInterface.getInterface(this, site, + new SiteSettingsInterface.SiteSettingsListener() { + @Override + public void onSaveError(Exception error) { + setState(SiteCreationPhase.FAILURE); + } + + @Override + public void onFetchError(Exception error) { + setState(SiteCreationPhase.FAILURE); + } + + @Override + public void onSettingsUpdated() { + // we'll just handle onSettingsSaved() + } + + @Override + public void onSettingsSaved() { + setState(SiteCreationPhase.SET_THEME); + SiteModel site = mSiteStore.getSiteBySiteId(mNewSiteRemoteId); + activateTheme(site, mSiteTheme); + } + + @Override + public void onCredentialsValidated(Exception error) { + if (error != null) { + setState(SiteCreationPhase.FAILURE); + } + } + }); + + if (siteSettings == null) { + setState(SiteCreationPhase.FAILURE); + return; + } + + siteSettings.init(false); + siteSettings.setTagline(mSiteTagline); + siteSettings.saveSettings(); + } else if (mSiteCreationPhase == SiteCreationPhase.SET_TAGLINE) { + setState(SiteCreationPhase.SET_THEME); + activateTheme(site, mSiteTheme); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java index 08f2b769de2a..9813ec1ef03b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsInterface.java @@ -227,9 +227,11 @@ protected void finalize() throws Throwable { public void saveSettings() { SiteSettingsTable.saveSettings(mSettings); - siteSettingsPreferences(mContext).edit().putString(LANGUAGE_PREF_KEY, mSettings.language).apply(); - siteSettingsPreferences(mContext).edit().putInt(DEF_CATEGORY_PREF_KEY, mSettings.defaultCategory).apply(); - siteSettingsPreferences(mContext).edit().putString(DEF_FORMAT_PREF_KEY, mSettings.defaultPostFormat).apply(); + siteSettingsPreferences(mContext).edit() + .putString(LANGUAGE_PREF_KEY, mSettings.language) + .putInt(DEF_CATEGORY_PREF_KEY, mSettings.defaultCategory) + .putString(DEF_FORMAT_PREF_KEY, mSettings.defaultPostFormat) + .apply(); } public @NonNull String getTitle() { @@ -940,9 +942,7 @@ public void run() { } protected void notifyFetchErrorOnUiThread(final Exception error) { - if (mContext == null - || (mContext instanceof Activity && ((Activity) mContext).isFinishing()) - || mListener == null) { + if (mListener == null) { return; } @@ -955,9 +955,7 @@ public void run() { } protected void notifySaveErrorOnUiThread(final Exception error) { - if (mContext == null - || (mContext instanceof Activity && ((Activity) mContext).isFinishing()) - || mListener == null) { + if (mListener == null) { return; } @@ -973,9 +971,7 @@ public void run() { * Notifies listener that settings have been updated with the latest remote data. */ protected void notifyUpdatedOnUiThread() { - if (mContext == null - || (mContext instanceof Activity && ((Activity) mContext).isFinishing()) - || mListener == null) { + if (mListener == null) { return; } @@ -991,7 +987,9 @@ public void run() { * Notifies listener that settings have been saved or an error occurred while saving. */ protected void notifySavedOnUiThread() { - if (mContext == null || mListener == null) return; + if (mListener == null) { + return; + } new Handler().post(new Runnable() { @Override diff --git a/WordPress/src/main/res/layout/new_blog_activity.xml b/WordPress/src/main/res/layout/new_blog_activity.xml index 8bdf62edb83e..58e55488f85d 100644 --- a/WordPress/src/main/res/layout/new_blog_activity.xml +++ b/WordPress/src/main/res/layout/new_blog_activity.xml @@ -2,13 +2,12 @@ android:id="@+id/main_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/nux_background" - android:orientation="vertical"> + android:orientation="vertical" + android:baselineAligned="true"> - + android:layout_height="match_parent"/> - \ No newline at end of file + diff --git a/WordPress/src/main/res/layout/site_creating_screen.xml b/WordPress/src/main/res/layout/site_creating_screen.xml new file mode 100644 index 000000000000..84185c5161af --- /dev/null +++ b/WordPress/src/main/res/layout/site_creating_screen.xml @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 2b1b30aea8cd..eeb6390d954f 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -18,6 +18,13 @@ Logging out from your account will remove all of @%s’s WordPress.com data from this device, including local drafts and local changes. This account has two step authentication enabled. Visit your security settings on WordPress.com and generate an application-specific password. + Preparing your site… + Fetching site info… + Setting the tagline… + Setting the theme… + Sorry, failed to properly create your site :( + Success! + Select categories Separate tags with commas diff --git a/libs/utils/WordPressUtils/build.gradle b/libs/utils/WordPressUtils/build.gradle index 2906a1f36df8..ca6ac552c1ea 100644 --- a/libs/utils/WordPressUtils/build.gradle +++ b/libs/utils/WordPressUtils/build.gradle @@ -21,6 +21,7 @@ dependencies { compile 'com.android.support:support-v13:25.3.1' compile 'com.android.support:design:25.3.1' compile 'com.android.support:recyclerview-v7:25.3.1' + compile 'org.greenrobot:eventbus:3.0.0' } android { diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForeground.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForeground.java new file mode 100644 index 000000000000..da3172c84db2 --- /dev/null +++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForeground.java @@ -0,0 +1,138 @@ +package org.wordpress.android.util; + +import android.app.Notification; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.IBinder; +import android.support.annotation.CallSuper; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationManagerCompat; + +import org.greenrobot.eventbus.EventBus; + +public abstract class AutoForeground extends Service { + + public static final int NOTIFICATION_ID_PROGRESS = 1; + public static final int NOTIFICATION_ID_SUCCESS = 2; + public static final int NOTIFICATION_ID_FAILURE = 3; + + public static class ServiceEventConnection { + private final ServiceConnection mServiceConnection; + + public ServiceEventConnection(Context context, Class clazz, Object client) { + EventBus.getDefault().register(client); + + mServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + // nothing here + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + // nothing here + } + }; + + context.bindService(new Intent(context, clazz), mServiceConnection, Context.BIND_AUTO_CREATE); + } + + public void disconnect(Context context, Object client) { + context.unbindService(mServiceConnection); + EventBus.getDefault().unregister(client); + } + } + + private class LocalBinder extends Binder {} + + private final IBinder mBinder = new LocalBinder(); + + private final Class mEventClass; + + protected abstract EventClass getCurrentStateEvent(); + protected abstract Notification getNotification(); + protected abstract boolean isInProgress(); + protected abstract boolean isError(); + + protected AutoForeground(Class eventClass) { + mEventClass = eventClass; + } + + @Nullable + @CallSuper + @Override + public IBinder onBind(Intent intent) { + notifyState(); + + return mBinder; + } + + @CallSuper + @Override + public void onRebind(Intent intent) { + super.onRebind(intent); + + background(); + notifyState(); + } + + @CallSuper + @Override + public boolean onUnbind(Intent intent) { + if (!hasConnectedClients()) { + promoteForeground(); + } + + return true; // call onRebind() if new clients connect + } + + private EventBus getEventBus() { + return EventBus.getDefault(); + } + + private boolean hasConnectedClients() { + return getEventBus().hasSubscriberForEvent(mEventClass); + } + + private void promoteForeground() { + if (isInProgress()) { + startForeground(NOTIFICATION_ID_PROGRESS, getNotification()); + } + } + + private void background() { + stopForeground(true); + } + + @CallSuper + protected void notifyState() { + if (hasConnectedClients()) { + // just send a message to the connected clients + getEventBus().post(getCurrentStateEvent()); + return; + } + + // ok, no connected clients so, update will be redirected to a notification + + if (isInProgress()) { + // operation still is progress so, update the notification + NotificationManagerCompat.from(this).notify(NOTIFICATION_ID_PROGRESS, getNotification()); + return; + } + + // operation has ended so, demote the Service to a background one + background(); + + // dismiss the sticky notification + NotificationManagerCompat.from(this).cancel(NOTIFICATION_ID_PROGRESS); + + // put out a simple success/failure notification + NotificationManagerCompat.from(this).notify( + isError() ? NOTIFICATION_ID_FAILURE : NOTIFICATION_ID_SUCCESS, + getNotification()); + } +}