Skip to content

Commit

Permalink
UPDATE: Migrate to LiveData based ViewModel, with Activity reference …
Browse files Browse the repository at this point in the history
…removed from AppListViewModel.
  • Loading branch information
oasisfeng committed May 14, 2018
1 parent 2c7ea17 commit 0fbc0ab
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 181 deletions.
7 changes: 4 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ dependencies {

// Any dependency with classes involved in data-binding in feature module must be declared with "api",
// since these classes are used in data-binding task of dependent module ("assembly").
api 'com.android.support:design:27.0.2'
api 'com.android.support:appcompat-v7:27.0.2'
api 'com.android.support:cardview-v7:27.0.2'
api 'android.arch.lifecycle:extensions:1.1.1'
api 'com.android.support:appcompat-v7:27.1.1'
api 'com.android.support:design:27.1.1'
api 'com.android.support:cardview-v7:27.1.1'
api 'eu.chainfire:libsuperuser:1.0.0.201510071325'
api 'uk.co.samuelwall:material-tap-target-prompt:2.0.1'
api(name: 'setup-wizard-lib-platform-release', ext: 'aar')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.oasisfeng.common.app;

import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
import android.databinding.ObservableList;
import android.support.annotation.Nullable;
import android.util.Log;

import com.oasisfeng.android.databinding.ObservableSortedList;
import com.oasisfeng.island.mobile.BR;
import com.oasisfeng.island.model.AppViewModel;

import java.util.HashMap;
Expand All @@ -19,7 +17,7 @@
*
* Created by Oasis on 2016/6/24.
*/
public class BaseAppListViewModel<T extends AppViewModel> extends BaseObservable {
public class BaseAppListViewModel<T extends AppViewModel> extends ViewModel {

// TODO: Rebuild the whole AbstractAppListViewModel to keep immutability?
protected void replaceApps(final List<T> apps) {
Expand All @@ -36,7 +34,7 @@ protected T putApp(final String pkg, final T app) {
Log.d(TAG, "Update in place: " + pkg);
final int index = mApps.indexOf(old_app_vm);
mApps.updateItemAt(index, app);
if (mSelection == old_app_vm) setSelection(app); // Keep the selection unchanged
if (mSelection.getValue() == old_app_vm) setSelection(app); // Keep the selection unchanged
} else {
Log.d(TAG, "Put: " + pkg);
mApps.add(app);
Expand Down Expand Up @@ -67,26 +65,23 @@ protected void removeApp(final String pkg) {
if (app == null) return;
Log.d(TAG, "Remove: " + pkg);
mApps.remove(app);
if (mSelection == app) setSelection(null);
if (mSelection.getValue() == app) setSelection(null);
}

protected boolean contains(final String pkg) { return mAppsByPackage.containsKey(pkg); }
protected int size() { return mApps.size(); }

/* Selection related */

@SuppressWarnings("WeakerAccess"/* Publicly accessed in app_detail.xml */) @Bindable public @Nullable T getSelection() { return mSelection; }

public void clearSelection() {
setSelection(null);
}

protected void setSelection(final T selection) {
if (mSelection == selection) return;
if (mSelection != null) mSelection.selected.set(false);
mSelection = selection;
if (selection != null) selection.selected.set(true);
notifyPropertyChanged(BR.selection);
if (mSelection.getValue() == selection) return;
if (mSelection.getValue() != null) mSelection.getValue().selected.setValue(false);
mSelection.setValue(selection);
if (selection != null) selection.selected.setValue(true);
}

protected BaseAppListViewModel(final Class<T> clazz) {
Expand All @@ -97,5 +92,5 @@ protected BaseAppListViewModel(final Class<T> clazz) {

private final ObservableSortedList<T> mApps;
private final Map<String, T> mAppsByPackage = new HashMap<>(); // Enforced constraint: apps from different users must not be shown at the same time.
private transient T mSelection;
public final MutableLiveData<T> mSelection = new MutableLiveData<>();
}
15 changes: 8 additions & 7 deletions app/src/main/java/com/oasisfeng/common/app/BaseAppViewModel.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.oasisfeng.common.app;

import android.databinding.BaseObservable;
import android.databinding.ObservableBoolean;
import android.arch.lifecycle.ViewModel;
import android.databinding.ObservableField;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;

import com.oasisfeng.android.ui.IconResizer;
import com.oasisfeng.androidx.lifecycle.NonNullMutableLiveData;
import com.oasisfeng.island.IslandApplication;
import com.oasisfeng.island.mobile.R;

import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;

Expand All @@ -16,11 +18,11 @@
*
* Created by Oasis on 2016/8/11.
*/
public class BaseAppViewModel extends BaseObservable {
public class BaseAppViewModel extends ViewModel {

public final AppInfo info;
public final ObservableField<Drawable> icon = new ObservableField<>();
public transient final ObservableBoolean selected = new ObservableBoolean(false);
public final ObservableField<Drawable> icon = new ObservableField<>(); // Issue in data-binding - MutableLiveData causes initially empty icons.
public transient final NonNullMutableLiveData<Boolean> selected = new NonNullMutableLiveData<>(false);
private volatile boolean mIconLoadingStarted;

public boolean isSystem() { return (info.flags & FLAG_SYSTEM) != 0; }
Expand All @@ -44,6 +46,5 @@ protected boolean isContentSameAs(final BaseAppViewModel another) {
return TextUtils.equals(info.getLabel(), another.info.getLabel());
}


private final static IconResizer sIconResizer = new IconResizer(); // TODO: Avoid static
private final static IconResizer sIconResizer = new IconResizer((int) IslandApplication.$().getResources().getDimension(R.dimen.app_icon_size)); // TODO: Avoid static
}
6 changes: 3 additions & 3 deletions app/src/main/java/com/oasisfeng/island/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.oasisfeng.island;

import android.app.Activity;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
Expand All @@ -13,6 +12,7 @@
import android.os.Bundle;
import android.os.SystemClock;
import android.os.UserHandle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;

import com.oasisfeng.android.base.Scopes;
Expand All @@ -35,7 +35,7 @@
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.DONT_KILL_APP;

public class MainActivity extends Activity {
public class MainActivity extends FragmentActivity {

@Override protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand Down Expand Up @@ -103,7 +103,7 @@ private void onCreateInProfile() {
private void startMainUi(final Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
if (savedInstanceState != null) return;
getFragmentManager().beginTransaction().replace(R.id.container, new AppListFragment()).commit();
getSupportFragmentManager().beginTransaction().replace(R.id.container, new AppListFragment()).commit();
performOverallAnalyticsIfNeeded();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.app.Activity;
import android.app.Fragment;
import android.app.admin.DevicePolicyManager;
import android.arch.lifecycle.ViewModelProviders;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.databinding.Observable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
Expand All @@ -21,6 +20,8 @@
import android.provider.DocumentsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
Expand All @@ -45,7 +46,6 @@
import com.oasisfeng.island.engine.IIslandManager;
import com.oasisfeng.island.engine.IslandManager;
import com.oasisfeng.island.guide.UserGuide;
import com.oasisfeng.island.mobile.BR;
import com.oasisfeng.island.mobile.BuildConfig;
import com.oasisfeng.island.mobile.R;
import com.oasisfeng.island.mobile.databinding.AppListBinding;
Expand All @@ -62,8 +62,11 @@

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;

import javax.annotation.ParametersAreNonnullByDefault;

import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.Context.BIND_AUTO_CREATE;
import static android.content.Intent.ACTION_OPEN_DOCUMENT;
Expand All @@ -79,14 +82,15 @@
import static com.oasisfeng.island.analytics.Analytics.Param.ITEM_ID;

/** The main UI - App list */
@ParametersAreNonnullByDefault
public class AppListFragment extends Fragment {

@Override public void onCreate(final Bundle savedInstanceState) {
@Override public void onCreate(final @Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
final Activity activity = getActivity();
final FragmentActivity activity = getActivity();
mShuttleContext = new ShuttleContext(activity);
mViewModel = new AppListViewModel();
mViewModel = ViewModelProviders.of(this).get(AppListViewModel.class);
mViewModel.mProfileController = IslandManager.NULL;
mUserGuide = UserGuide.initializeIfNeeded(activity, mViewModel);
IslandAppListProvider.getInstance(activity).registerObserver(mAppChangeObserver);
Expand Down Expand Up @@ -115,7 +119,6 @@ public class AppListFragment extends Fragment {

@Override public void onDestroy() {
IslandAppListProvider.getInstance(getActivity()).unregisterObserver(mAppChangeObserver);
mViewModel.removeOnPropertyChangedCallback(onPropertyChangedCallback);
super.onDestroy();
}

Expand Down Expand Up @@ -155,22 +158,19 @@ public class AppListFragment extends Fragment {
}
};

private final Observable.OnPropertyChangedCallback onPropertyChangedCallback = new Observable.OnPropertyChangedCallback() { @Override public void onPropertyChanged(final Observable observable, final int var) {
if (var == BR.selection) invalidateOptionsMenu();
}};

private void invalidateOptionsMenu() {
final Activity activity = getActivity();
if (activity != null) activity.invalidateOptionsMenu();
}

@Nullable @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final @Nullable Bundle saved_state) {
final Activity activity = getActivity();
@Nullable @Override public View onCreateView(final LayoutInflater inflater, final @Nullable ViewGroup container, final @Nullable Bundle saved_state) {
final Activity activity = Objects.requireNonNull(getActivity());
mBinding = AppListBinding.inflate(inflater, container, false);
mBinding.setApps(mViewModel);
mBinding.setGuide(mUserGuide);
mBinding.setLifecycleOwner(this);
mViewModel.attach(activity, mBinding.details.toolbar.getMenu(), saved_state);
mViewModel.addOnPropertyChangedCallback(onPropertyChangedCallback);
mViewModel.mSelection.observe(this, selection -> invalidateOptionsMenu());

if (! Services.bind(activity, IIslandManager.class, mIslandManagerConnection = new ServiceConnection() {
@Override public void onServiceConnected(final ComponentName name, final IBinder service) {
Expand All @@ -186,7 +186,7 @@ private void invalidateOptionsMenu() {
mBinding.filters.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override public void onItemSelected(final AdapterView<?> parent, final View view, final int position, final long id) {
if (getActivity() == null) return;
mViewModel.setFilterPrimaryChoice(position);
mViewModel.mFilterPrimaryChoice.setValue(position);
}
@Override public void onNothingSelected(final AdapterView<?> parent) {}
});
Expand All @@ -196,7 +196,8 @@ private void invalidateOptionsMenu() {

@Override public void onDestroyView() {
if (mIslandManagerConnection != null) {
getActivity().unbindService(mIslandManagerConnection);
final FragmentActivity activity = getActivity();
if (activity != null) activity.unbindService(mIslandManagerConnection);
mIslandManagerConnection = null;
}
super.onDestroyView();
Expand All @@ -210,7 +211,7 @@ private void invalidateOptionsMenu() {
final Context context = getActivity();
final MenuItem.OnMenuItemClickListener tip = mUserGuide == null ? null : mUserGuide.getAvailableTip();
menu.findItem(R.id.menu_tip).setVisible(tip != null).setOnMenuItemClickListener(tip);
menu.findItem(R.id.menu_search).setVisible(mViewModel.getSelection() == null).setOnActionExpandListener(mOnActionExpandListener);
menu.findItem(R.id.menu_search).setVisible(mViewModel.mSelection.getValue() == null).setOnActionExpandListener(mOnActionExpandListener);
menu.findItem(R.id.menu_files).setVisible(context != null && Users.hasProfile() &&
(! Permissions.has(context, WRITE_EXTERNAL_STORAGE) || findFileBrowser(context) != null));
menu.findItem(R.id.menu_show_system).setChecked(mViewModel.areSystemAppsIncluded());
Expand Down Expand Up @@ -252,7 +253,7 @@ private void invalidateOptionsMenu() {
}

private void requestPermissionAndLaunchFilesExplorerInIsland() {
final Context context = getActivity();
final Context context = Objects.requireNonNull(getActivity());
if (context.checkPermission(WRITE_EXTERNAL_STORAGE, Process.myPid(), Process.myUid()) == PERMISSION_GRANTED) {
final ListenableFuture<Boolean> future = MethodShuttle.runInProfile(context, () -> {
final Intent intent = findFileBrowser(context);
Expand Down
30 changes: 14 additions & 16 deletions app/src/main/java/com/oasisfeng/island/guide/UserGuide.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.oasisfeng.island.guide;

import android.app.Activity;
import android.arch.lifecycle.Observer;
import android.databinding.BindingAdapter;
import android.databinding.Observable;
import android.databinding.ObservableField;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.FragmentActivity;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
Expand All @@ -16,7 +17,6 @@
import com.oasisfeng.common.app.AppListProvider;
import com.oasisfeng.island.data.IslandAppInfo;
import com.oasisfeng.island.data.IslandAppListProvider;
import com.oasisfeng.island.mobile.BR;
import com.oasisfeng.island.mobile.R;
import com.oasisfeng.island.model.AppListViewModel;
import com.oasisfeng.island.model.AppListViewModel.Filter;
Expand Down Expand Up @@ -92,26 +92,24 @@ private static View findProperTarget(final View view) {
return view;
}

public static @Nullable UserGuide initializeIfNeeded(final Activity activity, final AppListViewModel vm) {
public static @Nullable UserGuide initializeIfNeeded(final FragmentActivity activity, final AppListViewModel vm) {
final Scopes.Scope scope = Scopes.app(activity);

final boolean action_tips_pending = UserGuide.anyActionTipPending(scope);
final boolean action_tips_pending = anyActionTipPending(scope);
if (! action_tips_pending && scope.isMarked(SCOPE_KEY_TIP_FILTER)) return null;
final UserGuide guide = new UserGuide(activity, scope);

vm.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() { @Override public void onPropertyChanged(final Observable sender, final int property) {
if (property == BR.filterPrimaryChoice) {
final AppListViewModel vm = (AppListViewModel) sender;
final Filter filter = vm.getFilterPrimaryOptions().get(vm.getFilterPrimaryChoice()).parent();
if (guide.mFilter != null && filter != guide.mFilter) {
scope.markOnly(SCOPE_KEY_TIP_FILTER); // User just switched filter, no need to show tip for filter switching.
activity.invalidateOptionsMenu();
if (! UserGuide.anyActionTipPending(scope))
vm.removeOnPropertyChangedCallback(this); // No need to monitor filter or selection any more.
}
guide.mFilter = filter;
} else if (property == BR.selection) guide.mAppSelection = ((AppListViewModel) sender).getSelection();
vm.mFilterPrimaryChoice.observe(activity, new Observer<Integer>() { @Override public void onChanged(final Integer choice) {
final Filter filter = vm.mFilterPrimaryOptions.getValue().get(choice).parent();
if (guide.mFilter != null && filter != guide.mFilter) {
scope.markOnly(SCOPE_KEY_TIP_FILTER); // User just switched filter, no need to show tip for filter switching.
activity.invalidateOptionsMenu();
if (! anyActionTipPending(scope))
vm.mFilterPrimaryChoice.removeObserver(this); // No need to monitor filter or selection any more.
}
guide.mFilter = filter;
}});
vm.mSelection.observe(activity, selection -> guide.mAppSelection = selection);
if (action_tips_pending) {
final IslandAppListProvider provider = IslandAppListProvider.getInstance(activity);
provider.registerObserver(new AppListProvider.PackageChangeObserver<IslandAppInfo>() {
Expand Down
Loading

0 comments on commit 0fbc0ab

Please sign in to comment.