diff --git a/sample/src/main/java/net/grandcentrix/thirtyinch/sample/HelloWorldPresenter.java b/sample/src/main/java/net/grandcentrix/thirtyinch/sample/HelloWorldPresenter.java index 59604239..7f08d0d7 100644 --- a/sample/src/main/java/net/grandcentrix/thirtyinch/sample/HelloWorldPresenter.java +++ b/sample/src/main/java/net/grandcentrix/thirtyinch/sample/HelloWorldPresenter.java @@ -20,7 +20,9 @@ import net.grandcentrix.thirtyinch.rx.RxTiPresenterUtils; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import java.math.BigInteger; import java.util.concurrent.TimeUnit; import rx.Observable; @@ -66,7 +68,16 @@ public void call(final Void aVoid) { protected void onCreate() { super.onCreate(); - mText.onNext("Hello World!"); + final byte[] state = getPersistentState(); + if (state != null) { + mCounter = new BigInteger(state).intValue(); + } + + if (mCounter == 0) { + mText.onNext("Click the Button"); + } else { + mText.onNext("Count: " + mCounter); + } rxSubscriptionHelper.manageSubscription(Observable.interval(0, 1, TimeUnit.SECONDS) .compose(RxTiPresenterUtils.deliverLatestToView(this)) @@ -105,6 +116,13 @@ public void call(final Integer integer) { .subscribe()); } + @Nullable + @Override + protected byte[] onSavePersistentState() { + final byte[] bytes = BigInteger.valueOf(mCounter).toByteArray(); + return bytes; + } + /** * fake a heavy calculation */ diff --git a/sample/src/main/java/net/grandcentrix/thirtyinch/sample/SampleApp.java b/sample/src/main/java/net/grandcentrix/thirtyinch/sample/SampleApp.java index 7d0d3c7f..c64fcdb9 100644 --- a/sample/src/main/java/net/grandcentrix/thirtyinch/sample/SampleApp.java +++ b/sample/src/main/java/net/grandcentrix/thirtyinch/sample/SampleApp.java @@ -16,7 +16,10 @@ package net.grandcentrix.thirtyinch.sample; +import net.grandcentrix.thirtyinch.TiConfiguration; import net.grandcentrix.thirtyinch.TiLog; +import net.grandcentrix.thirtyinch.TiPresenter; +import net.grandcentrix.thirtyinch.serialize.PresenterStateSerializer; import android.app.Application; @@ -28,5 +31,9 @@ public void onCreate() { // log ThirtyInch output with logcat TiLog.setLogger(TiLog.LOGCAT); + + TiPresenter.setDefaultConfig(new TiConfiguration.Builder() + .setPresenterSerializer(new PresenterStateSerializer(this)) + .build()); } } diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiConfiguration.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiConfiguration.java index 94e7d97b..e8511c2b 100644 --- a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiConfiguration.java +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiConfiguration.java @@ -23,6 +23,7 @@ import android.app.Activity; import android.app.Application; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; /** @@ -144,6 +145,12 @@ public Builder setDistinctUntilChangedInterceptorEnabled(final boolean enabled) return this; } + //TODO documentation + public Builder setPresenterSerializer(TiPresenterSerializer serializer) { + mConfig.mPresenterSerializer = serializer; + return this; + } + /** * When set to true the {@link TiPresenter} will be restored when the {@link * Activity} recreates due to a configuration changes such as the orientation change. @@ -197,7 +204,6 @@ public Builder setUseStaticSaviorToRetain(final boolean enabled) { mConfig.mUseStaticSaviorToRetain = enabled; return this; } - } public static final TiConfiguration DEFAULT = new Builder().build(); @@ -206,6 +212,8 @@ public Builder setUseStaticSaviorToRetain(final boolean enabled) { private boolean mDistinctUntilChangedInterceptorEnabled = true; + private TiPresenterSerializer mPresenterSerializer; + private boolean mRetainPresenter = true; private boolean mUseStaticSaviorToRetain = true; @@ -216,6 +224,11 @@ public Builder setUseStaticSaviorToRetain(final boolean enabled) { private TiConfiguration() { } + @Nullable + public TiPresenterSerializer getPresenterSerializer() { + return mPresenterSerializer; + } + public boolean isCallOnMainThreadInterceptorEnabled() { return mCallOnMainThreadInterceptorEnabled; } diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java index 56ac2d21..e0602ce6 100644 --- a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java @@ -28,6 +28,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Queue; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; /** @@ -85,6 +90,13 @@ public enum State { private final TiConfiguration mConfig; + private String mId; + + private final ExecutorService mPersistentStateExecutorService = + Executors.newSingleThreadExecutor(); + + private Future mPersistentStateFuture; + private LinkedBlockingQueue> mPostponedViewActions = new LinkedBlockingQueue<>(); private State mState = State.INITIALIZED; @@ -95,11 +107,11 @@ public static void setDefaultConfig(final TiConfiguration config) { sDefaultConfig = config; } - public TiPresenter() { this(sDefaultConfig); } + /** * Constructs a presenter with a different configuration then the default one. Change the * default configuration with {@link #setDefaultConfig(TiConfiguration)} @@ -135,7 +147,6 @@ public void onRemove() { }; } - /** * bind a new view to this presenter. * @@ -237,6 +248,11 @@ public final void destroy() { // release everything, no new states will be posted mLifecycleObservers.clear(); + + final TiPresenterSerializer serializer = getConfig().getPresenterSerializer(); + if (serializer != null) { + serializer.free(this); + } } /** @@ -272,6 +288,11 @@ public final void detachView() { mView = null; } + public void generatNewId() { + final String id = getClass().getSimpleName() + ":" + hashCode() + ":" + System.nanoTime(); + setId(id); + } + /** * @return the presenter configuration */ @@ -280,6 +301,13 @@ public TiConfiguration getConfig() { return mConfig; } + /** + * @return A unique id of this instance. + */ + public final String getId() { + return mId; + } + /** * @return the current lifecycle state */ @@ -315,6 +343,51 @@ public boolean isViewAttached() { return mState == State.VIEW_ATTACHED; } + //TODO documentation + public void persistState() { + mPersistentStateExecutorService.submit(new Runnable() { + @Override + public void run() { + TiPresenterSerializer serializer = mConfig.getPresenterSerializer(); + if (serializer != null) { + final byte[] data = onSavePersistentState(); + serializer.serialize(TiPresenter.this, data); + } + } + }); + } + + @NonNull + public Future prefetchPersistentState() { + mPersistentStateFuture = mPersistentStateExecutorService.submit(new Callable() { + @Override + public byte[] call() throws Exception { + final TiPresenterSerializer serializer = mConfig.getPresenterSerializer(); + if (serializer != null) { + return serializer.deserialize(TiPresenter.this); + } + return null; + } + }); + + return mPersistentStateFuture; + } + + /** + * the id can only be set once + */ + public void setId(@NonNull final String id) { + //noinspection ConstantConditions + if (id == null) { + throw new IllegalArgumentException("the id cannot be null"); + } + if (mId == null) { + mId = id; + } else { + throw new IllegalArgumentException("the id can only be set once"); + } + } + @Override public String toString() { final String viewName; @@ -329,6 +402,24 @@ public String toString() { + "{view = " + viewName + "}"; } + //TODO documentation + @Nullable + protected byte[] getPersistentState() { + Future future = mPersistentStateFuture; + if (future == null) { + future = mPersistentStateFuture = prefetchPersistentState(); + } + try { + // wait for result even when not prefetched + return future.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + return null; + } + /** * Gives access to the postponed actions while the view is not attached. * @@ -400,6 +491,12 @@ protected void onDetachView() { mCalled = true; } + //TODO documentation + @Nullable + protected byte[] onSavePersistentState() { + return null; + } + /** * @deprecated use {@link #onDetachView()} instead */ diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenterSerializer.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenterSerializer.java new file mode 100644 index 00000000..c4078de5 --- /dev/null +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenterSerializer.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 grandcentrix GmbH + * 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 + * 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 net.grandcentrix.thirtyinch; + +import android.support.annotation.NonNull; + +public interface TiPresenterSerializer { + + @NonNull + byte[] deserialize(@NonNull TiPresenter presenter); + + void free(@NonNull TiPresenter presenter); + + void serialize(@NonNull final TiPresenter presenter, final byte[] data); +} diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/PresenterSavior.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/PresenterSavior.java index 11486b93..2f6e17df 100644 --- a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/PresenterSavior.java +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/PresenterSavior.java @@ -52,7 +52,7 @@ public TiPresenter recover(final String id) { } public String safe(@NonNull final TiPresenter presenter) { - final String id = generateId(presenter); + final String id = presenter.getId(); TiLog.v(TAG, "safe presenter with id " + id + " " + presenter); mPresenters.put(id, presenter); return id; @@ -62,10 +62,4 @@ public String safe(@NonNull final TiPresenter presenter) { void clear() { mPresenters.clear(); } - - private String generateId(@NonNull final TiPresenter presenter) { - return presenter.getClass().getSimpleName() - + ":" + presenter.hashCode() - + ":" + System.nanoTime(); - } } diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/TiActivityDelegate.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/TiActivityDelegate.java index a6141c44..26716719 100644 --- a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/TiActivityDelegate.java +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/TiActivityDelegate.java @@ -61,12 +61,6 @@ public class TiActivityDelegate

, V extends TiView> */ private P mPresenter; - /** - * The id of the presenter this view got attached to. Will be stored in the savedInstanceState - * to find the same presenter after the Activity got recreated. - */ - private String mPresenterId; - private final TiPresenterProvider

mPresenterProvider; private final DelegatedTiActivity

mTiActivity; @@ -136,10 +130,10 @@ public void onCreate_afterSuper(final Bundle savedInstanceState) { "recovered Presenter from lastCustomNonConfigurationInstance " + mPresenter); } + String recoveredPresenterId = null; // try to recover with the PresenterSavior if (savedInstanceState != null) { - final String recoveredPresenterId = savedInstanceState - .getString(SAVED_STATE_PRESENTER_ID); + recoveredPresenterId = savedInstanceState.getString(SAVED_STATE_PRESENTER_ID); if (mPresenter == null) { if (recoveredPresenterId != null) { @@ -160,23 +154,26 @@ public void onCreate_afterSuper(final Bundle savedInstanceState) { TiLog.i(mLogTag.getLoggingTag(), "could not recover the Presenter " + "although it's not the first start of the Activity. This is normal when " + "configured as .setRetainPresenterEnabled(false)."); - } else { - // save recovered presenter with new id. No other instance of this activity, - // holding the presenter before, is now able to remove the reference to - // this presenter from the savior - PresenterSavior.INSTANCE.free(recoveredPresenterId); - mPresenterId = PresenterSavior.INSTANCE.safe(mPresenter); } } if (mPresenter == null) { // could not recover, create a new presenter mPresenter = mPresenterProvider.providePresenter(); + + if (recoveredPresenterId == null) { + mPresenter.generatNewId(); + } else { + mPresenter.setId(recoveredPresenterId); + mPresenter.prefetchPersistentState(); + } + TiLog.v(mLogTag.getLoggingTag(), "created Presenter: " + mPresenter); final TiConfiguration config = mPresenter.getConfig(); if (config.shouldRetainPresenter() && config.useStaticSaviorToRetain()) { - mPresenterId = PresenterSavior.INSTANCE.safe(mPresenter); + PresenterSavior.INSTANCE.safe(mPresenter); } + mPresenter.create(); } @@ -227,7 +224,7 @@ public void onDestroy_afterSuper() { if (destroyPresenter) { mPresenter.destroy(); - PresenterSavior.INSTANCE.free(mPresenterId); + PresenterSavior.INSTANCE.free(mPresenter.getId()); } else { TiLog.v(mLogTag.getLoggingTag(), "not destroying " + mPresenter + " which will be reused by the next Activity instance, recreating..."); @@ -235,7 +232,8 @@ public void onDestroy_afterSuper() { } public void onSaveInstanceState_afterSuper(final Bundle outState) { - outState.putString(SAVED_STATE_PRESENTER_ID, mPresenterId); + outState.putString(SAVED_STATE_PRESENTER_ID, mPresenter.getId()); + mPresenter.persistState(); } public void onStart_afterSuper() { diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/TiFragmentDelegate.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/TiFragmentDelegate.java index cb25366b..f2810894 100644 --- a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/TiFragmentDelegate.java +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/TiFragmentDelegate.java @@ -102,10 +102,11 @@ public void onCreateView_beforeSuper(final LayoutInflater inflater, } public void onCreate_afterSuper(final Bundle savedInstanceState) { + final String recoveredPresenterId; if (mPresenter == null && savedInstanceState != null) { // recover with Savior // this should always work. - final String recoveredPresenterId = savedInstanceState + recoveredPresenterId = savedInstanceState .getString(SAVED_STATE_PRESENTER_ID); if (recoveredPresenterId != null) { TiLog.v(mLogTag.getLoggingTag(), @@ -127,6 +128,7 @@ public void onCreate_afterSuper(final Bundle savedInstanceState) { mPresenter = mPresenterProvider.providePresenter(); TiLog.v(mLogTag.getLoggingTag(), "created Presenter: " + mPresenter); final TiConfiguration config = mPresenter.getConfig(); + if (config.shouldRetainPresenter() && config.useStaticSaviorToRetain()) { mPresenterId = PresenterSavior.INSTANCE.safe(mPresenter); } @@ -191,6 +193,7 @@ public void onDestroy_afterSuper() { if (destroyPresenter) { mPresenter.destroy(); PresenterSavior.INSTANCE.free(mPresenterId); + } else { TiLog.v(mLogTag.getLoggingTag(), "not destroying " + mPresenter + " which will be reused by the next Activity instance, recreating..."); @@ -199,6 +202,7 @@ public void onDestroy_afterSuper() { public void onSaveInstanceState_afterSuper(final Bundle outState) { outState.putString(SAVED_STATE_PRESENTER_ID, mPresenterId); + mPresenter.persistState(); } public void onStart_afterSuper() { diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/serialize/FileUtils.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/serialize/FileUtils.java new file mode 100644 index 00000000..d78a2d2a --- /dev/null +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/serialize/FileUtils.java @@ -0,0 +1,106 @@ +package net.grandcentrix.thirtyinch.serialize; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; + +/** + * Created by rwondratschek on 12/14/16. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +/*package*/ final class FileUtils { + + public static void close(Closeable closeable) { + if (closeable != null) { + if (closeable instanceof OutputStream) { + try { + ((OutputStream) closeable).flush(); + } catch (IOException ignored) { + } + } + + if (closeable instanceof FileOutputStream) { + try { + ((FileOutputStream) closeable).getFD().sync(); + } catch (IOException ignored) { + } + } + + try { + closeable.close(); + } catch (IOException ignored) { + } + } + } + + public static void delete(File file) throws IOException { + if (!file.exists()) { + return; + } + if (file.isDirectory()) { + File[] files = file.listFiles(); + for (File file1 : files) { + delete(file1); + } + } + if (!file.delete()) { + throw new IOException("could not delete file " + file); + } + } + + public static byte[] readFile(File file) throws IOException { + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + byte[] buffer = new byte[(int) file.length()]; + + int read; + int offset = 0; + + while ((read = fis.read(buffer, offset, buffer.length - offset)) >= 0 + && offset < buffer.length) { + offset += read; + } + + if (offset != buffer.length) { + return Arrays.copyOf(buffer, offset); + } else { + return buffer; + } + + } finally { + close(fis); + } + } + + public static void writeFile(File file, byte[] data) throws IOException { + if (file == null || data == null) { + throw new IllegalArgumentException(); + } + + if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) { + throw new IOException("Could not load parent directory for " + file.getAbsolutePath()); + } + + if (!file.exists() && !file.createNewFile()) { + throw new IOException("Could not load file for " + file.getAbsolutePath()); + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file); + fos.write(data); + + } finally { + close(fos); + } + } + + private FileUtils() { + new AssertionError("no instances"); + } +} diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/serialize/PresenterStateSerializer.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/serialize/PresenterStateSerializer.java new file mode 100644 index 00000000..d2e6540e --- /dev/null +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/serialize/PresenterStateSerializer.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2016 grandcentrix GmbH + * 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 + * 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 net.grandcentrix.thirtyinch.serialize; + + +import net.grandcentrix.thirtyinch.TiLog; +import net.grandcentrix.thirtyinch.TiPresenter; +import net.grandcentrix.thirtyinch.TiPresenterSerializer; + +import android.content.Context; +import android.support.annotation.NonNull; + +import java.io.File; +import java.io.IOException; + +public class PresenterStateSerializer implements TiPresenterSerializer { + + private static final String TAG = PresenterStateSerializer.class.getSimpleName(); + + private final File mCacheDir; + + public PresenterStateSerializer(@NonNull final Context context) { + mCacheDir = new File(context.getCacheDir(), "TiPresenterStates"); + } + + @NonNull + @Override + public byte[] deserialize(@NonNull final TiPresenter presenter) { + final File file = getStateFile(presenter); + TiLog.v(TAG, "deserialize " + file.getName()); + try { + return FileUtils.readFile(file); + } catch (IOException e) { + TiLog.v(TAG, "could not read from file " + file.getName()); + e.printStackTrace(); + + } + return null; + } + + @Override + public void free(@NonNull final TiPresenter presenter) { + final File file = getStateFile(presenter); + TiLog.v(TAG, "free " + file.getName()); + try { + FileUtils.delete(file); + } catch (IOException e) { + TiLog.v(TAG, "could not delete file " + file.getName()); + e.printStackTrace(); + } + } + + @Override + public void serialize(@NonNull final TiPresenter presenter, final byte[] data) { + final File file = getStateFile(presenter); + TiLog.v(TAG, "serialize " + file.getName()); + try { + FileUtils.writeFile(file, data); + } catch (IOException e) { + TiLog.v(TAG, "could not write to file " + file.getName()); + e.printStackTrace(); + } + } + + @NonNull + private File getStateFile(final @NonNull TiPresenter presenter) { + return new File(mCacheDir, presenter.getId()); + } +}