Merge branch 'new-ui--feedback-from-alpha' into 'master'

Implement a handful of UX tweaks in response to alpha feedback

Closes #879

See merge request !464
This commit is contained in:
Hans-Christoph Steiner 2017-04-06 10:39:49 +00:00
commit e28e29455e
16 changed files with 122 additions and 69 deletions

View File

@ -9,8 +9,10 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.content.LocalBroadcastManager;
@ -188,6 +190,13 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
if (app != null) {
visiblePackageName = app.packageName;
}
appObserver = new AppObserver(new Handler());
getContentResolver().registerContentObserver(
AppProvider.getHighestPriorityMetadataUri(app.packageName),
true,
appObserver);
updateNotificationsForApp();
}
@ -195,6 +204,8 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
super.onStop();
visiblePackageName = null;
getContentResolver().unregisterContentObserver(appObserver);
// When leaving the app details, make sure to refresh app status for this app, since
// we might want to show notifications for it now.
updateNotificationsForApp();
@ -432,7 +443,9 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
case Installer.ACTION_INSTALL_COMPLETE:
adapter.clearProgress();
unregisterInstallReceiver();
onAppChanged();
// Don't try and update the view here, because the InstalledAppProviderService
// hasn't had time to do its thing and mark the app as installed. Instead, we
// wait for that service to notify us, and then we will respond in appObserver.
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
adapter.clearProgress();
@ -457,8 +470,14 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
unregisterInstallReceiver();
break;
case Installer.ACTION_INSTALL_USER_INTERACTION:
PendingIntent installPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
if (!isAppVisible(apk.packageName)) {
Utils.debugLog(TAG, "Ignore request for user interaction from installer, because " + apk.packageName + " is no longer showing.");
break;
}
Utils.debugLog(TAG, "Automatically showing package manager for " + apk.packageName + " as it is being viewed by the user.");
PendingIntent installPendingIntent = intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
installPendingIntent.send();
@ -649,4 +668,26 @@ public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog
}
startUninstall();
}
// observer to update view when package has been installed/deleted
private AppObserver appObserver;
class AppObserver extends ContentObserver {
AppObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
onChange(selfChange, null);
}
@Override
public void onChange(boolean selfChange, Uri uri) {
onAppChanged();
}
}
}

View File

@ -330,14 +330,13 @@ class NotificationHelper {
App app = entry.app;
AppUpdateStatusManager.Status status = entry.status;
int iconSmall = R.drawable.ic_launcher;
Bitmap iconLarge = getLargeIconForEntry(entry);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)
.setAutoCancel(true)
.setContentTitle(getSingleItemTitleString(app, status))
.setContentText(getSingleItemContentString(app, status))
.setSmallIcon(iconSmall)
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.fdroid_blue))
.setLargeIcon(iconLarge)
.setLocalOnly(true)
@ -416,7 +415,7 @@ class NotificationHelper {
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)
.setAutoCancel(!useStackedNotifications())
.setSmallIcon(R.drawable.ic_launcher)
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.fdroid_blue))
.setContentTitle(title)
.setContentText(text)
@ -444,7 +443,7 @@ class NotificationHelper {
new NotificationCompat.Builder(context)
.setAutoCancel(true)
.setLargeIcon(iconLarge)
.setSmallIcon(R.drawable.ic_launcher)
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.fdroid_blue))
.setContentTitle(app.name)
.setContentText(context.getString(R.string.notification_content_single_installed))
@ -491,7 +490,7 @@ class NotificationHelper {
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)
.setAutoCancel(!useStackedNotifications())
.setSmallIcon(R.drawable.ic_launcher)
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.fdroid_blue))
.setContentTitle(title)
.setContentText(text)

View File

