Merge branch 'swap_ux_improvements' of https://gitlab.com/eighthave/fdroidclient
This commit is contained in:
commit
295ef8f02a
@ -4,6 +4,7 @@
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="src" path="extern/nanohttpd/core/src/main/java"/>
|
||||
<classpathentry kind="src" path="gen"/>
|
||||
<classpathentry combineaccessrules="false" kind="src" path="/AndroidPinning"/>
|
||||
<classpathentry combineaccessrules="false" kind="src" path="/MemorizingActivity"/>
|
||||
|
@ -4,23 +4,50 @@
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<ToggleButton
|
||||
<Button
|
||||
android:id="@+id/enable_wifi"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/enable_wifi" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/repoSwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="15dp"
|
||||
android:text="@string/touch_to_turn_on_local_repo" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/sharing_uri" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sharing_uri"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="15dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textStyle="bold"
|
||||
android:typeface="monospace" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wifiNetwork"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/wifi_network" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wifiNetworkName"
|
||||
android:id="@+id/wifi_network"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="15dp"
|
||||
|
56
res/layout/select_local_apps_list_item.xml
Normal file
56
res/layout/select_local_apps_list_item.xml
Normal file
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2010 The Android Open Source Project
|
||||
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.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/activatedBackgroundIndicator"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:paddingBottom="2dip"
|
||||
android:paddingTop="2dip"
|
||||
tools:ignore="NewApi" >
|
||||
|
||||
<!-- TODO remove NewApi ignore when appcompat-v7 is in place -->
|
||||
|
||||
<ImageView
|
||||
android:layout_width="48dip"
|
||||
android:layout_height="48dip"
|
||||
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:layout_marginTop="6dip"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TwoLineListItem
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:mode="twoLine" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/application_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:layout_marginTop="6dip"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/package_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignStart="@+id/application_label"
|
||||
android:layout_below="@+id/application_label"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall" />
|
||||
</TwoLineListItem>
|
||||
|
||||
</LinearLayout>
|
@ -1,9 +1,14 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="@android:drawable/ic_menu_search"
|
||||
android:showAsAction="ifRoom"
|
||||
android:title="@string/menu_search"/>
|
||||
<item
|
||||
android:id="@+id/action_update_repo"
|
||||
android:icon="@android:drawable/ic_input_add"
|
||||
android:showAsAction="ifRoom|withText"
|
||||
android:showAsAction="always|withText"
|
||||
android:title="@string/update_repo"/>
|
||||
|
||||
</menu>
|
@ -1,10 +1,15 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:actionViewClass="android.widget.SearchView"
|
||||
android:icon="@android:drawable/ic_menu_search"
|
||||
android:showAsAction="collapseActionView|ifRoom"
|
||||
android:title="@string/menu_search"/>
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:orderInCategory="100"
|
||||
android:showAsAction="never"
|
||||
android:icon="@android:drawable/ic_menu_preferences"
|
||||
android:showAsAction="never"
|
||||
android:title="@string/menu_preferences"/>
|
||||
|
||||
</menu>
|
@ -156,6 +156,8 @@
|
||||
<string name="local_repo_running">Your local FDroid repo is accessible.</string>
|
||||
<string name="setup_repo">Setup Local Repo</string>
|
||||
<string name="touch_to_configure_local_repo">Touch to setup your local repo.</string>
|
||||
<string name="touch_to_turn_on_local_repo">Touch to turn on your local repo.</string>
|
||||
<string name="touch_to_turn_off_local_repo">Touch to turn off your local repo.</string>
|
||||
<string name="updating">Updating…</string>
|
||||
<string name="update_repo">Update Repo</string>
|
||||
<string name="deleting_repo">Deleting current repo…</string>
|
||||
@ -168,6 +170,7 @@
|
||||
<string name="icon">icon</string>
|
||||
<string name="fingerprint">Fingerprint:</string>
|
||||
<string name="wifi_network">WiFi Network:</string>
|
||||
<string name="sharing_uri">Sharing URL:</string>
|
||||
<string name="enable_wifi">Enable WiFi</string>
|
||||
<string name="enabling_wifi">Enabling WiFi…</string>
|
||||
<string name="same_wifi_instructions">To connect to other people\'s devices, make sure both devices are on the same WiFi network. Then either type the URL above into F-Droid, or scan this QR Code:</string>
|
||||
|
@ -122,7 +122,7 @@ public class FDroidApp extends Application {
|
||||
// it is more deterministic as to when this gets called...
|
||||
Preferences.setup(this);
|
||||
|
||||
//Apply the Google PRNG fixes to properly seed SecureRandom
|
||||
// Apply the Google PRNG fixes to properly seed SecureRandom
|
||||
PRNGFixes.apply();
|
||||
|
||||
localRepo = new LocalRepoManager(getApplicationContext());
|
||||
@ -301,14 +301,17 @@ public class FDroidApp extends Application {
|
||||
};
|
||||
|
||||
public static void startLocalRepoService(Context context) {
|
||||
context.bindService(new Intent(context, LocalRepoService.class),
|
||||
serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
localRepoServiceIsBound = true;
|
||||
if (!localRepoServiceIsBound) {
|
||||
Context app = context.getApplicationContext();
|
||||
app.bindService(new Intent(app, LocalRepoService.class),
|
||||
serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
localRepoServiceIsBound = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopLocalRepoService(Context context) {
|
||||
if (localRepoServiceIsBound) {
|
||||
context.unbindService(serviceConnection);
|
||||
context.getApplicationContext().unbindService(serviceConnection);
|
||||
localRepoServiceIsBound = false;
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,10 @@ package org.fdroid.fdroid;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||
@ -44,10 +47,12 @@ public class PackageAddedReceiver extends PackageReceiver {
|
||||
Log.d("FDroid", "Inserting installed app info for '" + appId + "' (v" + info.versionCode + ")");
|
||||
|
||||
Uri uri = InstalledAppProvider.getContentUri();
|
||||
ContentValues values = new ContentValues(3);
|
||||
ContentValues values = new ContentValues(4);
|
||||
values.put(InstalledAppProvider.DataColumns.APP_ID, appId);
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode);
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName);
|
||||
values.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL,
|
||||
InstalledAppProvider.getApplicationLabel(context, appId));
|
||||
context.getContentResolver().insert(uri, values);
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,13 @@ package org.fdroid.fdroid;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||
|
||||
/**
|
||||
@ -46,10 +50,12 @@ public class PackageUpgradedReceiver extends PackageReceiver {
|
||||
Log.d("FDroid", "Updating installed app info for '" + appId + "' to v" + info.versionCode + " (" + info.versionName + ")");
|
||||
|
||||
Uri uri = InstalledAppProvider.getContentUri();
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(InstalledAppProvider.DataColumns.APP_ID, info.packageName);
|
||||
ContentValues values = new ContentValues(4);
|
||||
values.put(InstalledAppProvider.DataColumns.APP_ID, appId);
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode);
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName);
|
||||
values.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL,
|
||||
InstalledAppProvider.getApplicationLabel(context, appId));
|
||||
context.getContentResolver().insert(uri, values);
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,8 @@ import com.google.zxing.WriterException;
|
||||
import com.google.zxing.encode.Contents;
|
||||
import com.google.zxing.encode.QRCodeEncoder;
|
||||
|
||||
// zxing is android-8 and above
|
||||
@TargetApi(8)
|
||||
public class QrGenAsyncTask extends AsyncTask<String, Void, Void> {
|
||||
private static final String TAG = "QrGenAsyncTask";
|
||||
|
||||
|
@ -91,12 +91,13 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
public static final String TABLE_INSTALLED_APP = "fdroid_installedApp";
|
||||
private static final String CREATE_TABLE_INSTALLED_APP = "CREATE TABLE " + TABLE_INSTALLED_APP
|
||||
+ " ( "
|
||||
+ "appId TEXT NOT NULL PRIMARY KEY, "
|
||||
+ "versionCode INT NOT NULL, "
|
||||
+ "versionName TEXT NOT NULL "
|
||||
+ InstalledAppProvider.DataColumns.APP_ID + " TEXT NOT NULL PRIMARY KEY, "
|
||||
+ InstalledAppProvider.DataColumns.VERSION_CODE + " INT NOT NULL, "
|
||||
+ InstalledAppProvider.DataColumns.VERSION_NAME + " TEXT NOT NULL, "
|
||||
+ InstalledAppProvider.DataColumns.APPLICATION_LABEL + " TEXT NOT NULL "
|
||||
+ " );";
|
||||
|
||||
private static final int DB_VERSION = 44;
|
||||
private static final int DB_VERSION = 46;
|
||||
|
||||
private Context context;
|
||||
|
||||
@ -249,11 +250,11 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
addLastUpdatedToRepo(db, oldVersion);
|
||||
renameRepoId(db, oldVersion);
|
||||
populateRepoNames(db, oldVersion);
|
||||
|
||||
if (oldVersion < 43) createInstalledApp(db);
|
||||
addAppLabelToInstalledCache(db, oldVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Migrate repo list to new structure. (No way to change primary
|
||||
* key in sqlite - table must be recreated).
|
||||
*/
|
||||
@ -398,6 +399,17 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(CREATE_TABLE_INSTALLED_APP);
|
||||
}
|
||||
|
||||
private void addAppLabelToInstalledCache(SQLiteDatabase db, int oldVersion) {
|
||||
if (oldVersion < 45) {
|
||||
Log.i(TAG, "Adding applicationLabel to installed app table. " +
|
||||
"Turns out we will need to repopulate the cache after doing this, " +
|
||||
"so just dropping and recreating the table (instead of altering and adding a column). " +
|
||||
"This will force the entire cache to be rebuilt, including app names.");
|
||||
db.execSQL("DROP TABLE fdroid_installedApp;");
|
||||
createInstalledApp(db);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean columnExists(SQLiteDatabase db,
|
||||
String table, String column) {
|
||||
return (db.rawQuery( "select * from " + table + " limit 0,1", null )
|
||||
|
@ -135,6 +135,8 @@ public class InstalledAppCacheUpdater {
|
||||
.withValue(InstalledAppProvider.DataColumns.APP_ID, info.packageName)
|
||||
.withValue(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode)
|
||||
.withValue(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName)
|
||||
.withValue(InstalledAppProvider.DataColumns.APPLICATION_LABEL,
|
||||
InstalledAppProvider.getApplicationLabel(context, info.packageName))
|
||||
.build();
|
||||
ops.add(op);
|
||||
}
|
||||
|
@ -3,9 +3,14 @@ package org.fdroid.fdroid.data;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.res.Resources.NotFoundException;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.R;
|
||||
|
||||
import java.util.HashMap;
|
||||
@ -49,17 +54,24 @@ public class InstalledAppProvider extends FDroidProvider {
|
||||
public static final String APP_ID = "appId";
|
||||
public static final String VERSION_CODE = "versionCode";
|
||||
public static final String VERSION_NAME = "versionName";
|
||||
public static final String APPLICATION_LABEL = "applicationLabel";
|
||||
|
||||
public static String[] ALL = { _ID, APP_ID, VERSION_CODE, VERSION_NAME };
|
||||
public static String[] ALL = {
|
||||
_ID, APP_ID, VERSION_CODE, VERSION_NAME, APPLICATION_LABEL,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private static final String PROVIDER_NAME = "InstalledAppProvider";
|
||||
|
||||
private static final String PATH_SEARCH = "search";
|
||||
private static final int CODE_SEARCH = CODE_SINGLE + 1;
|
||||
|
||||
private static final UriMatcher matcher = new UriMatcher(-1);
|
||||
|
||||
static {
|
||||
matcher.addURI(getAuthority(), null, CODE_LIST);
|
||||
matcher.addURI(getAuthority(), PATH_SEARCH + "/*", CODE_SEARCH);
|
||||
matcher.addURI(getAuthority(), "*", CODE_SINGLE);
|
||||
}
|
||||
|
||||
@ -71,6 +83,27 @@ public class InstalledAppProvider extends FDroidProvider {
|
||||
return Uri.withAppendedPath(getContentUri(), appId);
|
||||
}
|
||||
|
||||
public static Uri getSearchUri(String keywords) {
|
||||
return getContentUri().buildUpon()
|
||||
.appendPath(PATH_SEARCH)
|
||||
.appendPath(keywords)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static String getApplicationLabel(Context context, String packageName) {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
ApplicationInfo appInfo;
|
||||
try {
|
||||
appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
|
||||
return appInfo.loadLabel(pm).toString();
|
||||
} catch (NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NotFoundException e) {
|
||||
Log.d("InstalledAppProvider", "getApplicationLabel: " + e.getMessage());
|
||||
}
|
||||
return packageName; // all else fails, return id
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTableName() {
|
||||
return DBHelper.TABLE_INSTALLED_APP;
|
||||
@ -94,8 +127,16 @@ public class InstalledAppProvider extends FDroidProvider {
|
||||
return new QuerySelection("appId = ?", new String[] { appId } );
|
||||
}
|
||||
|
||||
private QuerySelection querySearch(String keywords) {
|
||||
return new QuerySelection("applicationLabel LIKE ?", new String[] { "%" + keywords + "%" } );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
|
||||
if (sortOrder == null) {
|
||||
sortOrder = DataColumns.APPLICATION_LABEL;
|
||||
}
|
||||
|
||||
QuerySelection selection = new QuerySelection(customSelection, selectionArgs);
|
||||
switch (matcher.match(uri)) {
|
||||
case CODE_LIST:
|
||||
@ -105,13 +146,17 @@ public class InstalledAppProvider extends FDroidProvider {
|
||||
selection = selection.add(queryApp(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case CODE_SEARCH:
|
||||
selection = selection.add(querySearch(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
default:
|
||||
String message = "Invalid URI for installed app content provider: " + uri;
|
||||
Log.e("FDroid", message);
|
||||
throw new UnsupportedOperationException(message);
|
||||
}
|
||||
|
||||
Cursor cursor = read().query(getTableName(), projection, selection.getSelection(), selection.getArgs(), null, null, null);
|
||||
Cursor cursor = read().query(getTableName(), projection, selection.getSelection(), selection.getArgs(), null, null, sortOrder);
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
return cursor;
|
||||
}
|
||||
|
@ -34,6 +34,10 @@ import java.util.Random;
|
||||
public class LocalRepoService extends Service {
|
||||
private static final String TAG = "LocalRepoService";
|
||||
|
||||
public static final String STATE = "org.fdroid.fdroid.action.LOCAL_REPO_STATE";
|
||||
public static final String STARTED = "org.fdroid.fdroid.category.LOCAL_REPO_STARTED";
|
||||
public static final String STOPPED = "org.fdroid.fdroid.category.LOCAL_REPO_STOPPED";
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
// Unique Identification Number for the Notification.
|
||||
// We use it on Notification start, and to cancel it.
|
||||
@ -148,6 +152,9 @@ public class LocalRepoService extends Service {
|
||||
}
|
||||
};
|
||||
new Thread(webServer).start();
|
||||
Intent intent = new Intent(STATE);
|
||||
intent.putExtra(STATE, STARTED);
|
||||
LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
private void stopWebServer() {
|
||||
@ -158,5 +165,8 @@ public class LocalRepoService extends Service {
|
||||
Message msg = webServerThreadHandler.obtainMessage();
|
||||
msg.obj = webServerThreadHandler.getLooper().getThread().getName() + " says stop";
|
||||
webServerThreadHandler.sendMessage(msg);
|
||||
Intent intent = new Intent(STATE);
|
||||
intent.putExtra(STATE, STOPPED);
|
||||
LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
@ -19,21 +19,25 @@ import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
import android.widget.*;
|
||||
|
||||
import org.fdroid.fdroid.*;
|
||||
import org.fdroid.fdroid.localrepo.LocalRepoService;
|
||||
import org.fdroid.fdroid.net.WifiStateChangeService;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class LocalRepoActivity extends Activity {
|
||||
private static final String TAG = "LocalRepoActivity";
|
||||
private ProgressDialog repoProgress;
|
||||
|
||||
private WifiManager wifiManager;
|
||||
private ToggleButton repoSwitch;
|
||||
private Button enableWifiButton;
|
||||
private CheckBox repoSwitch;
|
||||
|
||||
private Timer stopTimer;
|
||||
|
||||
private int SET_IP_ADDRESS = 7345;
|
||||
private int UPDATE_REPO = 7346;
|
||||
@ -42,9 +46,11 @@ public class LocalRepoActivity extends Activity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
((FDroidApp) getApplication()).applyTheme(this);
|
||||
setContentView(R.layout.local_repo_activity);
|
||||
|
||||
repoSwitch = (ToggleButton) findViewById(R.id.repoSwitch);
|
||||
enableWifiButton = (Button) findViewById(R.id.enable_wifi);
|
||||
repoSwitch = (CheckBox) findViewById(R.id.repoSwitch);
|
||||
wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
|
||||
}
|
||||
|
||||
@ -52,19 +58,35 @@ public class LocalRepoActivity extends Activity {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resetNetworkInfo();
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
|
||||
new IntentFilter(WifiStateChangeService.BROADCAST));
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(onLocalRepoChange,
|
||||
new IntentFilter(LocalRepoService.STATE));
|
||||
// if no local repo exists, create one with only FDroid in it
|
||||
if (!FDroidApp.localRepo.xmlIndex.exists())
|
||||
new UpdateAsyncTask(this, new String[] {
|
||||
getPackageName(),
|
||||
}).execute();
|
||||
|
||||
// start repo by default
|
||||
FDroidApp.startLocalRepoService(LocalRepoActivity.this);
|
||||
// automatically turn off after 15 minutes
|
||||
stopTimer = new Timer();
|
||||
stopTimer.schedule(new TimerTask() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
FDroidApp.stopLocalRepoService(LocalRepoActivity.this);
|
||||
}
|
||||
}, 900000); // 15 minutes
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(onLocalRepoChange);
|
||||
}
|
||||
|
||||
private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
|
||||
@ -74,19 +96,33 @@ public class LocalRepoActivity extends Activity {
|
||||
}
|
||||
};
|
||||
|
||||
private BroadcastReceiver onLocalRepoChange = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent i) {
|
||||
String state = i.getStringExtra(LocalRepoService.STATE);
|
||||
if (state != null && state.equals(LocalRepoService.STARTED))
|
||||
setRepoSwitchChecked(true);
|
||||
else
|
||||
setRepoSwitchChecked(false);
|
||||
}
|
||||
};
|
||||
|
||||
private void resetNetworkInfo() {
|
||||
int wifiState = wifiManager.getWifiState();
|
||||
if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
|
||||
setUIFromWifi();
|
||||
wireRepoSwitchToWebServer();
|
||||
repoSwitch.setVisibility(View.VISIBLE);
|
||||
enableWifiButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
repoSwitch.setChecked(false);
|
||||
repoSwitch.setText(R.string.enable_wifi);
|
||||
repoSwitch.setTextOn(getString(R.string.enabling_wifi));
|
||||
repoSwitch.setTextOff(getString(R.string.enable_wifi));
|
||||
repoSwitch.setOnClickListener(new View.OnClickListener() {
|
||||
repoSwitch.setVisibility(View.GONE);
|
||||
enableWifiButton.setVisibility(View.VISIBLE);
|
||||
enableWifiButton.setText(R.string.enable_wifi);
|
||||
enableWifiButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
enableWifiButton.setText(R.string.enabling_wifi);
|
||||
wifiManager.setWifiEnabled(true);
|
||||
/*
|
||||
* Once the wifi is connected to a network, then
|
||||
@ -158,24 +194,34 @@ public class LocalRepoActivity extends Activity {
|
||||
repoSwitch.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
setRepoSwitchChecked(repoSwitch.isChecked());
|
||||
if (repoSwitch.isChecked()) {
|
||||
FDroidApp.startLocalRepoService(LocalRepoActivity.this);
|
||||
} else {
|
||||
FDroidApp.stopLocalRepoService(LocalRepoActivity.this);
|
||||
stopTimer.cancel(); // disable automatic stop
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setRepoSwitchChecked(boolean checked) {
|
||||
repoSwitch.setChecked(checked);
|
||||
if (checked) {
|
||||
repoSwitch.setText(R.string.local_repo_running);
|
||||
} else {
|
||||
repoSwitch.setText(R.string.touch_to_turn_on_local_repo);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(14)
|
||||
private void setUIFromWifi() {
|
||||
if (TextUtils.isEmpty(FDroidApp.repo.address))
|
||||
return;
|
||||
// the fingerprint is not useful on the button label
|
||||
String buttonLabel = FDroidApp.repo.address.replaceAll("\\?.*$", "");
|
||||
repoSwitch.setText(buttonLabel);
|
||||
repoSwitch.setTextOn(buttonLabel);
|
||||
repoSwitch.setTextOff(buttonLabel);
|
||||
TextView sharingUriTextView = (TextView) findViewById(R.id.sharing_uri);
|
||||
sharingUriTextView.setText(buttonLabel);
|
||||
/*
|
||||
* Set URL to UPPER for compact QR Code, FDroid will translate it back.
|
||||
* Remove the SSID from the query string since SSIDs are case-sensitive.
|
||||
@ -188,9 +234,10 @@ public class LocalRepoActivity extends Activity {
|
||||
.replaceAll("ssid=[^?]*", "")
|
||||
.toUpperCase(Locale.ENGLISH);
|
||||
Log.i("QRURI", qrUriString);
|
||||
new QrGenAsyncTask(this, R.id.repoQrCode).execute(qrUriString);
|
||||
if (Build.VERSION.SDK_INT >= 8) // zxing requires >= 8
|
||||
new QrGenAsyncTask(this, R.id.repoQrCode).execute(qrUriString);
|
||||
|
||||
TextView wifiNetworkNameTextView = (TextView) findViewById(R.id.wifiNetworkName);
|
||||
TextView wifiNetworkNameTextView = (TextView) findViewById(R.id.wifi_network);
|
||||
wifiNetworkNameTextView.setText(FDroidApp.ssid);
|
||||
|
||||
TextView fingerprintTextView = (TextView) findViewById(R.id.fingerprint);
|
||||
|
@ -2,11 +2,8 @@
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.*;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
@ -27,6 +24,7 @@ public class QrWizardDownloadActivity extends Activity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
((FDroidApp) getApplication()).applyTheme(this);
|
||||
setContentView(R.layout.qr_wizard_activity);
|
||||
TextView instructions = (TextView) findViewById(R.id.qrWizardInstructions);
|
||||
instructions.setText(R.string.qr_wizard_download_instructions);
|
||||
@ -73,7 +71,8 @@ public class QrWizardDownloadActivity extends Activity {
|
||||
qrString += "://" + FDroidApp.ipAddressString;
|
||||
qrString += ":" + FDroidApp.port;
|
||||
|
||||
new QrGenAsyncTask(this, R.id.qrWizardImage).execute(qrString);
|
||||
if (Build.VERSION.SDK_INT >= 8) // zxing requires >= 8
|
||||
new QrGenAsyncTask(this, R.id.qrWizardImage).execute(qrString);
|
||||
Log.i(TAG, "qr: " + qrString);
|
||||
|
||||
TextView wifiNetworkName = (TextView) findViewById(R.id.qrWifiNetworkName);
|
||||
|
@ -2,12 +2,10 @@
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.*;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.util.Log;
|
||||
@ -33,6 +31,7 @@ public class QrWizardWifiNetworkActivity extends Activity {
|
||||
wifiManager.setWifiEnabled(true);
|
||||
FDroidApp.startLocalRepoService(this);
|
||||
|
||||
((FDroidApp) getApplication()).applyTheme(this);
|
||||
setContentView(R.layout.qr_wizard_activity);
|
||||
TextView instructions = (TextView) findViewById(R.id.qrWizardInstructions);
|
||||
instructions.setText(R.string.qr_wizard_wifi_network_instructions);
|
||||
@ -92,12 +91,11 @@ public class QrWizardWifiNetworkActivity extends Activity {
|
||||
if (wifiInfo.getHiddenSSID())
|
||||
qrString += ";H:true";
|
||||
qrString += ";;";
|
||||
new QrGenAsyncTask(this, R.id.qrWizardImage).execute(qrString);
|
||||
Log.i(TAG, "qr: " + qrString);
|
||||
if (Build.VERSION.SDK_INT >= 8) // zxing requires >= 8
|
||||
new QrGenAsyncTask(this, R.id.qrWizardImage).execute(qrString);
|
||||
|
||||
TextView wifiNetworkName = (TextView) findViewById(R.id.qrWifiNetworkName);
|
||||
wifiNetworkName.setText(wifiInfo.getSSID());
|
||||
Log.i(TAG, "wifi network name: " + wifiInfo.getSSID());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,27 +2,28 @@
|
||||
package org.fdroid.fdroid.views;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.*;
|
||||
import android.widget.SearchView;
|
||||
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.PreferencesActivity;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.views.fragments.SelectLocalAppsFragment;
|
||||
|
||||
@TargetApi(11)
|
||||
// TODO replace with appcompat-v7
|
||||
public class SelectLocalAppsActivity extends FragmentActivity {
|
||||
public class SelectLocalAppsActivity extends Activity {
|
||||
private static final String TAG = "SelectLocalAppsActivity";
|
||||
private SelectLocalAppsFragment selectLocalAppsFragment = null;
|
||||
private SearchView searchView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
((FDroidApp) getApplication()).applyTheme(this);
|
||||
setContentView(R.layout.select_local_apps_activity);
|
||||
}
|
||||
|
||||
@ -30,13 +31,15 @@ public class SelectLocalAppsActivity extends FragmentActivity {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (selectLocalAppsFragment == null)
|
||||
selectLocalAppsFragment = (SelectLocalAppsFragment) getSupportFragmentManager().findFragmentById(
|
||||
R.id.fragment_app_list);
|
||||
selectLocalAppsFragment = (SelectLocalAppsFragment) getFragmentManager()
|
||||
.findFragmentById(R.id.fragment_app_list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.select_local_apps_activity, menu);
|
||||
searchView = (SearchView) menu.findItem(R.id.action_search).getActionView();
|
||||
searchView.setOnQueryTextListener(selectLocalAppsFragment);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -47,6 +50,10 @@ public class SelectLocalAppsActivity extends FragmentActivity {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
return true;
|
||||
case R.id.action_search:
|
||||
SearchView searchView = (SearchView) item.getActionView();
|
||||
searchView.setIconified(false);
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
startActivity(new Intent(this, PreferencesActivity.class));
|
||||
return true;
|
||||
@ -60,6 +67,7 @@ public class SelectLocalAppsActivity extends FragmentActivity {
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
MenuInflater inflater = mode.getMenuInflater();
|
||||
inflater.inflate(R.menu.select_local_apps_action_mode, menu);
|
||||
menu.findItem(R.id.action_search).setActionView(searchView);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -15,17 +15,23 @@ limitations under the License.
|
||||
package org.fdroid.fdroid.views.fragments;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.ListFragment;
|
||||
import android.app.LoaderManager.LoaderCallbacks;
|
||||
import android.content.CursorLoader;
|
||||
import android.content.Loader;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.widget.SimpleCursorAdapter;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.ActionMode;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
import android.widget.*;
|
||||
import android.widget.SearchView.OnQueryTextListener;
|
||||
import android.widget.SimpleCursorAdapter.ViewBinder;
|
||||
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.R;
|
||||
@ -35,39 +41,72 @@ import org.fdroid.fdroid.views.SelectLocalAppsActivity;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
public class SelectLocalAppsFragment extends ListFragment implements LoaderCallbacks<Cursor> {
|
||||
//TODO replace with appcompat-v7
|
||||
@TargetApi(11)
|
||||
public class SelectLocalAppsFragment extends ListFragment
|
||||
implements LoaderCallbacks<Cursor>, OnQueryTextListener {
|
||||
|
||||
private PackageManager packageManager;
|
||||
private Drawable defaultAppIcon;
|
||||
private SelectLocalAppsActivity selectLocalAppsActivity;
|
||||
private ActionMode mActionMode = null;
|
||||
private String mCurrentFilterString;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@TargetApi(11)
|
||||
// TODO replace with appcompat-v7
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
setEmptyText(getString(R.string.no_applications_found));
|
||||
|
||||
packageManager = getActivity().getPackageManager();
|
||||
defaultAppIcon = getActivity().getResources()
|
||||
.getDrawable(android.R.drawable.sym_def_app_icon);
|
||||
|
||||
selectLocalAppsActivity = (SelectLocalAppsActivity) getActivity();
|
||||
ListView listView = getListView();
|
||||
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
||||
SimpleCursorAdapter adapter = new SimpleCursorAdapter(getActivity(),
|
||||
android.R.layout.simple_list_item_activated_1,
|
||||
R.layout.select_local_apps_list_item,
|
||||
null,
|
||||
new String[] {
|
||||
InstalledAppProvider.DataColumns.APPLICATION_LABEL,
|
||||
InstalledAppProvider.DataColumns.APP_ID,
|
||||
},
|
||||
new int[] {
|
||||
android.R.id.text1,
|
||||
R.id.application_label,
|
||||
R.id.package_name,
|
||||
},
|
||||
0);
|
||||
adapter.setViewBinder(new ViewBinder() {
|
||||
|
||||
@Override
|
||||
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
|
||||
Log.i("SelectLocalAppsFragment", "ViewBinder " + columnIndex);
|
||||
if (columnIndex == cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)) {
|
||||
String packageName = cursor.getString(columnIndex);
|
||||
TextView textView = (TextView) view.findViewById(R.id.package_name);
|
||||
textView.setText(packageName);
|
||||
LinearLayout ll = (LinearLayout) view.getParent().getParent();
|
||||
ImageView iconView = (ImageView) ll.getChildAt(0);
|
||||
Drawable icon;
|
||||
try {
|
||||
icon = packageManager.getApplicationIcon(packageName);
|
||||
} catch (NameNotFoundException e) {
|
||||
icon = defaultAppIcon;
|
||||
}
|
||||
iconView.setImageDrawable(icon);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
setListAdapter(adapter);
|
||||
setListShown(false);
|
||||
setListShown(false); // start out with a progress indicator
|
||||
|
||||
// either reconnect with an existing loader or start a new one
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
@ -84,15 +123,13 @@ public class SelectLocalAppsFragment extends ListFragment implements LoaderCallb
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(11)
|
||||
// TODO replace with appcompat-v7
|
||||
@Override
|
||||
public void onListItemClick(ListView l, View v, int position, long id) {
|
||||
if (mActionMode == null)
|
||||
mActionMode = selectLocalAppsActivity
|
||||
.startActionMode(selectLocalAppsActivity.mActionModeCallback);
|
||||
Cursor cursor = (Cursor) l.getAdapter().getItem(position);
|
||||
String packageName = cursor.getString(1);
|
||||
Cursor c = (Cursor) l.getAdapter().getItem(position);
|
||||
String packageName = c.getString(c.getColumnIndex(DataColumns.APP_ID));
|
||||
if (FDroidApp.selectedApps.contains(packageName)) {
|
||||
FDroidApp.selectedApps.remove(packageName);
|
||||
} else {
|
||||
@ -102,13 +139,19 @@ public class SelectLocalAppsFragment extends ListFragment implements LoaderCallb
|
||||
|
||||
@Override
|
||||
public CursorLoader onCreateLoader(int id, Bundle args) {
|
||||
Uri baseUri;
|
||||
if (TextUtils.isEmpty(mCurrentFilterString)) {
|
||||
baseUri = InstalledAppProvider.getContentUri();
|
||||
} else {
|
||||
baseUri = InstalledAppProvider.getSearchUri(mCurrentFilterString);
|
||||
}
|
||||
CursorLoader loader = new CursorLoader(
|
||||
this.getActivity(),
|
||||
InstalledAppProvider.getContentUri(),
|
||||
baseUri,
|
||||
InstalledAppProvider.DataColumns.ALL,
|
||||
null,
|
||||
null,
|
||||
InstalledAppProvider.DataColumns.APP_ID);
|
||||
InstalledAppProvider.DataColumns.APPLICATION_LABEL);
|
||||
return loader;
|
||||
}
|
||||
|
||||
@ -123,7 +166,7 @@ public class SelectLocalAppsFragment extends ListFragment implements LoaderCallb
|
||||
Cursor c = ((Cursor) listView.getItemAtPosition(i));
|
||||
String packageName = c.getString(c.getColumnIndex(DataColumns.APP_ID));
|
||||
if (TextUtils.equals(packageName, fdroid)) {
|
||||
listView.setItemChecked(i, true);
|
||||
listView.setItemChecked(i, true); // always include FDroid
|
||||
} else {
|
||||
for (String selected : FDroidApp.selectedApps) {
|
||||
if (TextUtils.equals(packageName, selected)) {
|
||||
@ -144,4 +187,28 @@ public class SelectLocalAppsFragment extends ListFragment implements LoaderCallb
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
((SimpleCursorAdapter) this.getListAdapter()).swapCursor(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
|
||||
if (mCurrentFilterString == null && newFilter == null) {
|
||||
return true;
|
||||
}
|
||||
if (mCurrentFilterString != null && mCurrentFilterString.equals(newFilter)) {
|
||||
return true;
|
||||
}
|
||||
mCurrentFilterString = newFilter;
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
// this is not needed since we respond to every change in text
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getCurrentFilterString() {
|
||||
return mCurrentFilterString;
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,11 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.ContactsContract;
|
||||
import android.test.ProviderTestCase2MockContext;
|
||||
|
||||
import mock.MockContextEmptyComponents;
|
||||
import mock.MockContextSwappableComponents;
|
||||
import mock.MockFDroidResources;
|
||||
|
||||
import org.fdroid.fdroid.data.*;
|
||||
|
||||
import java.util.List;
|
||||
@ -151,6 +153,7 @@ public abstract class FDroidProviderTest<T extends FDroidProvider> extends Provi
|
||||
InstalledAppProvider.DataColumns.APP_ID,
|
||||
InstalledAppProvider.DataColumns.VERSION_CODE,
|
||||
InstalledAppProvider.DataColumns.VERSION_NAME,
|
||||
InstalledAppProvider.DataColumns.APPLICATION_LABEL,
|
||||
};
|
||||
|
||||
Cursor cursor = getMockContentResolver().query(uri, projection, null, null, null);
|
||||
|
Loading…
x
Reference in New Issue
Block a user