@ -21,7 +21,6 @@ import java.io.FilenameFilter;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
@ -64,7 +63,7 @@ public class InstalledAppProviderService extends IntentService {
public void onCreate() {
super.onCreate();
notifyEvents = PublishSubject.create();
notifyEvents.debounce(1, TimeUnit.SECONDS)
notifyEvents
.subscribeOn(Schedulers.newThread())
.subscribe(new Action1<Void>() {
@Override

View File

@ -35,7 +35,6 @@ import android.widget.Toast;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
@ -413,15 +412,12 @@ public class AppDetailsRecyclerViewAdapter
} else {
lastUpdateView.setVisibility(View.GONE);
}
Apk suggestedApk = getSuggestedApk();
// TODO replace this whatsNew test code with what comes from suggestedApk once that exists
//if (suggestedApk == null || TextUtils.isEmpty(suggestedApk.whatsNew)) {
// TODO populate whatsNew with suggestedApk.whatsNew once that exists
String whatsNew = null;
if (BuildConfig.DEBUG) {
if (Math.random() > 0.5) {
whatsNew = "This section will contain the 'what's new' information for the apk.\n\n\t• Bug fixes.";
}
}
//noinspection ConstantConditions
if (suggestedApk == null || TextUtils.isEmpty(whatsNew)) {
whatsNewView.setVisibility(View.GONE);
} else {
@ -431,7 +427,7 @@ public class AppDetailsRecyclerViewAdapter
StringBuilder sbWhatsNew = new StringBuilder();
sbWhatsNew.append(whatsNewView.getContext().getString(R.string.details_new_in_version, suggestedApk.versionName).toUpperCase(locale));
sbWhatsNew.append("\n\n");
sbWhatsNew.append("This section will contain the 'what's new' information for the apk.\n\n\t• Bug fixes.");
sbWhatsNew.append(whatsNew);
whatsNewView.setText(sbWhatsNew);
whatsNewView.setVisibility(View.VISIBLE);
}

View File

@ -121,7 +121,7 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
Schema.AppMetadataTable.Cols.ALL,
null,
null,
null
Schema.AppMetadataTable.Cols.NAME
);
}

View File

@ -448,14 +448,15 @@ public class AppListItemController extends RecyclerView.ViewHolder {
private final BroadcastReceiver onInstallAction = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (currentApp == null || installButton == null) {
Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
if (currentApp == null || !TextUtils.equals(apk.packageName, currentApp.packageName)) {
return;
}
configureAppName(currentApp);
configureActionButton(currentApp);
Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
if (!TextUtils.equals(apk.packageName, currentApp.packageName)) {
if (installButton == null) {
return;
}

View File

@ -153,7 +153,7 @@ public class CategoryController extends RecyclerView.ViewHolder implements Loade
},
null,
null,
null
Schema.AppMetadataTable.Cols.NAME
);
}
}

View File

@ -5,13 +5,9 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
@ -32,9 +28,7 @@ import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.UriCompat;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.ManageReposActivity;
import org.fdroid.fdroid.views.apps.AppListActivity;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
@ -53,8 +47,7 @@ import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
* When switching from one screen to the next, we stay within this activity. The new screen will
* get inflated (if required)
*/
public class MainActivity extends AppCompatActivity implements BottomNavigationBar.OnTabSelectedListener,
LoaderManager.LoaderCallbacks<Cursor> {
public class MainActivity extends AppCompatActivity implements BottomNavigationBar.OnTabSelectedListener {
private static final String TAG = "MainActivity";
@ -66,8 +59,6 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
private static final String STATE_SELECTED_MENU_ID = "selectedMenuId";
private static final int LOADER_NUM_UPDATES = 1;
private static final int REQUEST_SWAP = 3;
private RecyclerView pager;
@ -89,7 +80,7 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
pager.setLayoutManager(new NonScrollingHorizontalLayoutManager(this));
pager.setAdapter(adapter);
updatesBadge = new BadgeItem();
updatesBadge = new BadgeItem().hide(false);
bottomNavigation = (BottomNavigationBar) findViewById(R.id.bottom_navigation);
bottomNavigation.setTabSelectedListener(this)
@ -105,8 +96,8 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
.initialise();
IntentFilter updateableAppsFilter = new IntentFilter(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED);
updateableAppsFilter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(onUpdateableAppsChanged, updateableAppsFilter);
getSupportLoaderManager().initLoader(LOADER_NUM_UPDATES, null, this);
if (savedInstanceState != null) {
selectedMenuId = savedInstanceState.getInt(STATE_SELECTED_MENU_ID, R.id.whats_new);
@ -319,22 +310,6 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
}
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri = AppProvider.getCanUpdateUri();
String[] projection = new String[]{Schema.AppMetadataTable.Cols._COUNT};
return new CursorLoader(this, uri, projection, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
cursor.moveToFirst();
int canUpdateCount = cursor.getInt(cursor.getColumnIndex(Schema.AppMetadataTable.Cols._COUNT));
cursor.close();
refreshUpdatesBadge(canUpdateCount);
}
private void refreshUpdatesBadge(int canUpdateCount) {
if (canUpdateCount == 0) {
updatesBadge.hide(true);
@ -344,11 +319,6 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
private static class NonScrollingHorizontalLayoutManager extends LinearLayoutManager {
NonScrollingHorizontalLayoutManager(Context context) {
super(context, LinearLayoutManager.HORIZONTAL, false);
@ -365,11 +335,43 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationB
}
}
/**
* There are a bunch of reasons why we would get notified about app statuses.
* The ones we are interested in are those which would result in the "items requiring user interaction"
* to increase or decrease:
* * Bulk updates of ready-to-install-apps (relating to {@link org.fdroid.fdroid.AppUpdateStatusService}.
* * Change in status to:
* * {@link AppUpdateStatusManager.Status#ReadyToInstall} (Causes the count to go UP by one)
* * {@link AppUpdateStatusManager.Status#Installed} (Causes the count to go DOWN by one)
*/
private final BroadcastReceiver onUpdateableAppsChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (AppUpdateStatusManager.REASON_UPDATES_AVAILABLE.equals(intent.getStringExtra(AppUpdateStatusManager.EXTRA_REASON_FOR_CHANGE))) {
getSupportLoaderManager().restartLoader(LOADER_NUM_UPDATES, null, MainActivity.this);
boolean updateBadge = false;
AppUpdateStatusManager manager = AppUpdateStatusManager.getInstance(context);
if (AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED.equals(intent.getAction()) &&
AppUpdateStatusManager.REASON_READY_TO_INSTALL.equals(intent.getStringExtra(AppUpdateStatusManager.EXTRA_REASON_FOR_CHANGE))) {
updateBadge = true;
}
// Check if we have moved into the ReadyToInstall or Installed state.
AppUpdateStatusManager.AppUpdateStatus status = manager.get(intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL));
boolean isStatusChange = intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false);
if (isStatusChange && status != null && (status.status == AppUpdateStatusManager.Status.ReadyToInstall || status.status == AppUpdateStatusManager.Status.Installed)) {
updateBadge = true;
}
if (updateBadge) {
int count = 0;
for (AppUpdateStatusManager.AppUpdateStatus s : manager.getAll()) {
if (s.status == AppUpdateStatusManager.Status.ReadyToInstall) {
count++;
}
}
refreshUpdatesBadge(count);
}
}
};

View File

@ -0,0 +1,7 @@
<vector android:height="24dp" android:viewportHeight="48.0"
android:viewportWidth="48.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m2.61,1.25c-0.45,0.01 -0.76,0.19 -0.98,0.45 0,0 0,0 0.01,0.01 -0,0 -0.01,0.01 -0.01,0.01 -0,-0 -0,-0 -0,-0.01 -0,0 -0.01,0.02 -0.02,0.03 -0.01,0.01 -0.01,0.02 -0.02,0.03 -0.15,0.2 -0.49,0.8 0.01,1.5L5.21,7.93C5.08,8.26 5,8.62 5,9v3,1 2.46L5,16c0,1.66 1.34,3 3,3h32c1.66,0 3,-1.34 3,-3L43,15.46 43,9.54 43,9C43,8.62 42.92,8.26 42.79,7.93l3.61,-4.66c0.5,-0.7 0.16,-1.3 0.01,-1.5 -0.01,-0.01 -0.01,-0.02 -0.02,-0.03 -0,-0.01 -0.01,-0.02 -0.02,-0.03 -0,0 -0,0 -0,0.01 -0,-0 -0.01,-0.01 -0.01,-0.01 0,-0 0,-0 0.01,-0.01 -0.22,-0.26 -0.53,-0.44 -0.98,-0.45 -0.38,0.01 -0.74,0.18 -0.97,0.49L40.99,6.18C40.86,6.13 40.74,6.09 40.61,6.06 40.41,6.02 40.21,6 40,6L8,6C7.65,6 7.32,6.07 7.01,6.18L3.58,1.74C3.35,1.43 2.99,1.26 2.61,1.25ZM14.38,9.75c1.86,0 3.38,1.51 3.38,3.38 0,1.86 -1.51,3.38 -3.38,3.38C12.51,16.5 11,14.99 11,13.13 11,11.26 12.51,9.75 14.38,9.75ZM33.88,9.75c1.86,0 3.38,1.51 3.38,3.38 0,1.86 -1.51,3.38 -3.38,3.38 -1.86,0 -3.38,-1.51 -3.38,-3.38 0,-1.86 1.51,-3.38 3.38,-3.38zM8,20.97c-1.66,0 -3,1.34 -3,3L5,30 5,36 5,42.23 5,43c0,1.66 1.34,3 3,3h32c1.66,0 3,-1.34 3,-3L43,42.23 43,24.73 43,23.97c0,-1.66 -1.34,-3 -3,-3z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineJoin="miter" android:strokeWidth="2.5"/>
</vector>

View File

@ -35,6 +35,8 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/header_height"
android:foreground="?attr/selectableItemBackground"
android:clickable="true"
>
<android.support.constraint.ConstraintLayout

View File

@ -3,7 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:clickable="true">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"

View File

@ -3,7 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="220dp">
android:layout_height="220dp"
android:foreground="?attr/selectableItemBackground"
android:clickable="true">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"

View File

@ -3,7 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:clickable="true">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"

View File

@ -4,7 +4,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:clickable="true"
android:background="?attr/selectableItemBackground">
<!-- Ignore ContentDescription because it is kind of meaningless to have TTS read out "App icon"
when it will inevitably read out the name of the app straight after. -->

View File

@ -78,10 +78,6 @@
android:orientation="horizontal"
android:paddingTop="@dimen/category_preview__app_list__padding__vertical"
android:paddingBottom="@dimen/category_preview__app_list__padding__vertical"
android:clipToPadding="false"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
android:clipToPadding="false" />
</android.support.constraint.ConstraintLayout>

View File

@ -7,7 +7,9 @@
android:layout_height="wrap_content"
android:padding="4dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
android:paddingBottom="8dp"
android:clickable="true"
android:background="?attr/selectableItemBackground">
<!-- Ignore ContentDescription because it is kind of meaningless to have TTS read out "App icon"
when it will inevitably read out the name of the app straight after. -->