Compare commits
	
		
			57 Commits
		
	
	
		
			1.0-beta12
			...
			1.0-beta18
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 396f1a23a6 | |||
| db939dab9c | |||
| 30d9d72133 | |||
| df581f4c51 | |||
| 4c89bfe1cf | |||
| d88d3c900e | |||
| f6ebd62c8d | |||
| 760e423b9b | |||
| f45f6c3919 | |||
| dd22caaa50 | |||
| 49d87c3c75 | |||
| c7dbe660b9 | |||
| b825b33250 | |||
| 33e932e630 | |||
| 072f732924 | |||
| f58a22dadb | |||
| 1329aecde4 | |||
| f27f438998 | |||
| c1d74e4781 | |||
| 34e2e0673b | |||
| f4b6bcb8d9 | |||
| 1e8b71e43b | |||
| 9dd1b457e3 | |||
| 287de9deb5 | |||
| 696cd6c0a6 | |||
| a23ae14f1d | |||
| 415107a6c8 | |||
| cc18f34161 | |||
| 852e38b97d | |||
| 858651e808 | |||
| 625848bd9d | |||
| 4622ad68f0 | |||
| d05f1f98e5 | |||
| 973e4a0dca | |||
| 3a98cc115a | |||
| e2aa0e8b1d | |||
| ec3009a257 | |||
| e79bfdb244 | |||
| 898c49802b | |||
| e064012551 | |||
| faa6752b10 | |||
| 593a390b40 | |||
| ccfeb5b479 | |||
| 8057980f6c | |||
| 8af8419b7c | |||
| 433c757107 | |||
| 1c284eba26 | |||
| bf52d2f3de | |||
| a67560c28b | |||
| bf070da20a | |||
| 73944b5883 | |||
| 263fc8893f | |||
| 6540df4fc9 | |||
| 422c7ac803 | |||
| 5bc1bc2a47 | |||
| c7200d06bc | |||
| 3bdf1bd6bf | 
							
								
								
									
										107
									
								
								app/build.gradle
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								app/build.gradle
									
									
									
									
									
								
							| @@ -1,32 +1,36 @@ | |||||||
| apply plugin: 'idea' |  | ||||||
| apply plugin: 'com.android.application' | apply plugin: 'com.android.application' | ||||||
|  | apply plugin: 'kotlin-android' | ||||||
|  | apply plugin: 'kotlin-android-extensions' | ||||||
|  | apply plugin: 'idea' | ||||||
|  |  | ||||||
| ext { | ext { | ||||||
|     appName = "Abit" |     appName = "Abit" | ||||||
| } | } | ||||||
| if (project.hasProperty("project.configs") | if (project.hasProperty("project.configs") | ||||||
|     && new File(project.property("project.configs") + appName + ".gradle").exists()) { |     && new File(project.property("project.configs") + appName + ".gradle").exists()) { | ||||||
|     apply from: project.property("project.configs") + appName + ".gradle"; |     apply from: project.property("project.configs") + appName + ".gradle" | ||||||
| } | } | ||||||
|  |  | ||||||
| //noinspection GroovyMissingReturnStatement | //noinspection GroovyMissingReturnStatement | ||||||
| android { | android { | ||||||
|     compileSdkVersion 25 |     compileSdkVersion 27 | ||||||
|     buildToolsVersion "25.0.2" |     buildToolsVersion "26.0.2" | ||||||
|  |  | ||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         applicationId "ch.dissem.apps." + appName.toLowerCase() |         applicationId "ch.dissem.apps.${appName.toLowerCase()}" | ||||||
|         minSdkVersion 19 |         minSdkVersion 19 | ||||||
|         targetSdkVersion 25 |         targetSdkVersion 27 | ||||||
|         versionCode 12 |         versionCode 18 | ||||||
|         versionName "1.0-beta12" |         versionName "1.0-beta18" | ||||||
|         jackOptions.enabled = false |  | ||||||
|         multiDexEnabled true |         multiDexEnabled true | ||||||
|     } |     } | ||||||
|     compileOptions { |     compileOptions { | ||||||
|         sourceCompatibility JavaVersion.VERSION_1_7 |         sourceCompatibility JavaVersion.VERSION_1_7 | ||||||
|         targetCompatibility JavaVersion.VERSION_1_7 |         targetCompatibility JavaVersion.VERSION_1_7 | ||||||
|     } |     } | ||||||
|  |     lintOptions { | ||||||
|  |         abortOnError false | ||||||
|  |     } | ||||||
|     buildTypes { |     buildTypes { | ||||||
|         release { |         release { | ||||||
|             minifyEnabled false |             minifyEnabled false | ||||||
| @@ -35,60 +39,77 @@ android { | |||||||
|             signingConfig signingConfigs.release |             signingConfig signingConfigs.release | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |     packagingOptions { | ||||||
|  |         exclude 'META-INF/core.kotlin_module' | ||||||
|  |     } | ||||||
|  |     testOptions { | ||||||
|  |         unitTests { | ||||||
|  |             includeAndroidResources = true | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| //ext.jabitVersion = '2.0.4' | //ext.jabitVersion = '2.0.4' | ||||||
| ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' | ext.jabitVersion = 'feature-refactoring-SNAPSHOT' | ||||||
| ext.supportVersion = '25.3.1' | ext.supportVersion = '27.0.2' | ||||||
| dependencies { | dependencies { | ||||||
|     compile fileTree(dir: 'libs', include: ['*.jar']) |     implementation fileTree(dir: 'libs', include: ['*.jar']) | ||||||
|  |     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" | ||||||
|  |     implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" | ||||||
|  |     implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" | ||||||
|  |     implementation "org.jetbrains.anko:anko:$anko_version" | ||||||
|  |  | ||||||
|     compile "com.android.support:appcompat-v7:$supportVersion" |     implementation "com.android.support:appcompat-v7:$supportVersion" | ||||||
|     compile "com.android.support:support-v4:$supportVersion" |     implementation "com.android.support:preference-v7:$supportVersion" | ||||||
|     compile "com.android.support:design:$supportVersion" |     implementation "com.android.support:cardview-v7:$supportVersion" | ||||||
|     compile "com.android.support:multidex:1.0.1" |     implementation "com.android.support:support-v4:$supportVersion" | ||||||
|  |     implementation "com.android.support:design:$supportVersion" | ||||||
|  |     implementation "com.android.support:multidex:1.0.2" | ||||||
|  |  | ||||||
|     compile "ch.dissem.jabit:jabit-core:$jabitVersion" |     implementation "ch.dissem.jabit:jabit-core:$jabitVersion" | ||||||
|     compile "ch.dissem.jabit:jabit-networking:$jabitVersion" |     implementation "ch.dissem.jabit:jabit-networking:$jabitVersion" | ||||||
|     compile "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion" |     implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion" | ||||||
|     compile "ch.dissem.jabit:jabit-extensions:$jabitVersion" |     implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion" | ||||||
|     compile "ch.dissem.jabit:jabit-wif:$jabitVersion" |     implementation "ch.dissem.jabit:jabit-wif:$jabitVersion" | ||||||
|  |     implementation "ch.dissem.jabit:jabit-exports:$jabitVersion" | ||||||
|  |  | ||||||
|     compile 'org.slf4j:slf4j-android:1.7.25' |     implementation 'org.slf4j:slf4j-android:1.7.25' | ||||||
|  |  | ||||||
|     compile 'com.mikepenz:materialize:1.0.1@aar' |     implementation 'com.mikepenz:materialize:1.1.2@aar' | ||||||
|     compile('com.mikepenz:materialdrawer:5.9.0@aar') { |     implementation('com.mikepenz:materialdrawer:6.0.2@aar') { | ||||||
|         transitive = true |         transitive = true | ||||||
|     } |     } | ||||||
|     compile('com.mikepenz:aboutlibraries:5.9.5@aar') { |     implementation('com.mikepenz:aboutlibraries:6.0.2@aar') { | ||||||
|         transitive = true |         transitive = true | ||||||
|     } |     } | ||||||
|     compile "com.mikepenz:iconics-core:2.8.3@aar" |     implementation "com.mikepenz:iconics-core:3.0.0@aar" | ||||||
|     compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' |     implementation "com.mikepenz:iconics-views:3.0.0@aar" | ||||||
|     compile 'com.mikepenz:community-material-typeface:1.9.32.1@aar' |     implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar' | ||||||
|  |     implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar' | ||||||
|  |  | ||||||
|     compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar' |     implementation 'com.journeyapps:zxing-android-embedded:3.5.0@aar' | ||||||
|     compile 'com.google.zxing:core:3.3.0' |     implementation 'com.google.zxing:core:3.3.1' | ||||||
|  |  | ||||||
|     compile 'io.github.yavski:fab-speed-dial:1.0.6' |     implementation 'com.github.kobakei:MaterialFabSpeedDial:1.1.8' | ||||||
|     compile 'com.github.amlcurran.showcaseview:library:5.4.3' |     implementation 'com.github.amlcurran.showcaseview:library:5.4.3' | ||||||
|     compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') { |     implementation('com.github.h6ah4i:android-advancedrecyclerview:0.11.0@aar') { | ||||||
|         transitive = true |         transitive = true | ||||||
|     } |     } | ||||||
|     compile 'com.github.angads25:filepicker:1.1.0' |     implementation 'com.github.angads25:filepicker:1.1.1' | ||||||
|     compile 'com.android.support.constraint:constraint-layout:1.0.2' |     implementation 'com.android.support.constraint:constraint-layout:1.0.2' | ||||||
|  |  | ||||||
|     testCompile 'junit:junit:4.12' |     testImplementation 'junit:junit:4.12' | ||||||
|     testCompile 'org.mockito:mockito-core:2.7.22' |     testImplementation 'org.mockito:mockito-core:2.13.0' | ||||||
|  |     testImplementation 'org.hamcrest:hamcrest-library:1.3' | ||||||
|  |     testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0' | ||||||
|  |     testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" | ||||||
|  |     testImplementation 'org.robolectric:robolectric:3.6.1' | ||||||
|  |     testImplementation "org.robolectric:shadows-multidex:3.6.1" | ||||||
|  |  | ||||||
|  |     androidTestImplementation "com.android.support:multidex:1.0.2" | ||||||
| } | } | ||||||
|  |  | ||||||
| idea.module { | idea.module { | ||||||
|     downloadJavadoc = true |     downloadJavadoc = true | ||||||
|     downloadSources = true |     downloadSources = true | ||||||
| } | } | ||||||
|  |  | ||||||
| android { |  | ||||||
|     lintOptions { |  | ||||||
|         abortOnError false |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -4,9 +4,9 @@ | |||||||
|     xmlns:android="http://schemas.android.com/apk/res/android" |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:tools="http://schemas.android.com/tools"> |     xmlns:tools="http://schemas.android.com/tools"> | ||||||
|  |  | ||||||
|     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> |  | ||||||
|     <uses-permission android:name="android.permission.INTERNET"/> |     <uses-permission android:name="android.permission.INTERNET"/> | ||||||
|     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> |     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> | ||||||
|  |     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> | ||||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> |     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> | ||||||
|     <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/> |     <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/> | ||||||
|     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/> |     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/> | ||||||
| @@ -19,8 +19,7 @@ | |||||||
|         android:icon="@mipmap/ic_launcher" |         android:icon="@mipmap/ic_launcher" | ||||||
|         android:label="@string/app_name" |         android:label="@string/app_name" | ||||||
|         android:theme="@style/AppTheme" |         android:theme="@style/AppTheme" | ||||||
|         android:name="android.support.multidex.MultiDexApplication" |         android:name="android.support.multidex.MultiDexApplication"> | ||||||
|         tools:replace="android:allowBackup"> |  | ||||||
|         <activity |         <activity | ||||||
|             android:name=".MainActivity" |             android:name=".MainActivity" | ||||||
|             android:label="@string/app_name"> |             android:label="@string/app_name"> | ||||||
| @@ -84,16 +83,6 @@ | |||||||
|                 <category android:name="android.intent.category.DEFAULT"/> |                 <category android:name="android.intent.category.DEFAULT"/> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </activity> |         </activity> | ||||||
|         <activity |  | ||||||
|             android:name=".SettingsActivity" |  | ||||||
|             android:label="@string/settings" |  | ||||||
|             android:parentActivityName=".MainActivity"> |  | ||||||
|             <intent-filter> |  | ||||||
|                 <action android:name="android.intent.action.MANAGE_NETWORK_USAGE"/> |  | ||||||
|  |  | ||||||
|                 <category android:name="android.intent.category.DEFAULT"/> |  | ||||||
|             </intent-filter> |  | ||||||
|         </activity> |  | ||||||
|         <activity |         <activity | ||||||
|             android:name=".CreateAddressActivity" |             android:name=".CreateAddressActivity" | ||||||
|             android:label="@string/title_activity_open_bitmessage_link" |             android:label="@string/title_activity_open_bitmessage_link" | ||||||
| @@ -144,6 +133,17 @@ | |||||||
|             android:exported="false" |             android:exported="false" | ||||||
|             android:syncable="true"/> |             android:syncable="true"/> | ||||||
|  |  | ||||||
|  |         <!-- Exports --> | ||||||
|  |         <provider | ||||||
|  |             android:name="android.support.v4.content.FileProvider" | ||||||
|  |             android:authorities="ch.dissem.apps.abit.fileprovider" | ||||||
|  |             android:exported="false" | ||||||
|  |             android:grantUriPermissions="true"> | ||||||
|  |             <meta-data | ||||||
|  |                 android:name="android.support.FILE_PROVIDER_PATHS" | ||||||
|  |                 android:resource="@xml/file_paths" /> | ||||||
|  |         </provider> | ||||||
|  |  | ||||||
|         <service |         <service | ||||||
|             android:name=".synchronization.AuthenticatorService" |             android:name=".synchronization.AuthenticatorService" | ||||||
|             android:exported="true" |             android:exported="true" | ||||||
| @@ -173,19 +173,28 @@ | |||||||
|             android:exported="false"/> |             android:exported="false"/> | ||||||
|  |  | ||||||
|         <!-- Receive Wi-Fi connection state changes --> |         <!-- Receive Wi-Fi connection state changes --> | ||||||
|         <receiver android:name=".listener.WifiReceiver"> |         <receiver android:name=".listener.WifiReceiver" android:enabled="@bool/is_pre_api_21"> | ||||||
|             <intent-filter> |             <intent-filter> | ||||||
|                 <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> |                 <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </receiver> |         </receiver> | ||||||
|  |         <receiver android:name=".service.StartServiceReceiver" android:enabled="@bool/is_post_api_21"> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="android.intent.action.BOOT_COMPLETED" /> | ||||||
|  |             </intent-filter> | ||||||
|  |         </receiver> | ||||||
|  |         <service | ||||||
|  |             android:name=".service.StartupNodeOnWifiService" | ||||||
|  |             android:permission="android.permission.BIND_JOB_SERVICE" | ||||||
|  |             android:exported="true"/> | ||||||
|  |  | ||||||
|         <activity |         <activity | ||||||
|             android:name=".StatusActivity" |             android:name=".StatusActivity" | ||||||
|             android:label="@string/title_activity_status" |             android:label="@string/title_activity_status" | ||||||
|             android:parentActivityName=".SettingsActivity"> |             android:parentActivityName=".MainActivity"> | ||||||
|             <meta-data |             <meta-data | ||||||
|                 android:name="android.support.PARENT_ACTIVITY" |                 android:name="android.support.PARENT_ACTIVITY" | ||||||
|                 android:value=".SettingsActivity"/> |                 android:value=".MainActivity"/> | ||||||
|         </activity> |         </activity> | ||||||
|     </application> |     </application> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,146 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.v4.app.ListFragment; |  | ||||||
| import android.view.View; |  | ||||||
| import android.widget.ListView; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.listener.ListSelectionListener; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public abstract class AbstractItemListFragment<T> extends ListFragment implements ListHolder { |  | ||||||
|     /** |  | ||||||
|      * The serialization (saved instance state) Bundle key representing the |  | ||||||
|      * activated item position. Only used on tablets. |  | ||||||
|      */ |  | ||||||
|     private static final String STATE_ACTIVATED_POSITION = "activated_position"; |  | ||||||
|     /** |  | ||||||
|      * A dummy implementation of the {@link ListSelectionListener} interface that does |  | ||||||
|      * nothing. Used only when this fragment is not attached to an activity. |  | ||||||
|      */ |  | ||||||
|     private static final ListSelectionListener<Object> dummyCallbacks = |  | ||||||
|         new ListSelectionListener<Object>() { |  | ||||||
|             @Override |  | ||||||
|             public void onItemSelected(Object item) { |  | ||||||
|                 // NO OP |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|     /** |  | ||||||
|      * The fragment's current callback object, which is notified of list item |  | ||||||
|      * clicks. |  | ||||||
|      */ |  | ||||||
|     private ListSelectionListener<? super T> callbacks = dummyCallbacks; |  | ||||||
|     /** |  | ||||||
|      * The current activated item position. Only used on tablets. |  | ||||||
|      */ |  | ||||||
|     private int activatedPosition = ListView.INVALID_POSITION; |  | ||||||
|     private boolean activateOnItemClick; |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onViewCreated(View view, Bundle savedInstanceState) { |  | ||||||
|         super.onViewCreated(view, savedInstanceState); |  | ||||||
|  |  | ||||||
|         // Restore the previously serialized activated item position. |  | ||||||
|         if (savedInstanceState != null |  | ||||||
|             && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { |  | ||||||
|             setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onResume() { |  | ||||||
|         super.onResume(); |  | ||||||
|  |  | ||||||
|         // When setting CHOICE_MODE_SINGLE, ListView will automatically |  | ||||||
|         // give items the 'activated' state when touched. |  | ||||||
|         getListView().setChoiceMode(activateOnItemClick |  | ||||||
|             ? ListView.CHOICE_MODE_SINGLE |  | ||||||
|             : ListView.CHOICE_MODE_NONE); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onAttach(Context context) { |  | ||||||
|         super.onAttach(context); |  | ||||||
|  |  | ||||||
|         // Activities containing this fragment must implement its callbacks. |  | ||||||
|         if (context instanceof ListSelectionListener) { |  | ||||||
|             //noinspection unchecked |  | ||||||
|             callbacks = (ListSelectionListener) context; |  | ||||||
|         } else { |  | ||||||
|             throw new IllegalStateException("Activity must implement fragment's callbacks."); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onDetach() { |  | ||||||
|         super.onDetach(); |  | ||||||
|  |  | ||||||
|         // Reset the active callbacks interface to the dummy implementation. |  | ||||||
|         callbacks = dummyCallbacks; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onListItemClick(ListView listView, View view, int position, long id) { |  | ||||||
|         super.onListItemClick(listView, view, position, id); |  | ||||||
|  |  | ||||||
|         // Notify the active callbacks interface (the activity, if the |  | ||||||
|         // fragment is attached to one) that an item has been selected. |  | ||||||
|         //noinspection unchecked |  | ||||||
|         callbacks.onItemSelected((T) listView.getItemAtPosition(position)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onSaveInstanceState(Bundle outState) { |  | ||||||
|         super.onSaveInstanceState(outState); |  | ||||||
|         if (activatedPosition != ListView.INVALID_POSITION) { |  | ||||||
|             // Serialize and persist the activated item position. |  | ||||||
|             outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Turns on activate-on-click mode. When this mode is on, list items will be |  | ||||||
|      * given the 'activated' state when touched. |  | ||||||
|      */ |  | ||||||
|     public void setActivateOnItemClick(boolean activateOnItemClick) { |  | ||||||
|         this.activateOnItemClick = activateOnItemClick; |  | ||||||
|  |  | ||||||
|         if (isVisible()) { |  | ||||||
|             // When setting CHOICE_MODE_SINGLE, ListView will automatically |  | ||||||
|             // give items the 'activated' state when touched. |  | ||||||
|             getListView().setChoiceMode(activateOnItemClick |  | ||||||
|                 ? ListView.CHOICE_MODE_SINGLE |  | ||||||
|                 : ListView.CHOICE_MODE_NONE); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void setActivatedPosition(int position) { |  | ||||||
|         if (position == ListView.INVALID_POSITION) { |  | ||||||
|             getListView().setItemChecked(activatedPosition, false); |  | ||||||
|         } else { |  | ||||||
|             getListView().setItemChecked(position, true); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         activatedPosition = position; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,147 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v4.app.ListFragment | ||||||
|  | import android.view.View | ||||||
|  | import android.widget.ListView | ||||||
|  |  | ||||||
|  | import ch.dissem.apps.abit.listener.ListSelectionListener | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | abstract class AbstractItemListFragment<L, T> : ListFragment(), ListHolder<L> { | ||||||
|  |     /** | ||||||
|  |      * The fragment's current callback object, which is notified of list item | ||||||
|  |      * clicks. | ||||||
|  |      */ | ||||||
|  |     @Suppress("UNCHECKED_CAST") | ||||||
|  |     private var callbacks: ListSelectionListener<T> = DummyCallback as ListSelectionListener<T> | ||||||
|  |     /** | ||||||
|  |      * The current activated item position. Only used on tablets. | ||||||
|  |      */ | ||||||
|  |     private var activatedPosition = ListView.INVALID_POSITION | ||||||
|  |     private var activateOnItemClick: Boolean = false | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|  |         // Restore the previously serialized activated item position. | ||||||
|  |         if (savedInstanceState != null && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { | ||||||
|  |             setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onResume() { | ||||||
|  |         super.onResume() | ||||||
|  |  | ||||||
|  |         // When setting CHOICE_MODE_SINGLE, ListView will automatically | ||||||
|  |         // give items the 'activated' state when touched. | ||||||
|  |         listView.choiceMode = if (activateOnItemClick) | ||||||
|  |             ListView.CHOICE_MODE_SINGLE | ||||||
|  |         else | ||||||
|  |             ListView.CHOICE_MODE_NONE | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onAttach(context: Context?) { | ||||||
|  |         super.onAttach(context) | ||||||
|  |  | ||||||
|  |         // Activities containing this fragment must implement its callbacks. | ||||||
|  |         if (context is ListSelectionListener<*>) { | ||||||
|  |             @Suppress("UNCHECKED_CAST") | ||||||
|  |             callbacks = context as ListSelectionListener<T> | ||||||
|  |         } else { | ||||||
|  |             throw IllegalStateException("Activity must implement fragment's callbacks.") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onDetach() { | ||||||
|  |         super.onDetach() | ||||||
|  |  | ||||||
|  |         // Reset the active callbacks interface to the dummy implementation. | ||||||
|  |         @Suppress("UNCHECKED_CAST") | ||||||
|  |         callbacks = DummyCallback as ListSelectionListener<T> | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onListItemClick(listView: ListView, view: View?, position: Int, id: Long) { | ||||||
|  |         super.onListItemClick(listView, view, position, id) | ||||||
|  |  | ||||||
|  |         // Notify the active callbacks interface (the activity, if the | ||||||
|  |         // fragment is attached to one) that an item has been selected. | ||||||
|  |         @Suppress("UNCHECKED_CAST") | ||||||
|  |         (listView.getItemAtPosition(position) as? T)?.let { | ||||||
|  |             callbacks.onItemSelected(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onSaveInstanceState(outState: Bundle) { | ||||||
|  |         super.onSaveInstanceState(outState) | ||||||
|  |         if (activatedPosition != ListView.INVALID_POSITION) { | ||||||
|  |             // Serialize and persist the activated item position. | ||||||
|  |             outState.putInt(STATE_ACTIVATED_POSITION, activatedPosition) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Turns on activate-on-click mode. When this mode is on, list items will be | ||||||
|  |      * given the 'activated' state when touched. | ||||||
|  |      */ | ||||||
|  |     override fun setActivateOnItemClick(activateOnItemClick: Boolean) { | ||||||
|  |         this.activateOnItemClick = activateOnItemClick | ||||||
|  |  | ||||||
|  |         if (isVisible) { | ||||||
|  |             // When setting CHOICE_MODE_SINGLE, ListView will automatically | ||||||
|  |             // give items the 'activated' state when touched. | ||||||
|  |             listView.choiceMode = if (activateOnItemClick) | ||||||
|  |                 ListView.CHOICE_MODE_SINGLE | ||||||
|  |             else | ||||||
|  |                 ListView.CHOICE_MODE_NONE | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setActivatedPosition(position: Int) { | ||||||
|  |         if (position == ListView.INVALID_POSITION) { | ||||||
|  |             listView.setItemChecked(activatedPosition, false) | ||||||
|  |         } else { | ||||||
|  |             listView.setItemChecked(position, true) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         activatedPosition = position | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun showPreviousList() = false | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A dummy implementation of the [ListSelectionListener] interface that does | ||||||
|  |      * nothing. Used only when this fragment is not attached to an activity. | ||||||
|  |      */ | ||||||
|  |     internal object DummyCallback : ListSelectionListener<Any> { | ||||||
|  |         override fun onItemSelected(item: Any) = Unit // NO OP | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * The serialization (saved instance state) Bundle key representing the | ||||||
|  |          * activated item position. Only used on tablets. | ||||||
|  |          */ | ||||||
|  |         internal const val STATE_ACTIVATED_POSITION = "activated_position" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -14,25 +14,25 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package ch.dissem.apps.abit; | package ch.dissem.apps.abit | ||||||
| 
 | 
 | ||||||
| import android.os.Bundle; | import android.os.Bundle | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * An activity representing a single Subscription detail screen. This |  * An activity representing a single Subscription detail screen. This | ||||||
|  * activity is only used on handset devices. On tablet-size devices, |  * activity is only used on handset devices. On tablet-size devices, | ||||||
|  * item details are presented side-by-side with a list of items |  * item details are presented side-by-side with a list of items | ||||||
|  * in a {@link MainActivity}. |  * in a [MainActivity]. | ||||||
|  * <p/> |  * | ||||||
|  |  * | ||||||
|  * This activity is mostly just a 'shell' activity containing nothing |  * This activity is mostly just a 'shell' activity containing nothing | ||||||
|  * more than a {@link AddressDetailFragment}. |  * more than a [AddressDetailFragment]. | ||||||
|  */ |  */ | ||||||
| public class AddressDetailActivity extends DetailActivity { | class AddressDetailActivity : DetailActivity() { | ||||||
| 
 | 
 | ||||||
|     @Override |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |         super.onCreate(savedInstanceState) | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
| 
 | 
 | ||||||
|         // savedInstanceState is non-null when there is fragment state |         // savedInstanceState is non-null when there is fragment state | ||||||
|         // saved from previous configurations of this activity |         // saved from previous configurations of this activity | ||||||
| @@ -42,18 +42,18 @@ public class AddressDetailActivity extends DetailActivity { | |||||||
|         // For more information, see the Fragments API guide at: |         // For more information, see the Fragments API guide at: | ||||||
|         // |         // | ||||||
|         // http://developer.android.com/guide/components/fragments.html |         // http://developer.android.com/guide/components/fragments.html | ||||||
|         // | 
 | ||||||
|         if (savedInstanceState == null) { |         if (savedInstanceState == null) { | ||||||
|             // Create the detail fragment and add it to the activity |             // Create the detail fragment and add it to the activity | ||||||
|             // using a fragment transaction. |             // using a fragment transaction. | ||||||
|             Bundle arguments = new Bundle(); |             val arguments = Bundle() | ||||||
|             arguments.putSerializable(AddressDetailFragment.ARG_ITEM, |             arguments.putSerializable(AddressDetailFragment.ARG_ITEM, | ||||||
|                     getIntent().getSerializableExtra(AddressDetailFragment.ARG_ITEM)); |                     intent.getSerializableExtra(AddressDetailFragment.ARG_ITEM)) | ||||||
|             AddressDetailFragment fragment = new AddressDetailFragment(); |             val fragment = AddressDetailFragment() | ||||||
|             fragment.setArguments(arguments); |             fragment.arguments = arguments | ||||||
|             getSupportFragmentManager().beginTransaction() |             supportFragmentManager.beginTransaction() | ||||||
|                     .add(R.id.content, fragment) |                     .add(R.id.content, fragment) | ||||||
|                     .commit(); |                     .commit() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,263 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.app.AlertDialog; |  | ||||||
| import android.content.DialogInterface; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.v4.app.Fragment; |  | ||||||
| import android.support.v4.app.FragmentActivity; |  | ||||||
| import android.text.Editable; |  | ||||||
| import android.text.TextWatcher; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.Menu; |  | ||||||
| import android.view.MenuInflater; |  | ||||||
| import android.view.MenuItem; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.CompoundButton; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.Switch; |  | ||||||
| import android.widget.TextView; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import com.mikepenz.community_material_typeface_library.CommunityMaterial; |  | ||||||
| import com.mikepenz.google_material_typeface_library.GoogleMaterial; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.apps.abit.util.Drawables; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.wif.WifExporter; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A fragment representing a single Message detail screen. |  | ||||||
|  * This fragment is either contained in a {@link MainActivity} |  | ||||||
|  * in two-pane mode (on tablets) or a {@link MessageDetailActivity} |  | ||||||
|  * on handsets. |  | ||||||
|  */ |  | ||||||
| public class AddressDetailFragment extends Fragment { |  | ||||||
|     /** |  | ||||||
|      * The fragment argument representing the item ID that this fragment |  | ||||||
|      * represents. |  | ||||||
|      */ |  | ||||||
|     public static final String ARG_ITEM = "item"; |  | ||||||
|     public static final String EXPORT_POSTFIX = ".keys.dat"; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The content this fragment is presenting. |  | ||||||
|      */ |  | ||||||
|     private BitmessageAddress item; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Mandatory empty constructor for the fragment manager to instantiate the |  | ||||||
|      * fragment (e.g. upon screen orientation changes). |  | ||||||
|      */ |  | ||||||
|     public AddressDetailFragment() { |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|  |  | ||||||
|         if (getArguments().containsKey(ARG_ITEM)) { |  | ||||||
|             // Load the dummy content specified by the fragment |  | ||||||
|             // arguments. In a real-world scenario, use a Loader |  | ||||||
|             // to load content from a content provider. |  | ||||||
|             item = (BitmessageAddress) getArguments().getSerializable(ARG_ITEM); |  | ||||||
|         } |  | ||||||
|         setHasOptionsMenu(true); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |  | ||||||
|         inflater.inflate(R.menu.address, menu); |  | ||||||
|  |  | ||||||
|         FragmentActivity activity = getActivity(); |  | ||||||
|         Drawables.addIcon(activity, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail); |  | ||||||
|         Drawables.addIcon(activity, menu, R.id.share, GoogleMaterial.Icon.gmd_share); |  | ||||||
|         Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); |  | ||||||
|         Drawables.addIcon(activity, menu, R.id.export, |  | ||||||
|             CommunityMaterial.Icon.cmd_export) |  | ||||||
|             .setVisible(item != null && item.getPrivateKey() != null); |  | ||||||
|  |  | ||||||
|         super.onCreateOptionsMenu(menu, inflater); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public boolean onOptionsItemSelected(MenuItem menuItem) { |  | ||||||
|         final Activity ctx = getActivity(); |  | ||||||
|         switch (menuItem.getItemId()) { |  | ||||||
|             case R.id.write_message: { |  | ||||||
|                 BitmessageAddress identity = Singleton.getIdentity(ctx); |  | ||||||
|                 if (identity == null) { |  | ||||||
|                     Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show(); |  | ||||||
|                 } else { |  | ||||||
|                     Intent intent = new Intent(ctx, ComposeMessageActivity.class); |  | ||||||
|                     intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity); |  | ||||||
|                     intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item); |  | ||||||
|                     startActivity(intent); |  | ||||||
|                 } |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|             case R.id.delete: { |  | ||||||
|                 int warning; |  | ||||||
|                 if (item.getPrivateKey() != null) |  | ||||||
|                     warning = R.string.delete_identity_warning; |  | ||||||
|                 else |  | ||||||
|                     warning = R.string.delete_contact_warning; |  | ||||||
|                 new AlertDialog.Builder(ctx) |  | ||||||
|                     .setMessage(warning) |  | ||||||
|                     .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { |  | ||||||
|                         @Override |  | ||||||
|                         public void onClick(DialogInterface dialog, int which) { |  | ||||||
|                             Singleton.getAddressRepository(ctx).remove(item); |  | ||||||
|                             MainActivity mainActivity = MainActivity.getInstance(); |  | ||||||
|                             if (item.getPrivateKey() != null && mainActivity != null) { |  | ||||||
|                                 mainActivity.removeIdentityEntry(item); |  | ||||||
|                             } |  | ||||||
|                             item = null; |  | ||||||
|                             ctx.onBackPressed(); |  | ||||||
|                         } |  | ||||||
|                     }) |  | ||||||
|                     .setNegativeButton(android.R.string.no, null) |  | ||||||
|                     .show(); |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|             case R.id.export: { |  | ||||||
|                 new AlertDialog.Builder(ctx) |  | ||||||
|                     .setMessage(R.string.confirm_export) |  | ||||||
|                     .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { |  | ||||||
|                         @Override |  | ||||||
|                         public void onClick(DialogInterface dialog, int which) { |  | ||||||
|                             Intent shareIntent = new Intent(Intent.ACTION_SEND); |  | ||||||
|                             shareIntent.setType("text/plain"); |  | ||||||
|                             shareIntent.putExtra(Intent.EXTRA_TITLE, item + |  | ||||||
|                                 EXPORT_POSTFIX); |  | ||||||
|                             WifExporter exporter = new WifExporter(Singleton |  | ||||||
|                                 .getBitmessageContext(ctx)); |  | ||||||
|                             exporter.addIdentity(item); |  | ||||||
|                             shareIntent.putExtra(Intent.EXTRA_TEXT, exporter.toString |  | ||||||
|                                 ()); |  | ||||||
|                             startActivity(Intent.createChooser(shareIntent, null)); |  | ||||||
|                         } |  | ||||||
|                     }) |  | ||||||
|                     .setNegativeButton(android.R.string.no, null) |  | ||||||
|                     .show(); |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|             case R.id.share: { |  | ||||||
|                 Intent shareIntent = new Intent(Intent.ACTION_SEND); |  | ||||||
|                 shareIntent.setType("text/plain"); |  | ||||||
|                 shareIntent.putExtra(Intent.EXTRA_TEXT, item.getAddress()); |  | ||||||
|                 startActivity(Intent.createChooser(shareIntent, null)); |  | ||||||
|             } |  | ||||||
|             default: |  | ||||||
|                 return false; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, |  | ||||||
|                              Bundle savedInstanceState) { |  | ||||||
|         View rootView = inflater.inflate(R.layout.fragment_address_detail, container, false); |  | ||||||
|  |  | ||||||
|         // Show the dummy content as text in a TextView. |  | ||||||
|         if (item != null) { |  | ||||||
|             FragmentActivity activity = getActivity(); |  | ||||||
|             if (item.isChan()) { |  | ||||||
|                 activity.setTitle(R.string.title_chan_detail); |  | ||||||
|             } else if (item.getPrivateKey() != null) { |  | ||||||
|                 activity.setTitle(R.string.title_identity_detail); |  | ||||||
|             } else if (item.isSubscribed()) { |  | ||||||
|                 activity.setTitle(R.string.title_subscription_detail); |  | ||||||
|             } else { |  | ||||||
|                 activity.setTitle(R.string.title_contact_detail); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             ((ImageView) rootView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); |  | ||||||
|             TextView name = (TextView) rootView.findViewById(R.id.name); |  | ||||||
|             name.setText(item.toString()); |  | ||||||
|             name.addTextChangedListener(new TextWatcher() { |  | ||||||
|                 @Override |  | ||||||
|                 public void beforeTextChanged(CharSequence s, int start, int count, int after) { |  | ||||||
|                     // Nothing to do |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 @Override |  | ||||||
|                 public void onTextChanged(CharSequence s, int start, int before, int count) { |  | ||||||
|                     // Nothing to do |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 @Override |  | ||||||
|                 public void afterTextChanged(Editable s) { |  | ||||||
|                     item.setAlias(s.toString()); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|             TextView address = (TextView) rootView.findViewById(R.id.address); |  | ||||||
|             address.setText(item.getAddress()); |  | ||||||
|             address.setSelected(true); |  | ||||||
|             ((TextView) rootView.findViewById(R.id.stream_number)).setText( |  | ||||||
|                 getString(R.string.stream_number, item.getStream())); |  | ||||||
|             if (item.getPrivateKey() == null) { |  | ||||||
|                 Switch active = (Switch) rootView.findViewById(R.id.active); |  | ||||||
|                 active.setChecked(item.isSubscribed()); |  | ||||||
|                 active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { |  | ||||||
|                     @Override |  | ||||||
|                     public void onCheckedChanged(CompoundButton button, boolean checked) { |  | ||||||
|                         item.setSubscribed(checked); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|                 ImageView pubkeyAvailableImg = (ImageView) rootView.findViewById(R.id |  | ||||||
|                     .pubkey_available); |  | ||||||
|  |  | ||||||
|                 if (item.getPubkey() == null) { |  | ||||||
|                     pubkeyAvailableImg.setAlpha(0.3f); |  | ||||||
|                     TextView pubkeyAvailableDesc = (TextView) rootView.findViewById(R.id |  | ||||||
|                         .pubkey_available_desc); |  | ||||||
|                     pubkeyAvailableDesc.setText(R.string.pubkey_not_available); |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 rootView.findViewById(R.id.active).setVisibility(View.GONE); |  | ||||||
|                 rootView.findViewById(R.id.pubkey_available).setVisibility(View.GONE); |  | ||||||
|                 rootView.findViewById(R.id.pubkey_available_desc).setVisibility(View.GONE); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // QR code |  | ||||||
|             ImageView qrCode = (ImageView) rootView.findViewById(R.id.qr_code); |  | ||||||
|             qrCode.setImageBitmap(Drawables.qrCode(item)); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return rootView; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onPause() { |  | ||||||
|         if (item != null) { |  | ||||||
|             Singleton.getAddressRepository(getContext()).save(item); |  | ||||||
|             MainActivity mainActivity = MainActivity.getInstance(); |  | ||||||
|             if (mainActivity != null && item.getPrivateKey() != null) { |  | ||||||
|                 mainActivity.updateIdentityEntry(item); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         super.onPause(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										210
									
								
								app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.app.AlertDialog | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v4.app.Fragment | ||||||
|  | import android.text.Editable | ||||||
|  | import android.text.TextWatcher | ||||||
|  | import android.view.* | ||||||
|  | import android.widget.Toast | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.apps.abit.util.Drawables | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  | import ch.dissem.bitmessage.wif.WifExporter | ||||||
|  | import com.mikepenz.community_material_typeface_library.CommunityMaterial | ||||||
|  | import com.mikepenz.google_material_typeface_library.GoogleMaterial | ||||||
|  | import kotlinx.android.synthetic.main.fragment_address_detail.* | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A fragment representing a single Message detail screen. | ||||||
|  |  * This fragment is either contained in a [MainActivity] | ||||||
|  |  * in two-pane mode (on tablets) or a [MessageDetailActivity] | ||||||
|  |  * on handsets. | ||||||
|  |  */ | ||||||
|  | class AddressDetailFragment : Fragment() { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The content this fragment is presenting. | ||||||
|  |      */ | ||||||
|  |     private var item: BitmessageAddress? = null | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |  | ||||||
|  |         arguments?.let { arguments -> | ||||||
|  |             if (arguments.containsKey(ARG_ITEM)) { | ||||||
|  |                 item = arguments.getSerializable(ARG_ITEM) as BitmessageAddress | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         setHasOptionsMenu(true) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||||
|  |         inflater.inflate(R.menu.address, menu) | ||||||
|  |  | ||||||
|  |         val ctx = activity!! | ||||||
|  |         Drawables.addIcon(ctx, menu, R.id.write_message, GoogleMaterial.Icon.gmd_mail) | ||||||
|  |         Drawables.addIcon(ctx, menu, R.id.share, GoogleMaterial.Icon.gmd_share) | ||||||
|  |         Drawables.addIcon(ctx, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete) | ||||||
|  |         Drawables.addIcon(ctx, menu, R.id.export, CommunityMaterial.Icon.cmd_export).isVisible = item?.privateKey != null | ||||||
|  |  | ||||||
|  |         super.onCreateOptionsMenu(menu, inflater) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean { | ||||||
|  |         val item = item ?: return false | ||||||
|  |         val ctx = activity ?: return false | ||||||
|  |         when (menuItem.itemId) { | ||||||
|  |             R.id.write_message -> { | ||||||
|  |                 val identity = Singleton.getIdentity(ctx) | ||||||
|  |                 if (identity == null) { | ||||||
|  |                     Toast.makeText(ctx, R.string.no_identity_warning, Toast.LENGTH_LONG).show() | ||||||
|  |                 } else { | ||||||
|  |                     val intent = Intent(ctx, ComposeMessageActivity::class.java) | ||||||
|  |                     intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, identity) | ||||||
|  |                     intent.putExtra(ComposeMessageActivity.EXTRA_RECIPIENT, item) | ||||||
|  |                     startActivity(intent) | ||||||
|  |                 } | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             R.id.delete -> { | ||||||
|  |                 val warning = if (item.privateKey != null) | ||||||
|  |                     R.string.delete_identity_warning | ||||||
|  |                 else | ||||||
|  |                     R.string.delete_contact_warning | ||||||
|  |                 AlertDialog.Builder(ctx) | ||||||
|  |                     .setMessage(warning) | ||||||
|  |                     .setPositiveButton(android.R.string.yes) { _, _ -> | ||||||
|  |                         Singleton.getAddressRepository(ctx).remove(item) | ||||||
|  |                         MainActivity.apply { | ||||||
|  |                             if (item.privateKey != null) { | ||||||
|  |                                 removeIdentityEntry(item) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         this.item = null | ||||||
|  |                         ctx.onBackPressed() | ||||||
|  |                     } | ||||||
|  |                     .setNegativeButton(android.R.string.no, null) | ||||||
|  |                     .show() | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             R.id.export -> { | ||||||
|  |                 AlertDialog.Builder(ctx) | ||||||
|  |                     .setMessage(R.string.confirm_export) | ||||||
|  |                     .setPositiveButton(android.R.string.yes) { _, _ -> | ||||||
|  |                         val shareIntent = Intent(Intent.ACTION_SEND).apply { | ||||||
|  |                             type = "text/plain" | ||||||
|  |                             putExtra( | ||||||
|  |                                 Intent.EXTRA_TITLE, | ||||||
|  |                                 "$item$EXPORT_POSTFIX" | ||||||
|  |                             ) | ||||||
|  |                             putExtra( | ||||||
|  |                                 Intent.EXTRA_TEXT, | ||||||
|  |                                 WifExporter(Singleton.getBitmessageContext(ctx)).apply { | ||||||
|  |                                     addIdentity(item) | ||||||
|  |                                 }.toString() | ||||||
|  |                             ) | ||||||
|  |                         } | ||||||
|  |                         startActivity(Intent.createChooser(shareIntent, null)) | ||||||
|  |                     } | ||||||
|  |                     .setNegativeButton(android.R.string.no, null) | ||||||
|  |                     .show() | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             R.id.share -> { | ||||||
|  |                 val shareIntent = Intent(Intent.ACTION_SEND) | ||||||
|  |                 shareIntent.type = "text/plain" | ||||||
|  |                 shareIntent.putExtra(Intent.EXTRA_TEXT, item.address) | ||||||
|  |                 startActivity(Intent.createChooser(shareIntent, null)) | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             else -> return false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View | ||||||
|  |         = inflater.inflate(R.layout.fragment_address_detail, container, false) | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |         // Show the dummy content as text in a TextView. | ||||||
|  |         item?.let { item -> | ||||||
|  |             activity?.let { activity -> | ||||||
|  |                 when { | ||||||
|  |                     item.isChan -> activity.setTitle(R.string.title_chan_detail) | ||||||
|  |                     item.privateKey != null -> activity.setTitle(R.string.title_identity_detail) | ||||||
|  |                     item.isSubscribed -> activity.setTitle(R.string.title_subscription_detail) | ||||||
|  |                     else -> activity.setTitle(R.string.title_contact_detail) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             avatar.setImageDrawable(Identicon(item)) | ||||||
|  |             name.setText(item.toString()) | ||||||
|  |             name.addTextChangedListener(object : TextWatcher { | ||||||
|  |                 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit // Nothing to do | ||||||
|  |  | ||||||
|  |                 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit // Nothing to do | ||||||
|  |  | ||||||
|  |                 override fun afterTextChanged(s: Editable) { | ||||||
|  |                     item.alias = s.toString() | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             address.text = item.address | ||||||
|  |             address.isSelected = true | ||||||
|  |             stream_number.text = getString(R.string.stream_number, item.stream) | ||||||
|  |             if (item.privateKey == null) { | ||||||
|  |                 active.isChecked = item.isSubscribed | ||||||
|  |                 active.setOnCheckedChangeListener { _, checked -> item.isSubscribed = checked } | ||||||
|  |  | ||||||
|  |                 if (item.pubkey == null) { | ||||||
|  |                     pubkey_available.alpha = 0.3f | ||||||
|  |                     pubkey_available_desc.setText(R.string.pubkey_not_available) | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 active.visibility = View.GONE | ||||||
|  |                 pubkey_available.visibility = View.GONE | ||||||
|  |                 pubkey_available_desc.visibility = View.GONE | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // QR code | ||||||
|  |             qr_code.setImageBitmap(Drawables.qrCode(item)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onPause() { | ||||||
|  |         item?.let { item -> | ||||||
|  |             Singleton.getAddressRepository(context!!).save(item) | ||||||
|  |             if (item.privateKey != null) { | ||||||
|  |                 MainActivity.apply { updateIdentityEntry(item) } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         super.onPause() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * The fragment argument representing the item ID that this fragment | ||||||
|  |          * represents. | ||||||
|  |          */ | ||||||
|  |         val ARG_ITEM = "item" | ||||||
|  |         val EXPORT_POSTFIX = ".keys.dat" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,166 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.annotation.NonNull; |  | ||||||
| import android.support.annotation.Nullable; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.MenuItem; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.ArrayAdapter; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.TextView; |  | ||||||
|  |  | ||||||
| import com.google.zxing.integration.android.IntentIntegrator; |  | ||||||
|  |  | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.Comparator; |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.listener.ActionBarListener; |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.Label; |  | ||||||
| import io.github.yavski.fabspeeddial.FabSpeedDial; |  | ||||||
| import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Fragment that shows a list of all contacts, the ones we subscribed to first. |  | ||||||
|  */ |  | ||||||
| public class AddressListFragment extends AbstractItemListFragment<BitmessageAddress> { |  | ||||||
|     @Override |  | ||||||
|     public void onResume() { |  | ||||||
|         super.onResume(); |  | ||||||
|  |  | ||||||
|         updateList(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void updateList() { |  | ||||||
|         List<BitmessageAddress> addresses = Singleton.getAddressRepository(getContext()) |  | ||||||
|             .getContacts(); |  | ||||||
|         Collections.sort(addresses, new Comparator<BitmessageAddress>() { |  | ||||||
|             @Override |  | ||||||
|             public int compare(BitmessageAddress lhs, BitmessageAddress rhs) { |  | ||||||
|                 // Yields the following order: |  | ||||||
|                 // * Subscribed addresses come first |  | ||||||
|                 // * Addresses with Aliases (alphabetically) |  | ||||||
|                 // * Addresses (alphabetically) |  | ||||||
|                 if (lhs.isSubscribed() == rhs.isSubscribed()) { |  | ||||||
|                     if (lhs.getAlias() != null) { |  | ||||||
|                         if (rhs.getAlias() != null) { |  | ||||||
|                             return lhs.getAlias().compareTo(rhs.getAlias()); |  | ||||||
|                         } else { |  | ||||||
|                             return -1; |  | ||||||
|                         } |  | ||||||
|                     } else if (rhs.getAlias() != null) { |  | ||||||
|                         return 1; |  | ||||||
|                     } else { |  | ||||||
|                         return lhs.getAddress().compareTo(rhs.getAddress()); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 if (lhs.isSubscribed()) { |  | ||||||
|                     return -1; |  | ||||||
|                 } else { |  | ||||||
|                     return 1; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         setListAdapter(new ArrayAdapter<BitmessageAddress>( |  | ||||||
|             getActivity(), |  | ||||||
|             android.R.layout.simple_list_item_activated_1, |  | ||||||
|             android.R.id.text1, |  | ||||||
|             addresses) { |  | ||||||
|             @NonNull |  | ||||||
|             @Override |  | ||||||
|             public View getView(int position, View convertView, @NonNull ViewGroup parent) { |  | ||||||
|                 if (convertView == null) { |  | ||||||
|                     LayoutInflater inflater = LayoutInflater.from(getContext()); |  | ||||||
|                     convertView = inflater.inflate(R.layout.subscription_row, parent, false); |  | ||||||
|                 } |  | ||||||
|                 BitmessageAddress item = getItem(position); |  | ||||||
|                 assert item != null; |  | ||||||
|                 ((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new |  | ||||||
|                     Identicon(item)); |  | ||||||
|                 TextView name = (TextView) convertView.findViewById(R.id.name); |  | ||||||
|                 name.setText(item.toString()); |  | ||||||
|                 TextView streamNumber = (TextView) convertView.findViewById(R.id.stream_number); |  | ||||||
|                 streamNumber.setText(getContext().getString(R.string.stream_number, |  | ||||||
|                     item.getStream())); |  | ||||||
|                 convertView.findViewById(R.id.subscribed).setVisibility(item.isSubscribed() ? |  | ||||||
|                     View.VISIBLE : View.INVISIBLE); |  | ||||||
|                 return convertView; |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onAttach(Context ctx) { |  | ||||||
|         super.onAttach(ctx); |  | ||||||
|         if (ctx instanceof ActionBarListener) { |  | ||||||
|             ((ActionBarListener) ctx).updateTitle(getString(R.string.contacts_and_subscriptions)); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle |  | ||||||
|         savedInstanceState) { |  | ||||||
|         View view = inflater.inflate(R.layout.fragment_address_list, container, false); |  | ||||||
|  |  | ||||||
|         FabSpeedDial fabSpeedDial = (FabSpeedDial) view.findViewById(R.id.fab_add_contact); |  | ||||||
|         fabSpeedDial.setMenuListener(new SimpleMenuListenerAdapter() { |  | ||||||
|             @Override |  | ||||||
|             public boolean onMenuItemSelected(MenuItem menuItem) { |  | ||||||
|                 switch (menuItem.getItemId()) { |  | ||||||
|                     case R.id.action_read_qr_code: |  | ||||||
|                         IntentIntegrator.forSupportFragment(AddressListFragment.this) |  | ||||||
|                             .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES) |  | ||||||
|                             .initiateScan(); |  | ||||||
|                         return true; |  | ||||||
|                     case R.id.action_create_contact: |  | ||||||
|                         Intent intent = new Intent(getActivity(), CreateAddressActivity.class); |  | ||||||
|                         startActivity(intent); |  | ||||||
|                         return true; |  | ||||||
|                     default: |  | ||||||
|                         return false; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         return view; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onActivityResult(int requestCode, int resultCode, Intent data) { |  | ||||||
|         if (data != null && data.hasExtra("SCAN_RESULT")) { |  | ||||||
|             Uri uri = Uri.parse(data.getStringExtra("SCAN_RESULT")); |  | ||||||
|             Intent intent = new Intent(getActivity(), CreateAddressActivity.class); |  | ||||||
|             intent.setData(uri); |  | ||||||
|             startActivity(intent); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void updateList(Label label) { |  | ||||||
|         updateList(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										145
									
								
								app/src/main/java/ch/dissem/apps/abit/AddressListFragment.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								app/src/main/java/ch/dissem/apps/abit/AddressListFragment.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.net.Uri | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.ArrayAdapter | ||||||
|  | import android.widget.ImageView | ||||||
|  | import android.widget.TextView | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.apps.abit.util.FabUtils | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  | import com.google.zxing.integration.android.IntentIntegrator | ||||||
|  | import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu | ||||||
|  | import org.jetbrains.anko.doAsync | ||||||
|  | import org.jetbrains.anko.uiThread | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Fragment that shows a list of all contacts, the ones we subscribed to first. | ||||||
|  |  */ | ||||||
|  | class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>() { | ||||||
|  |     private lateinit var adapter: ArrayAdapter<BitmessageAddress> | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |  | ||||||
|  |         adapter = object : ArrayAdapter<BitmessageAddress>( | ||||||
|  |             activity, | ||||||
|  |             R.layout.subscription_row, | ||||||
|  |             R.id.name, | ||||||
|  |             LinkedList()) { | ||||||
|  |             override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { | ||||||
|  |                 val result: View | ||||||
|  |                 val v: ViewHolder | ||||||
|  |                 if (convertView == null) { | ||||||
|  |                     val inflater = LayoutInflater.from(context) | ||||||
|  |                     val view = inflater.inflate(R.layout.subscription_row, parent, false) | ||||||
|  |                     v = ViewHolder( | ||||||
|  |                         ctx = context, | ||||||
|  |                         avatar = view.findViewById(R.id.avatar), | ||||||
|  |                         name = view.findViewById(R.id.name), | ||||||
|  |                         streamNumber = view.findViewById(R.id.stream_number), | ||||||
|  |                         subscribed = view.findViewById(R.id.subscribed) | ||||||
|  |                     ) | ||||||
|  |                     view.tag = v | ||||||
|  |                     result = view | ||||||
|  |                 } else { | ||||||
|  |                     v = convertView.tag as ViewHolder | ||||||
|  |                     result = convertView | ||||||
|  |                 } | ||||||
|  |                 getItem(position)?.let { item -> | ||||||
|  |                     v.avatar.setImageDrawable(Identicon(item)) | ||||||
|  |                     v.name.text = item.toString() | ||||||
|  |                     v.streamNumber.text = v.ctx.getString(R.string.stream_number, item.stream) | ||||||
|  |                     v.subscribed.visibility = if (item.isSubscribed) View.VISIBLE else View.INVISIBLE | ||||||
|  |                 } | ||||||
|  |                 return result | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         listAdapter = adapter | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onResume() { | ||||||
|  |         super.onResume() | ||||||
|  |  | ||||||
|  |         initFab(activity as MainActivity) | ||||||
|  |         updateList() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun updateList() { | ||||||
|  |         adapter.clear() | ||||||
|  |         context?.let { context -> | ||||||
|  |             val addressRepo = Singleton.getAddressRepository(context) | ||||||
|  |             doAsync { | ||||||
|  |                 addressRepo.getContactIds() | ||||||
|  |                     .map { addressRepo.getAddress(it) } | ||||||
|  |                     .forEach { address -> uiThread { adapter.add(address) } } | ||||||
|  |  | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun initFab(activity: MainActivity) { | ||||||
|  |         activity.updateTitle(getString(R.string.contacts_and_subscriptions)) | ||||||
|  |         val menu = FabSpeedDialMenu(activity) | ||||||
|  |         menu.add(R.string.scan_qr_code).setIcon(R.drawable.ic_action_qr_code) | ||||||
|  |         menu.add(R.string.create_contact).setIcon(R.drawable.ic_action_create_contact) | ||||||
|  |         FabUtils.initFab(activity, R.drawable.ic_action_add_contact, menu) | ||||||
|  |             .addOnMenuItemClickListener { _, _, itemId -> | ||||||
|  |                 when (itemId) { | ||||||
|  |                     1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment) | ||||||
|  |                         .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES) | ||||||
|  |                         .initiateScan() | ||||||
|  |                     2 -> { | ||||||
|  |                         val intent = Intent(getActivity(), CreateAddressActivity::class.java) | ||||||
|  |                         startActivity(intent) | ||||||
|  |                     } | ||||||
|  |                     else -> { | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = | ||||||
|  |         inflater.inflate(R.layout.fragment_address_list, container, false) | ||||||
|  |  | ||||||
|  |     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | ||||||
|  |         if (data != null && data.hasExtra("SCAN_RESULT")) { | ||||||
|  |             val uri = Uri.parse(data.getStringExtra("SCAN_RESULT")) | ||||||
|  |             val intent = Intent(activity, CreateAddressActivity::class.java) | ||||||
|  |             intent.data = uri | ||||||
|  |             startActivity(intent) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun updateList(label: Void) = updateList() | ||||||
|  |  | ||||||
|  |     private data class ViewHolder( | ||||||
|  |         val ctx: Context, | ||||||
|  |         val avatar: ImageView, | ||||||
|  |         val name: TextView, | ||||||
|  |         val streamNumber: TextView, | ||||||
|  |         val subscribed: View | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -1,105 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.v4.app.Fragment; |  | ||||||
| import android.support.v7.app.AppCompatActivity; |  | ||||||
| import android.support.v7.widget.Toolbar; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
|  |  | ||||||
| import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Compose a new message. |  | ||||||
|  */ |  | ||||||
| public class ComposeMessageActivity extends AppCompatActivity { |  | ||||||
|     public static final String EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER"; |  | ||||||
|     public static final String EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT"; |  | ||||||
|     public static final String EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT"; |  | ||||||
|     public static final String EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT"; |  | ||||||
|     public static final String EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST"; |  | ||||||
|     public static final String EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING"; |  | ||||||
|     public static final String EXTRA_PARENT = "ch.dissem.abit.Message.PARENT"; |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|         setContentView(R.layout.toolbar_layout); |  | ||||||
|  |  | ||||||
|         Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); |  | ||||||
|         setSupportActionBar(toolbar); |  | ||||||
|  |  | ||||||
|         //noinspection ConstantConditions |  | ||||||
|         getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_action_close); |  | ||||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); |  | ||||||
|         getSupportActionBar().setHomeButtonEnabled(false); |  | ||||||
|  |  | ||||||
|         // Display the fragment as the main content. |  | ||||||
|         ComposeMessageFragment fragment = new ComposeMessageFragment(); |  | ||||||
|         fragment.setArguments(getIntent().getExtras()); |  | ||||||
|         getSupportFragmentManager().beginTransaction() |  | ||||||
|             .replace(R.id.content, fragment) |  | ||||||
|             .commit(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static void launchReplyTo(Fragment fragment, Plaintext item) { |  | ||||||
|         fragment.startActivity(getReplyIntent(fragment.getActivity(), item)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static void launchReplyTo(Activity activity, Plaintext item) { |  | ||||||
|         activity.startActivity(getReplyIntent(activity, item)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static Intent getReplyIntent(Context ctx, Plaintext item) { |  | ||||||
|         Intent replyIntent = new Intent(ctx, ComposeMessageActivity.class); |  | ||||||
|         BitmessageAddress receivingIdentity = item.getTo(); |  | ||||||
|         if (receivingIdentity.isChan()) { |  | ||||||
|             // reply to chan, not to the sender of the message |  | ||||||
|             replyIntent.putExtra(EXTRA_RECIPIENT, receivingIdentity); |  | ||||||
|             // I hate when people send as chan, so it won't be the default behaviour. |  | ||||||
|             replyIntent.putExtra(EXTRA_IDENTITY, Singleton.getIdentity(ctx)); |  | ||||||
|         } else { |  | ||||||
|             replyIntent.putExtra(EXTRA_RECIPIENT, item.getFrom()); |  | ||||||
|             replyIntent.putExtra(EXTRA_IDENTITY, receivingIdentity); |  | ||||||
|         } |  | ||||||
|         // if the original message was sent using extended encoding, use it as well |  | ||||||
|         // so features like threading can be supported |  | ||||||
|         if (item.getEncoding() == EXTENDED) { |  | ||||||
|             replyIntent.putExtra(EXTRA_ENCODING, EXTENDED); |  | ||||||
|         } |  | ||||||
|         replyIntent.putExtra(EXTRA_PARENT, item); |  | ||||||
|         String prefix; |  | ||||||
|         if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3) |  | ||||||
|             .equalsIgnoreCase("RE:")) { |  | ||||||
|             prefix = ""; |  | ||||||
|         } else { |  | ||||||
|             prefix = "RE: "; |  | ||||||
|         } |  | ||||||
|         replyIntent.putExtra(EXTRA_SUBJECT, prefix + item.getSubject()); |  | ||||||
|         replyIntent.putExtra(EXTRA_CONTENT, |  | ||||||
|             "\n\n------------------------------------------------------\n" |  | ||||||
|                 + item.getText()); |  | ||||||
|         return replyIntent; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										104
									
								
								app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.app.Activity | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v4.app.Fragment | ||||||
|  | import android.support.v7.app.AppCompatActivity | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED | ||||||
|  | import kotlinx.android.synthetic.main.toolbar_layout.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compose a new message. | ||||||
|  |  */ | ||||||
|  | class ComposeMessageActivity : AppCompatActivity() { | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         setContentView(R.layout.toolbar_layout) | ||||||
|  |  | ||||||
|  |         setSupportActionBar(toolbar) | ||||||
|  |         supportActionBar?.apply { | ||||||
|  |             setHomeAsUpIndicator(R.drawable.ic_action_close) | ||||||
|  |             setDisplayHomeAsUpEnabled(true) | ||||||
|  |             setHomeButtonEnabled(false) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Display the fragment as the main content. | ||||||
|  |         val fragment = ComposeMessageFragment() | ||||||
|  |         fragment.arguments = intent.extras | ||||||
|  |         supportFragmentManager | ||||||
|  |             .beginTransaction() | ||||||
|  |             .replace(R.id.content, fragment) | ||||||
|  |             .commit() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         const val EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER" | ||||||
|  |         const val EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT" | ||||||
|  |         const val EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT" | ||||||
|  |         const val EXTRA_CONTENT = "ch.dissem.abit.Message.CONTENT" | ||||||
|  |         const val EXTRA_BROADCAST = "ch.dissem.abit.Message.IS_BROADCAST" | ||||||
|  |         const val EXTRA_ENCODING = "ch.dissem.abit.Message.ENCODING" | ||||||
|  |         const val EXTRA_PARENT = "ch.dissem.abit.Message.PARENT" | ||||||
|  |  | ||||||
|  |         fun launchReplyTo(fragment: Fragment, item: Plaintext) = | ||||||
|  |             fragment.startActivity(getReplyIntent( | ||||||
|  |                 ctx = fragment.activity ?: throw IllegalStateException("Fragment not attached to an activity"), | ||||||
|  |                 item = item | ||||||
|  |             )) | ||||||
|  |  | ||||||
|  |         fun launchReplyTo(activity: Activity, item: Plaintext) = | ||||||
|  |             activity.startActivity(getReplyIntent(activity, item)) | ||||||
|  |  | ||||||
|  |         private fun getReplyIntent(ctx: Context, item: Plaintext): Intent { | ||||||
|  |             val replyIntent = Intent(ctx, ComposeMessageActivity::class.java) | ||||||
|  |             val receivingIdentity = item.to | ||||||
|  |             if (receivingIdentity?.isChan == true) { | ||||||
|  |                 // reply to chan, not to the sender of the message | ||||||
|  |                 replyIntent.putExtra(EXTRA_RECIPIENT, receivingIdentity) | ||||||
|  |                 // I hate when people send as chan, so it won't be the default behaviour. | ||||||
|  |                 replyIntent.putExtra(EXTRA_IDENTITY, Singleton.getIdentity(ctx)) | ||||||
|  |             } else { | ||||||
|  |                 replyIntent.putExtra(EXTRA_RECIPIENT, item.from) | ||||||
|  |                 replyIntent.putExtra(EXTRA_IDENTITY, receivingIdentity) | ||||||
|  |             } | ||||||
|  |             // if the original message was sent using extended encoding, use it as well | ||||||
|  |             // so features like threading can be supported | ||||||
|  |             if (item.encoding == EXTENDED) { | ||||||
|  |                 replyIntent.putExtra(EXTRA_ENCODING, EXTENDED) | ||||||
|  |             } | ||||||
|  |             replyIntent.putExtra(EXTRA_PARENT, item) | ||||||
|  |             item.subject?.let { subject -> | ||||||
|  |                 val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals("RE:", ignoreCase = true)) { | ||||||
|  |                     "" | ||||||
|  |                 } else { | ||||||
|  |                     "RE: " | ||||||
|  |                 } | ||||||
|  |                 replyIntent.putExtra(EXTRA_SUBJECT, prefix + subject) | ||||||
|  |             } | ||||||
|  |             replyIntent.putExtra(EXTRA_CONTENT, | ||||||
|  |                 "\n\n------------------------------------------------------\n" + item.text!!) | ||||||
|  |             return replyIntent | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,266 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.v4.app.Fragment; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.Menu; |  | ||||||
| import android.view.MenuInflater; |  | ||||||
| import android.view.MenuItem; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.AdapterView; |  | ||||||
| import android.widget.AutoCompleteTextView; |  | ||||||
| import android.widget.EditText; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.adapter.ContactAdapter; |  | ||||||
| import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment; |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.extended.Message; |  | ||||||
|  |  | ||||||
| import static android.app.Activity.RESULT_OK; |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST; |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_CONTENT; |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_ENCODING; |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_PARENT; |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_RECIPIENT; |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_SUBJECT; |  | ||||||
| import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; |  | ||||||
| import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Compose a new message. |  | ||||||
|  */ |  | ||||||
| public class ComposeMessageFragment extends Fragment { |  | ||||||
|     private BitmessageAddress identity; |  | ||||||
|     private BitmessageAddress recipient; |  | ||||||
|     private String subject; |  | ||||||
|     private String content; |  | ||||||
|     private AutoCompleteTextView recipientInput; |  | ||||||
|     private EditText subjectInput; |  | ||||||
|     private EditText bodyInput; |  | ||||||
|     private boolean broadcast; |  | ||||||
|     private Plaintext.Encoding encoding; |  | ||||||
|     private Plaintext parent; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Mandatory empty constructor for the fragment manager to instantiate the |  | ||||||
|      * fragment (e.g. upon screen orientation changes). |  | ||||||
|      */ |  | ||||||
|     public ComposeMessageFragment() { |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|         if (getArguments() != null) { |  | ||||||
|             if (getArguments().containsKey(EXTRA_IDENTITY)) { |  | ||||||
|                 identity = (BitmessageAddress) getArguments().getSerializable(EXTRA_IDENTITY); |  | ||||||
|                 if (getActivity() != null) { |  | ||||||
|                     if (identity == null || identity.getPrivateKey() == null) { |  | ||||||
|                         identity = Singleton.getIdentity(getActivity()); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 throw new RuntimeException("No identity set for ComposeMessageFragment"); |  | ||||||
|             } |  | ||||||
|             broadcast = getArguments().getBoolean(EXTRA_BROADCAST, false); |  | ||||||
|             if (getArguments().containsKey(EXTRA_RECIPIENT)) { |  | ||||||
|                 recipient = (BitmessageAddress) getArguments().getSerializable(EXTRA_RECIPIENT); |  | ||||||
|             } |  | ||||||
|             if (getArguments().containsKey(EXTRA_SUBJECT)) { |  | ||||||
|                 subject = getArguments().getString(EXTRA_SUBJECT); |  | ||||||
|             } |  | ||||||
|             if (getArguments().containsKey(EXTRA_CONTENT)) { |  | ||||||
|                 content = getArguments().getString(EXTRA_CONTENT); |  | ||||||
|             } |  | ||||||
|             if (getArguments().containsKey(EXTRA_ENCODING)) { |  | ||||||
|                 encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING); |  | ||||||
|             } else { |  | ||||||
|                 encoding = Plaintext.Encoding.SIMPLE; |  | ||||||
|             } |  | ||||||
|             if (getArguments().containsKey(EXTRA_PARENT)) { |  | ||||||
|                 parent = (Plaintext) getArguments().getSerializable(EXTRA_PARENT); |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             throw new RuntimeException("No identity set for ComposeMessageFragment"); |  | ||||||
|         } |  | ||||||
|         setHasOptionsMenu(true); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, |  | ||||||
|                              Bundle savedInstanceState) { |  | ||||||
|         View rootView = inflater.inflate(R.layout.fragment_compose_message, container, false); |  | ||||||
|         recipientInput = (AutoCompleteTextView) rootView.findViewById(R.id.recipient); |  | ||||||
|         if (broadcast) { |  | ||||||
|             recipientInput.setVisibility(View.GONE); |  | ||||||
|         } else { |  | ||||||
|             final ContactAdapter adapter = new ContactAdapter(getContext()); |  | ||||||
|             recipientInput.setAdapter(adapter); |  | ||||||
|             recipientInput.setOnItemClickListener( |  | ||||||
|                 new AdapterView.OnItemClickListener() { |  | ||||||
|                     @Override |  | ||||||
|                     public void onItemClick(AdapterView<?> parent, View view, int pos, long id) { |  | ||||||
|                         adapter.getItem(pos); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             ); |  | ||||||
|             recipientInput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { |  | ||||||
|                 @Override |  | ||||||
|                 public void onItemSelected(AdapterView<?> parent, View view, int position, long |  | ||||||
|                     id) { |  | ||||||
|                     recipient = adapter.getItem(position); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 @Override |  | ||||||
|                 public void onNothingSelected(AdapterView<?> parent) { |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|             if (recipient != null) { |  | ||||||
|                 recipientInput.setText(recipient.toString()); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         subjectInput = (EditText) rootView.findViewById(R.id.subject); |  | ||||||
|         subjectInput.setText(subject); |  | ||||||
|         bodyInput = (EditText) rootView.findViewById(R.id.body); |  | ||||||
|         bodyInput.setText(content); |  | ||||||
|  |  | ||||||
|         if (recipient == null) { |  | ||||||
|             recipientInput.requestFocus(); |  | ||||||
|         } else if (subject == null || subject.isEmpty()) { |  | ||||||
|             subjectInput.requestFocus(); |  | ||||||
|         } else { |  | ||||||
|             bodyInput.requestFocus(); |  | ||||||
|             bodyInput.setSelection(0); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return rootView; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onAttach(Context context) { |  | ||||||
|         super.onAttach(context); |  | ||||||
|         if (identity == null || identity.getPrivateKey() == null) { |  | ||||||
|             identity = Singleton.getIdentity(context); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |  | ||||||
|         inflater.inflate(R.menu.compose, menu); |  | ||||||
|         super.onCreateOptionsMenu(menu, inflater); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public boolean onOptionsItemSelected(MenuItem item) { |  | ||||||
|         switch (item.getItemId()) { |  | ||||||
|             case R.id.send: |  | ||||||
|                 send(); |  | ||||||
|                 return true; |  | ||||||
|             case R.id.select_encoding: |  | ||||||
|                 SelectEncodingDialogFragment encodingDialog = new SelectEncodingDialogFragment(); |  | ||||||
|                 Bundle args = new Bundle(); |  | ||||||
|                 args.putSerializable(EXTRA_ENCODING, encoding); |  | ||||||
|                 encodingDialog.setArguments(args); |  | ||||||
|                 encodingDialog.setTargetFragment(this, 0); |  | ||||||
|                 encodingDialog.show(getFragmentManager(), "select encoding dialog"); |  | ||||||
|                 return true; |  | ||||||
|             default: |  | ||||||
|                 return super.onOptionsItemSelected(item); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onActivityResult(int requestCode, int resultCode, Intent data) { |  | ||||||
|         if (requestCode == 0 && resultCode == RESULT_OK) { |  | ||||||
|             encoding = (Plaintext.Encoding) data.getSerializableExtra(EXTRA_ENCODING); |  | ||||||
|         } else { |  | ||||||
|             super.onActivityResult(requestCode, resultCode, data); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void send() { |  | ||||||
|         Plaintext.Builder builder; |  | ||||||
|         BitmessageContext bmc = Singleton.getBitmessageContext(getContext()); |  | ||||||
|         if (broadcast) { |  | ||||||
|             builder = new Plaintext.Builder(BROADCAST) |  | ||||||
|                 .from(identity); |  | ||||||
|         } else { |  | ||||||
|             String inputString = recipientInput.getText().toString(); |  | ||||||
|             if (recipient == null || !recipient.toString().equals(inputString)) { |  | ||||||
|                 try { |  | ||||||
|                     recipient = new BitmessageAddress(inputString); |  | ||||||
|                 } catch (Exception e) { |  | ||||||
|                     List<BitmessageAddress> contacts = Singleton.getAddressRepository |  | ||||||
|                         (getContext()).getContacts(); |  | ||||||
|                     for (BitmessageAddress contact : contacts) { |  | ||||||
|                         if (inputString.equalsIgnoreCase(contact.getAlias())) { |  | ||||||
|                             recipient = contact; |  | ||||||
|                             if (inputString.equals(contact.getAlias())) |  | ||||||
|                                 break; |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             builder = new Plaintext.Builder(MSG) |  | ||||||
|                 .from(identity) |  | ||||||
|                 .to(recipient); |  | ||||||
|         } |  | ||||||
|         switch (encoding) { |  | ||||||
|             case SIMPLE: |  | ||||||
|                 builder.message( |  | ||||||
|                     subjectInput.getText().toString(), |  | ||||||
|                     bodyInput.getText().toString() |  | ||||||
|                 ); |  | ||||||
|                 break; |  | ||||||
|             case EXTENDED: |  | ||||||
|                 builder.message( |  | ||||||
|                     new Message.Builder() |  | ||||||
|                         .subject(subjectInput.getText().toString()) |  | ||||||
|                         .body(bodyInput.getText().toString()) |  | ||||||
|                         .addParent(parent) |  | ||||||
|                         .build() |  | ||||||
|                 ); |  | ||||||
|                 break; |  | ||||||
|             default: |  | ||||||
|                 Toast.makeText( |  | ||||||
|                     getContext(), |  | ||||||
|                     getContext().getString(R.string.error_unsupported_encoding, encoding), |  | ||||||
|                     Toast.LENGTH_LONG |  | ||||||
|                 ).show(); |  | ||||||
|                 builder.message( |  | ||||||
|                     subjectInput.getText().toString(), |  | ||||||
|                     bodyInput.getText().toString() |  | ||||||
|                 ); |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|         bmc.send(builder.build()); |  | ||||||
|         getActivity().finish(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										217
									
								
								app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,217 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.app.Activity.RESULT_OK | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v4.app.Fragment | ||||||
|  | import android.view.* | ||||||
|  | import android.widget.AdapterView | ||||||
|  | import android.widget.Toast | ||||||
|  | import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST | ||||||
|  | import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_CONTENT | ||||||
|  | import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING | ||||||
|  | import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY | ||||||
|  | import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_PARENT | ||||||
|  | import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_RECIPIENT | ||||||
|  | import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_SUBJECT | ||||||
|  | import ch.dissem.apps.abit.adapter.ContactAdapter | ||||||
|  | import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.apps.abit.util.Preferences | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext.Type.MSG | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.extended.Message | ||||||
|  | import kotlinx.android.synthetic.main.fragment_compose_message.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compose a new message. | ||||||
|  |  */ | ||||||
|  | class ComposeMessageFragment : Fragment() { | ||||||
|  |     private lateinit var identity: BitmessageAddress | ||||||
|  |     private var recipient: BitmessageAddress? = null | ||||||
|  |     private var subject: String = "" | ||||||
|  |     private var content: String = "" | ||||||
|  |  | ||||||
|  |     private var broadcast: Boolean = false | ||||||
|  |     private var encoding: Plaintext.Encoding = Plaintext.Encoding.SIMPLE | ||||||
|  |     private var parent: Plaintext? = null | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         arguments?.let { arguments -> | ||||||
|  |             var id = arguments.getSerializable(EXTRA_IDENTITY) as? BitmessageAddress | ||||||
|  |             if (context != null && (id == null || id.privateKey == null)) { | ||||||
|  |                 id = Singleton.getIdentity(context!!) | ||||||
|  |             } | ||||||
|  |             if (id?.privateKey != null) { | ||||||
|  |                 identity = id | ||||||
|  |             } else { | ||||||
|  |                 throw IllegalStateException("No identity set for ComposeMessageFragment") | ||||||
|  |             } | ||||||
|  |             broadcast = arguments.getBoolean(EXTRA_BROADCAST, false) | ||||||
|  |             if (arguments.containsKey(EXTRA_RECIPIENT)) { | ||||||
|  |                 recipient = arguments.getSerializable(EXTRA_RECIPIENT) as BitmessageAddress | ||||||
|  |             } | ||||||
|  |             if (arguments.containsKey(EXTRA_SUBJECT)) { | ||||||
|  |                 subject = arguments.getString(EXTRA_SUBJECT) | ||||||
|  |             } | ||||||
|  |             if (arguments.containsKey(EXTRA_CONTENT)) { | ||||||
|  |                 content = arguments.getString(EXTRA_CONTENT) | ||||||
|  |             } | ||||||
|  |             encoding = arguments.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: Plaintext.Encoding.SIMPLE | ||||||
|  |  | ||||||
|  |             if (arguments.containsKey(EXTRA_PARENT)) { | ||||||
|  |                 parent = arguments.getSerializable(EXTRA_PARENT) as Plaintext | ||||||
|  |             } | ||||||
|  |         } ?: { | ||||||
|  |             throw IllegalStateException("No identity set for ComposeMessageFragment") | ||||||
|  |         }.invoke() | ||||||
|  |         setHasOptionsMenu(true) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, | ||||||
|  |                               savedInstanceState: Bundle?): View = | ||||||
|  |             inflater.inflate(R.layout.fragment_compose_message, container, false) | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|  |         if (broadcast) { | ||||||
|  |             recipient_input.visibility = View.GONE | ||||||
|  |         } else { | ||||||
|  |             val adapter = ContactAdapter(context!!) | ||||||
|  |             recipient_input.setAdapter(adapter) | ||||||
|  |             recipient_input.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ -> adapter.getItem(pos) } | ||||||
|  |             recipient_input.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { | ||||||
|  |                 override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { | ||||||
|  |                     recipient = adapter.getItem(position) | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 override fun onNothingSelected(parent: AdapterView<*>) = Unit // leave current selection | ||||||
|  |             } | ||||||
|  |             recipient?.let { recipient_input.setText(it.toString()) } | ||||||
|  |         } | ||||||
|  |         subject_input.setText(subject) | ||||||
|  |         body_input.setText(content) | ||||||
|  |  | ||||||
|  |         when { | ||||||
|  |             recipient == null -> recipient_input.requestFocus() | ||||||
|  |             subject.isEmpty() -> subject_input.requestFocus() | ||||||
|  |             else -> { | ||||||
|  |                 body_input.requestFocus() | ||||||
|  |                 body_input.setSelection(0) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) { | ||||||
|  |         inflater.inflate(R.menu.compose, menu) | ||||||
|  |         super.onCreateOptionsMenu(menu, inflater) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||||
|  |         when (item.itemId) { | ||||||
|  |             R.id.send -> { | ||||||
|  |                 send() | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             R.id.select_encoding -> { | ||||||
|  |                 val encodingDialog = SelectEncodingDialogFragment() | ||||||
|  |                 val args = Bundle() | ||||||
|  |                 args.putSerializable(EXTRA_ENCODING, encoding) | ||||||
|  |                 encodingDialog.arguments = args | ||||||
|  |                 encodingDialog.setTargetFragment(this, 0) | ||||||
|  |                 encodingDialog.show(fragmentManager, "select encoding dialog") | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             else -> return super.onOptionsItemSelected(item) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = if (requestCode == 0 && data != null && resultCode == RESULT_OK) { | ||||||
|  |         encoding = data.getSerializableExtra(EXTRA_ENCODING) as Plaintext.Encoding | ||||||
|  |     } else { | ||||||
|  |         super.onActivityResult(requestCode, resultCode, data) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun send() { | ||||||
|  |         val builder: Plaintext.Builder | ||||||
|  |         val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity") | ||||||
|  |         val bmc = Singleton.getBitmessageContext(ctx) | ||||||
|  |         if (broadcast) { | ||||||
|  |             builder = Plaintext.Builder(BROADCAST).from(identity) | ||||||
|  |         } else { | ||||||
|  |             val inputString = recipient_input.text.toString() | ||||||
|  |             if (recipient == null || recipient?.toString() != inputString) { | ||||||
|  |                 try { | ||||||
|  |                     recipient = BitmessageAddress(inputString) | ||||||
|  |                 } catch (e: Exception) { | ||||||
|  |                     val contacts = Singleton.getAddressRepository(ctx).getContacts() | ||||||
|  |                     for (contact in contacts) { | ||||||
|  |                         if (inputString.equals(contact.alias, ignoreCase = true)) { | ||||||
|  |                             recipient = contact | ||||||
|  |                             if (inputString == contact.alias) | ||||||
|  |                                 break | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |             } | ||||||
|  |             if (recipient == null) { | ||||||
|  |                 Toast.makeText(context, R.string.error_msg_recipient_missing, Toast.LENGTH_LONG).show() | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|  |             builder = Plaintext.Builder(MSG) | ||||||
|  |                     .from(identity) | ||||||
|  |                     .to(recipient) | ||||||
|  |         } | ||||||
|  |         if (!Preferences.requestAcknowledgements(ctx)) { | ||||||
|  |             builder.preventAck() | ||||||
|  |         } | ||||||
|  |         when (encoding) { | ||||||
|  |             Plaintext.Encoding.SIMPLE -> builder.message( | ||||||
|  |                     subject_input.text.toString(), | ||||||
|  |                     body_input.text.toString() | ||||||
|  |             ) | ||||||
|  |             Plaintext.Encoding.EXTENDED -> builder.message( | ||||||
|  |                     Message.Builder() | ||||||
|  |                             .subject(subject_input.text.toString()) | ||||||
|  |                             .body(body_input.text.toString()) | ||||||
|  |                             .addParent(parent) | ||||||
|  |                             .build() | ||||||
|  |             ) | ||||||
|  |             else -> { | ||||||
|  |                 Toast.makeText( | ||||||
|  |                         ctx, | ||||||
|  |                         ctx.getString(R.string.error_unsupported_encoding, encoding), | ||||||
|  |                         Toast.LENGTH_LONG | ||||||
|  |                 ).show() | ||||||
|  |                 builder.message( | ||||||
|  |                         subject_input.text.toString(), | ||||||
|  |                         body_input.text.toString() | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         bmc.send(builder.build()) | ||||||
|  |         ctx.finish() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -1,169 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.net.Uri; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.v7.app.AppCompatActivity; |  | ||||||
| import android.util.Base64; |  | ||||||
| import android.view.View; |  | ||||||
| import android.widget.Button; |  | ||||||
| import android.widget.EditText; |  | ||||||
| import android.widget.Switch; |  | ||||||
| import android.widget.TextView; |  | ||||||
|  |  | ||||||
| import java.io.ByteArrayInputStream; |  | ||||||
| import java.io.InputStream; |  | ||||||
| import java.util.regex.Matcher; |  | ||||||
| import java.util.regex.Pattern; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.Pubkey; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.V2Pubkey; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.V3Pubkey; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.V4Pubkey; |  | ||||||
|  |  | ||||||
| import static android.util.Base64.URL_SAFE; |  | ||||||
|  |  | ||||||
| public class CreateAddressActivity extends AppCompatActivity { |  | ||||||
|     private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$"); |  | ||||||
|     private byte[] pubkeyBytes; |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|         Uri uri = getIntent().getData(); |  | ||||||
|         if (uri != null) |  | ||||||
|             setContentView(R.layout.activity_open_bitmessage_link); |  | ||||||
|         else |  | ||||||
|             setContentView(R.layout.activity_create_bitmessage_address); |  | ||||||
|  |  | ||||||
|         final TextView address = (TextView) findViewById(R.id.address); |  | ||||||
|         final EditText label = (EditText) findViewById(R.id.label); |  | ||||||
|         final Switch subscribe = (Switch) findViewById(R.id.subscribe); |  | ||||||
|  |  | ||||||
|         if (uri != null) { |  | ||||||
|             String addressText = getAddress(uri); |  | ||||||
|             String[] parameters = getParameters(uri); |  | ||||||
|             for (String parameter : parameters) { |  | ||||||
|                 Matcher matcher = KEY_VALUE_PATTERN.matcher(parameter); |  | ||||||
|                 if (matcher.find()) { |  | ||||||
|                     String key = matcher.group(1).toLowerCase(); |  | ||||||
|                     String value = matcher.group(2); |  | ||||||
|                     switch (key) { |  | ||||||
|                         case "label": |  | ||||||
|                             label.setText(value.trim()); |  | ||||||
|                             break; |  | ||||||
|                         case "action": |  | ||||||
|                             subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe")); |  | ||||||
|                             break; |  | ||||||
|                         case "pubkey": |  | ||||||
|                             pubkeyBytes = Base64.decode(value, URL_SAFE); |  | ||||||
|                             break; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             address.setText(addressText); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         final Button cancel = (Button) findViewById(R.id.cancel); |  | ||||||
|         cancel.setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 setResult(Activity.RESULT_CANCELED); |  | ||||||
|                 finish(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         final Button ok = (Button) findViewById(R.id.do_import); |  | ||||||
|         ok.setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 String addressText = String.valueOf(address.getText()).trim(); |  | ||||||
|                 try { |  | ||||||
|                     BitmessageAddress bmAddress = new BitmessageAddress(addressText); |  | ||||||
|                     bmAddress.setAlias(label.getText().toString()); |  | ||||||
|  |  | ||||||
|                     BitmessageContext bmc = Singleton.getBitmessageContext |  | ||||||
|                         (CreateAddressActivity.this); |  | ||||||
|                     bmc.addContact(bmAddress); |  | ||||||
|                     if (subscribe.isChecked()) { |  | ||||||
|                         bmc.addSubscribtion(bmAddress); |  | ||||||
|                     } |  | ||||||
|                     if (pubkeyBytes != null) { |  | ||||||
|                         try { |  | ||||||
|                             final Pubkey pubkey; |  | ||||||
|                             InputStream pubkeyStream = new ByteArrayInputStream(pubkeyBytes); |  | ||||||
|                             long stream = bmAddress.getStream(); |  | ||||||
|                             switch ((int) bmAddress.getVersion()) { |  | ||||||
|                                 case 2: |  | ||||||
|                                     pubkey = V2Pubkey.read(pubkeyStream, stream); |  | ||||||
|                                     break; |  | ||||||
|                                 case 3: |  | ||||||
|                                     pubkey = V3Pubkey.read(pubkeyStream, stream); |  | ||||||
|                                     break; |  | ||||||
|                                 case 4: |  | ||||||
|                                     pubkey = new V4Pubkey(V3Pubkey.read(pubkeyStream, stream)); |  | ||||||
|                                     break; |  | ||||||
|                                 default: |  | ||||||
|                                     pubkey = null; |  | ||||||
|                             } |  | ||||||
|                             if (pubkey != null) { |  | ||||||
|                                 bmAddress.setPubkey(pubkey); |  | ||||||
|                             } |  | ||||||
|                         } catch (Exception ignore) { |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     setResult(Activity.RESULT_OK); |  | ||||||
|                     finish(); |  | ||||||
|                 } catch (RuntimeException e) { |  | ||||||
|                     address.setError(getString(R.string.error_illegal_address)); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private String getAddress(Uri uri) { |  | ||||||
|         StringBuilder result = new StringBuilder(); |  | ||||||
|         String schemeSpecificPart = uri.getSchemeSpecificPart(); |  | ||||||
|         if (!schemeSpecificPart.startsWith("BM-")) { |  | ||||||
|             result.append("BM-"); |  | ||||||
|         } |  | ||||||
|         if (schemeSpecificPart.contains("?")) { |  | ||||||
|             result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?'))); |  | ||||||
|         } else if (schemeSpecificPart.contains("#")) { |  | ||||||
|             result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#'))); |  | ||||||
|         } else { |  | ||||||
|             result.append(schemeSpecificPart); |  | ||||||
|         } |  | ||||||
|         return result.toString(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private String[] getParameters(Uri uri) { |  | ||||||
|         int index = uri.getSchemeSpecificPart().indexOf('?'); |  | ||||||
|         if (index >= 0) { |  | ||||||
|             String parameterPart = uri.getSchemeSpecificPart().substring(index + 1); |  | ||||||
|             return parameterPart.split("&"); |  | ||||||
|         } else { |  | ||||||
|             return new String[0]; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										146
									
								
								app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.app.Activity | ||||||
|  | import android.net.Uri | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v7.app.AppCompatActivity | ||||||
|  | import android.util.Base64 | ||||||
|  | import android.util.Base64.URL_SAFE | ||||||
|  | import android.widget.Button | ||||||
|  | import android.widget.EditText | ||||||
|  | import android.widget.Switch | ||||||
|  | import android.widget.TextView | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  | import ch.dissem.bitmessage.entity.payload.V2Pubkey | ||||||
|  | import ch.dissem.bitmessage.entity.payload.V3Pubkey | ||||||
|  | import ch.dissem.bitmessage.entity.payload.V4Pubkey | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import java.io.ByteArrayInputStream | ||||||
|  | import java.util.regex.Pattern | ||||||
|  |  | ||||||
|  | class CreateAddressActivity : AppCompatActivity() { | ||||||
|  |     private var pubkeyBytes: ByteArray? = null | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         val uri = intent.data | ||||||
|  |         if (uri != null) | ||||||
|  |             setContentView(R.layout.activity_open_bitmessage_link) | ||||||
|  |         else | ||||||
|  |             setContentView(R.layout.activity_create_bitmessage_address) | ||||||
|  |  | ||||||
|  |         val address = findViewById<TextView>(R.id.address) | ||||||
|  |         val label = findViewById<EditText>(R.id.label) | ||||||
|  |         val subscribe = findViewById<Switch>(R.id.subscribe) | ||||||
|  |  | ||||||
|  |         if (uri != null) { | ||||||
|  |             val addressText = getAddress(uri) | ||||||
|  |             val parameters = getParameters(uri) | ||||||
|  |             for (parameter in parameters) { | ||||||
|  |                 val matcher = KEY_VALUE_PATTERN.matcher(parameter) | ||||||
|  |                 if (matcher.find()) { | ||||||
|  |                     val key = matcher.group(1).toLowerCase() | ||||||
|  |                     val value = matcher.group(2) | ||||||
|  |                     when (key) { | ||||||
|  |                         "label" -> label.setText(value.trim { it <= ' ' }) | ||||||
|  |                         "action" -> subscribe.isChecked = value.trim { it <= ' ' }.equals("subscribe", ignoreCase = true) | ||||||
|  |                         "pubkey" -> pubkeyBytes = Base64.decode(value, URL_SAFE) | ||||||
|  |                         else -> LOG.debug("Unknown attribute: $key=$value") | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             address.text = addressText | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val cancel = findViewById<Button>(R.id.cancel) | ||||||
|  |         cancel.setOnClickListener { | ||||||
|  |             setResult(Activity.RESULT_CANCELED) | ||||||
|  |             finish() | ||||||
|  |         } | ||||||
|  |         findViewById<Button>(R.id.do_import).setOnClickListener { onOK(address, label, subscribe) } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private fun onOK(address: TextView, label: EditText, subscribe: Switch) { | ||||||
|  |         val addressText = address.text.toString().trim { it <= ' ' } | ||||||
|  |         try { | ||||||
|  |             val bmAddress = BitmessageAddress(addressText) | ||||||
|  |             bmAddress.alias = label.text.toString() | ||||||
|  |  | ||||||
|  |             val bmc = Singleton.getBitmessageContext(applicationContext) | ||||||
|  |             bmc.addContact(bmAddress) | ||||||
|  |             if (subscribe.isChecked) { | ||||||
|  |                 bmc.addSubscribtion(bmAddress) | ||||||
|  |             } | ||||||
|  |             pubkeyBytes?.let { pubkeyBytes -> | ||||||
|  |                 try { | ||||||
|  |                     val pubkeyStream = ByteArrayInputStream(pubkeyBytes) | ||||||
|  |                     val stream = bmAddress.stream | ||||||
|  |                     when (bmAddress.version.toInt()) { | ||||||
|  |                         2 -> V2Pubkey.read(pubkeyStream, stream) | ||||||
|  |                         3 -> V3Pubkey.read(pubkeyStream, stream) | ||||||
|  |                         4 -> V4Pubkey(V3Pubkey.read(pubkeyStream, stream)) | ||||||
|  |                         else -> null | ||||||
|  |                     }?.let { bmAddress.pubkey = it } | ||||||
|  |                 } catch (ignore: Exception) { | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             setResult(Activity.RESULT_OK) | ||||||
|  |             finish() | ||||||
|  |         } catch (e: RuntimeException) { | ||||||
|  |             address.error = getString(R.string.error_illegal_address) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun getAddress(uri: Uri): String { | ||||||
|  |         val result = StringBuilder() | ||||||
|  |         val schemeSpecificPart = uri.schemeSpecificPart | ||||||
|  |         if (!schemeSpecificPart.startsWith("BM-")) { | ||||||
|  |             result.append("BM-") | ||||||
|  |         } | ||||||
|  |         when { | ||||||
|  |             schemeSpecificPart.contains("?") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('?'))) | ||||||
|  |             schemeSpecificPart.contains("#") -> result.append(schemeSpecificPart.substring(0, schemeSpecificPart.indexOf('#'))) | ||||||
|  |             else -> result.append(schemeSpecificPart) | ||||||
|  |         } | ||||||
|  |         return result.toString() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun getParameters(uri: Uri): Array<String> { | ||||||
|  |         val index = uri.schemeSpecificPart.indexOf('?') | ||||||
|  |         return if (index >= 0) { | ||||||
|  |             uri.schemeSpecificPart | ||||||
|  |                     .substring(index + 1) | ||||||
|  |                     .split("&".toRegex()) | ||||||
|  |                     .dropLastWhile { it.isEmpty() } | ||||||
|  |                     .toTypedArray() | ||||||
|  |         } else { | ||||||
|  |             emptyArray() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val LOG = LoggerFactory.getLogger(CreateAddressActivity::class.java) | ||||||
|  |  | ||||||
|  |         private val KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$") | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,53 +0,0 @@ | |||||||
| package ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.v4.app.NavUtils; |  | ||||||
| import android.support.v7.app.AppCompatActivity; |  | ||||||
| import android.support.v7.widget.Toolbar; |  | ||||||
| import android.view.MenuItem; |  | ||||||
|  |  | ||||||
| import com.mikepenz.materialize.MaterializeBuilder; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public abstract class DetailActivity extends AppCompatActivity { |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|         setContentView(R.layout.scrolling_toolbar_layout); |  | ||||||
|  |  | ||||||
|         final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); |  | ||||||
|         setSupportActionBar(toolbar); |  | ||||||
|         // Show the Up button in the action bar. |  | ||||||
|         //noinspection ConstantConditions |  | ||||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); |  | ||||||
|  |  | ||||||
|         new MaterializeBuilder() |  | ||||||
|             .withActivity(this) |  | ||||||
|             .withStatusBarColorRes(R.color.colorPrimaryDark) |  | ||||||
|             .withTranslucentStatusBarProgrammatically(true) |  | ||||||
|             .withStatusBarPadding(true) |  | ||||||
|             .build(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public boolean onOptionsItemSelected(MenuItem item) { |  | ||||||
|         switch (item.getItemId()) { |  | ||||||
|             case android.R.id.home: |  | ||||||
|                 // This ID represents the Home or Up button. In the case of this |  | ||||||
|                 // activity, the Up button is shown. Use NavUtils to allow users |  | ||||||
|                 // to navigate up one level in the application structure. For |  | ||||||
|                 // more details, see the Navigation pattern on Android Design: |  | ||||||
|                 // |  | ||||||
|                 // http://developer.android.com/design/patterns/navigation.html#up-vs-back |  | ||||||
|                 // |  | ||||||
|                 NavUtils.navigateUpTo(this, new Intent(this, MainActivity.class)); |  | ||||||
|                 return true; |  | ||||||
|             default: |  | ||||||
|                 return super.onOptionsItemSelected(item); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										48
									
								
								app/src/main/java/ch/dissem/apps/abit/DetailActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/src/main/java/ch/dissem/apps/abit/DetailActivity.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | package ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v4.app.NavUtils | ||||||
|  | import android.support.v7.app.AppCompatActivity | ||||||
|  | import android.view.MenuItem | ||||||
|  | import com.mikepenz.materialize.MaterializeBuilder | ||||||
|  | import kotlinx.android.synthetic.main.scrolling_toolbar_layout.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | abstract class DetailActivity : AppCompatActivity() { | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         setContentView(R.layout.scrolling_toolbar_layout) | ||||||
|  |  | ||||||
|  |         setSupportActionBar(toolbar) | ||||||
|  |         // Show the Up button in the action bar. | ||||||
|  |         supportActionBar?.apply { | ||||||
|  |             setDisplayHomeAsUpEnabled(true) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         MaterializeBuilder() | ||||||
|  |                 .withActivity(this) | ||||||
|  |                 .withStatusBarColorRes(R.color.colorPrimaryDark) | ||||||
|  |                 .withTranslucentStatusBarProgrammatically(true) | ||||||
|  |                 .withStatusBarPadding(true) | ||||||
|  |                 .build() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { | ||||||
|  |         android.R.id.home -> { | ||||||
|  |             // This ID represents the Home or Up button. In the case of this | ||||||
|  |             // activity, the Up button is shown. Use NavUtils to allow users | ||||||
|  |             // to navigate up one level in the application structure. For | ||||||
|  |             // more details, see the Navigation pattern on Android Design: | ||||||
|  |             // | ||||||
|  |             // http://developer.android.com/design/patterns/navigation.html#up-vs-back | ||||||
|  |             // | ||||||
|  |             NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java)) | ||||||
|  |             true | ||||||
|  |         } | ||||||
|  |         else -> super.onOptionsItemSelected(item) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,124 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.graphics.*; |  | ||||||
| import android.graphics.drawable.Drawable; |  | ||||||
| import android.support.annotation.NonNull; |  | ||||||
| import android.text.TextPaint; |  | ||||||
|  |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public class Identicon extends Drawable { |  | ||||||
|     private static final int SIZE = 9; |  | ||||||
|     private static final int CENTER_COLUMN = 5; |  | ||||||
|  |  | ||||||
|     private final Paint paint; |  | ||||||
|     private final int color; |  | ||||||
|     private final int background; |  | ||||||
|     private final boolean[][] fields; |  | ||||||
|     private final boolean chan; |  | ||||||
|     private final TextPaint textPaint; |  | ||||||
|  |  | ||||||
|     public Identicon(@NonNull BitmessageAddress input) { |  | ||||||
|         paint = new Paint(); |  | ||||||
|         paint.setStyle(Paint.Style.FILL); |  | ||||||
|         paint.setAntiAlias(true); |  | ||||||
|         textPaint = new TextPaint(); |  | ||||||
|         textPaint.setTextAlign(Paint.Align.CENTER); |  | ||||||
|         textPaint.setColor(0xFF607D8B); |  | ||||||
|         textPaint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)); |  | ||||||
|  |  | ||||||
|         chan = input.isChan(); |  | ||||||
|  |  | ||||||
|         byte[] hash = input.getRipe(); |  | ||||||
|  |  | ||||||
|         fields = new boolean[SIZE][SIZE]; |  | ||||||
|         color = Color.HSVToColor(new float[]{ |  | ||||||
|             Math.abs(hash[0] * hash[1] + hash[2]) % 360, |  | ||||||
|             0.8f, |  | ||||||
|             1.0f |  | ||||||
|         }); |  | ||||||
|         background = Color.HSVToColor(new float[]{ |  | ||||||
|             Math.abs(hash[1] * hash[2] + hash[0]) % 360, |  | ||||||
|             0.8f, |  | ||||||
|             1.0f |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         for (int row = 0; row < SIZE; row++) { |  | ||||||
|             if (!chan || row < 5 || row > 6) { |  | ||||||
|                 for (int column = 0; column <= CENTER_COLUMN; column++) { |  | ||||||
|                     if ( |  | ||||||
|                         (row - SIZE / 2) * (row - SIZE / 2) |  | ||||||
|                             + (column - SIZE / 2) * (column - SIZE / 2) |  | ||||||
|                             < SIZE / 2 * SIZE / 2 |  | ||||||
|                         ) { |  | ||||||
|                         fields[row][column] = hash[(row * CENTER_COLUMN + column) % hash.length] |  | ||||||
|                             >= 0; |  | ||||||
|                         fields[row][SIZE - column - 1] = fields[row][column]; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void draw(@NonNull Canvas canvas) { |  | ||||||
|         float x, y; |  | ||||||
|         float width = canvas.getWidth(); |  | ||||||
|         float height = canvas.getHeight(); |  | ||||||
|         float cellWidth = width / (float) SIZE; |  | ||||||
|         float cellHeight = height / (float) SIZE; |  | ||||||
|         paint.setColor(background); |  | ||||||
|         canvas.drawCircle(width / 2, height / 2, width / 2, paint); |  | ||||||
|         paint.setColor(color); |  | ||||||
|         for (int row = 0; row < SIZE; row++) { |  | ||||||
|             for (int column = 0; column < SIZE; column++) { |  | ||||||
|                 if (fields[row][column]) { |  | ||||||
|                     x = cellWidth * column; |  | ||||||
|                     y = cellHeight * row; |  | ||||||
|                     canvas.drawCircle( |  | ||||||
|                         x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2, |  | ||||||
|                         paint |  | ||||||
|                     ); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if (chan) { |  | ||||||
|             textPaint.setTextSize(2 * cellHeight); |  | ||||||
|             canvas.drawText("[chan]", width / 2, 6.7f * cellHeight, textPaint); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void setAlpha(int alpha) { |  | ||||||
|         paint.setAlpha(alpha); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void setColorFilter(ColorFilter cf) { |  | ||||||
|         paint.setColorFilter(cf); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public int getOpacity() { |  | ||||||
|         return PixelFormat.TRANSPARENT; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										98
									
								
								app/src/main/java/ch/dissem/apps/abit/Identicon.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								app/src/main/java/ch/dissem/apps/abit/Identicon.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.graphics.* | ||||||
|  | import android.graphics.drawable.Drawable | ||||||
|  | import android.text.TextPaint | ||||||
|  |  | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class Identicon(input: BitmessageAddress) : Drawable() { | ||||||
|  |  | ||||||
|  |     private val paint = Paint().apply { | ||||||
|  |         style = Paint.Style.FILL | ||||||
|  |         isAntiAlias = true | ||||||
|  |     } | ||||||
|  |     private val hash = input.ripe | ||||||
|  |     private val isChan = input.isChan | ||||||
|  |     private val fields = Array(SIZE) { BooleanArray(SIZE) }.apply { | ||||||
|  |         for (row in 0 until SIZE) { | ||||||
|  |             if (!isChan || row < 5 || row > 6) { | ||||||
|  |                 for (column in 0..CENTER_COLUMN) { | ||||||
|  |                     if ((row - SIZE / 2) * (row - SIZE / 2) + (column - SIZE / 2) * (column - SIZE / 2) < SIZE / 2 * SIZE / 2) { | ||||||
|  |                         this[row][column] = hash[(row * CENTER_COLUMN + column) % hash.size] >= 0 | ||||||
|  |                         this[row][SIZE - column - 1] = this[row][column] | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     private val color = Color.HSVToColor(floatArrayOf((Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(), 0.8f, 1.0f)) | ||||||
|  |     private val background = Color.HSVToColor(floatArrayOf((Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(), 0.8f, 1.0f)) | ||||||
|  |     private val textPaint = TextPaint().apply { | ||||||
|  |         textAlign = Paint.Align.CENTER | ||||||
|  |         color = 0xFF607D8B.toInt() | ||||||
|  |         typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun draw(canvas: Canvas) { | ||||||
|  |         var x: Float | ||||||
|  |         var y: Float | ||||||
|  |         val width = canvas.width.toFloat() | ||||||
|  |         val height = canvas.height.toFloat() | ||||||
|  |         val cellWidth = width / SIZE.toFloat() | ||||||
|  |         val cellHeight = height / SIZE.toFloat() | ||||||
|  |         paint.color = background | ||||||
|  |         canvas.drawCircle(width / 2, height / 2, width / 2, paint) | ||||||
|  |         paint.color = color | ||||||
|  |         for (row in 0 until SIZE) { | ||||||
|  |             for (column in 0 until SIZE) { | ||||||
|  |                 if (fields[row][column]) { | ||||||
|  |                     x = cellWidth * column | ||||||
|  |                     y = cellHeight * row | ||||||
|  |                     canvas.drawCircle( | ||||||
|  |                             x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2, | ||||||
|  |                             paint | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (isChan) { | ||||||
|  |             textPaint.textSize = 2 * cellHeight | ||||||
|  |             canvas.drawText("[isChan]", width / 2, 6.7f * cellHeight, textPaint) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun setAlpha(alpha: Int) { | ||||||
|  |         paint.alpha = alpha | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun setColorFilter(cf: ColorFilter?) { | ||||||
|  |         paint.colorFilter = cf | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getOpacity() = PixelFormat.TRANSPARENT | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val SIZE = 9 | ||||||
|  |         private val CENTER_COLUMN = 5 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,85 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.app.Fragment; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.annotation.Nullable; |  | ||||||
| import android.support.v4.content.ContextCompat; |  | ||||||
| import android.support.v7.widget.LinearLayoutManager; |  | ||||||
| import android.support.v7.widget.RecyclerView; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
|  |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator; |  | ||||||
|  |  | ||||||
| import java.io.IOException; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.adapter.AddressSelectorAdapter; |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.wif.WifImporter; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| public class ImportIdentitiesFragment extends Fragment { |  | ||||||
|     public static final String WIF_DATA = "wif_data"; |  | ||||||
|     private AddressSelectorAdapter adapter; |  | ||||||
|     private WifImporter importer; |  | ||||||
|  |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle |  | ||||||
|         savedInstanceState) { |  | ||||||
|         String wifData = getArguments().getString(WIF_DATA); |  | ||||||
|         BitmessageContext bmc = Singleton.getBitmessageContext(getActivity()); |  | ||||||
|         View view = inflater.inflate(R.layout.fragment_import_select_identities, container, false); |  | ||||||
|         try { |  | ||||||
|             importer = new WifImporter(bmc, wifData); |  | ||||||
|             adapter = new AddressSelectorAdapter(importer.getIdentities()); |  | ||||||
|             LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), |  | ||||||
|                 LinearLayoutManager.VERTICAL, |  | ||||||
|                 false); |  | ||||||
|             RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); |  | ||||||
|             recyclerView.setLayoutManager(layoutManager); |  | ||||||
|             recyclerView.setAdapter(adapter); |  | ||||||
|  |  | ||||||
|             recyclerView.addItemDecoration(new SimpleListDividerDecorator( |  | ||||||
|                 ContextCompat.getDrawable(getActivity(), R.drawable.list_divider_h), true)); |  | ||||||
|         } catch (IOException e) { |  | ||||||
|             return super.onCreateView(inflater, container, savedInstanceState); |  | ||||||
|         } |  | ||||||
|         view.findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 importer.importAll(adapter.getSelected()); |  | ||||||
|                 MainActivity mainActivity = MainActivity.getInstance(); |  | ||||||
|                 if (mainActivity != null) { |  | ||||||
|                     for (BitmessageAddress selected : adapter.getSelected()) { |  | ||||||
|                         mainActivity.addIdentityEntry(selected); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 getActivity().finish(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         return view; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,77 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.app.Fragment | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v4.content.ContextCompat | ||||||
|  | import android.support.v7.widget.LinearLayoutManager | ||||||
|  | import android.support.v7.widget.RecyclerView | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.Button | ||||||
|  |  | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator | ||||||
|  |  | ||||||
|  | import ch.dissem.apps.abit.adapter.AddressSelectorAdapter | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.bitmessage.wif.WifImporter | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class ImportIdentitiesFragment : Fragment() { | ||||||
|  |     private lateinit var adapter: AddressSelectorAdapter | ||||||
|  |     private lateinit var importer: WifImporter | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = | ||||||
|  |             inflater.inflate(R.layout.fragment_import_select_identities, container, false) | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|  |         val wifData = arguments.getString(WIF_DATA) | ||||||
|  |         val bmc = Singleton.getBitmessageContext(activity) | ||||||
|  |  | ||||||
|  |         importer = WifImporter(bmc, wifData) | ||||||
|  |         adapter = AddressSelectorAdapter(importer.getIdentities()) | ||||||
|  |         val layoutManager = LinearLayoutManager(activity, | ||||||
|  |                 LinearLayoutManager.VERTICAL, | ||||||
|  |                 false) | ||||||
|  |         val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view) | ||||||
|  |         recyclerView.layoutManager = layoutManager | ||||||
|  |         recyclerView.adapter = adapter | ||||||
|  |  | ||||||
|  |         recyclerView.addItemDecoration(SimpleListDividerDecorator( | ||||||
|  |                 ContextCompat.getDrawable(activity, R.drawable.list_divider_h), true)) | ||||||
|  |  | ||||||
|  |         view.findViewById<Button>(R.id.finish).setOnClickListener { | ||||||
|  |             importer.importAll(adapter.selected) | ||||||
|  |             MainActivity.apply { | ||||||
|  |                 for (selected in adapter.selected) { | ||||||
|  |                     addIdentityEntry(selected) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             activity.finish() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val WIF_DATA = "wif_data" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,56 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.os.Bundle; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.ImportIdentitiesFragment.WIF_DATA; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| public class ImportIdentityActivity extends DetailActivity { |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|  |  | ||||||
|         String wifData; |  | ||||||
|         if (savedInstanceState == null) { |  | ||||||
|             wifData = null; |  | ||||||
|         } else { |  | ||||||
|             wifData = savedInstanceState.getString(WIF_DATA); |  | ||||||
|         } |  | ||||||
|         if (wifData == null) { |  | ||||||
|             getFragmentManager().beginTransaction() |  | ||||||
|                 .replace(R.id.content, new InputWifFragment()) |  | ||||||
|                 .commit(); |  | ||||||
|         } else { |  | ||||||
|             Bundle bundle = new Bundle(); |  | ||||||
|             bundle.putString(WIF_DATA, wifData); |  | ||||||
|  |  | ||||||
|             ImportIdentitiesFragment fragment = new ImportIdentitiesFragment(); |  | ||||||
|             fragment.setArguments(bundle); |  | ||||||
|  |  | ||||||
|             getFragmentManager().beginTransaction() |  | ||||||
|                 .replace(R.id.content, fragment) |  | ||||||
|                 .commit(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,48 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class ImportIdentityActivity : DetailActivity() { | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |  | ||||||
|  |         val wifData: String? = savedInstanceState?.getString(ImportIdentitiesFragment.WIF_DATA) | ||||||
|  |  | ||||||
|  |         if (wifData == null) { | ||||||
|  |             fragmentManager.beginTransaction() | ||||||
|  |                     .replace(R.id.content, InputWifFragment()) | ||||||
|  |                     .commit() | ||||||
|  |         } else { | ||||||
|  |             val bundle = Bundle() | ||||||
|  |             bundle.putString(ImportIdentitiesFragment.WIF_DATA, wifData) | ||||||
|  |  | ||||||
|  |             val fragment = ImportIdentitiesFragment() | ||||||
|  |             fragment.arguments = bundle | ||||||
|  |  | ||||||
|  |             fragmentManager.beginTransaction() | ||||||
|  |                     .replace(R.id.content, fragment) | ||||||
|  |                     .commit() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,122 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.app.Fragment; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.annotation.Nullable; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.Menu; |  | ||||||
| import android.view.MenuInflater; |  | ||||||
| import android.view.MenuItem; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.TextView; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import com.github.angads25.filepicker.controller.DialogSelectionListener; |  | ||||||
| import com.github.angads25.filepicker.model.DialogConfigs; |  | ||||||
| import com.github.angads25.filepicker.model.DialogProperties; |  | ||||||
| import com.github.angads25.filepicker.view.FilePickerDialog; |  | ||||||
|  |  | ||||||
| import java.io.ByteArrayOutputStream; |  | ||||||
| import java.io.File; |  | ||||||
| import java.io.FileInputStream; |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.io.InputStream; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.ImportIdentitiesFragment.WIF_DATA; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| public class InputWifFragment extends Fragment { |  | ||||||
|     private TextView wifData; |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|         setHasOptionsMenu(true); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, |  | ||||||
|                              Bundle savedInstanceState) { |  | ||||||
|         View view = inflater.inflate(R.layout.fragment_import_input, container, false); |  | ||||||
|         wifData = (TextView) view.findViewById(R.id.wif_input); |  | ||||||
|  |  | ||||||
|         view.findViewById(R.id.next).setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 Bundle bundle = new Bundle(); |  | ||||||
|                 bundle.putString(WIF_DATA, wifData.getText().toString()); |  | ||||||
|  |  | ||||||
|                 ImportIdentitiesFragment fragment = new ImportIdentitiesFragment(); |  | ||||||
|                 fragment.setArguments(bundle); |  | ||||||
|  |  | ||||||
|                 getFragmentManager().beginTransaction() |  | ||||||
|                     .replace(R.id.content, fragment) |  | ||||||
|                     .commit(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         return view; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |  | ||||||
|         inflater.inflate(R.menu.import_input_data, menu); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public boolean onOptionsItemSelected(MenuItem item) { |  | ||||||
|         DialogProperties properties = new DialogProperties(); |  | ||||||
|         properties.selection_mode = DialogConfigs.SINGLE_MODE; |  | ||||||
|         properties.selection_type = DialogConfigs.FILE_SELECT; |  | ||||||
|         properties.root = new File(DialogConfigs.DEFAULT_DIR); |  | ||||||
|         properties.error_dir = new File(DialogConfigs.DEFAULT_DIR); |  | ||||||
|         properties.extensions = null; |  | ||||||
|         FilePickerDialog dialog = new FilePickerDialog(getActivity(), properties); |  | ||||||
|         dialog.setTitle(getString(R.string.select_file_title)); |  | ||||||
|         dialog.setDialogSelectionListener(new DialogSelectionListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onSelectedFilePaths(String[] files) { |  | ||||||
|                 if (files.length > 0) { |  | ||||||
|                     try (InputStream in = new FileInputStream(files[0])) { |  | ||||||
|                         ByteArrayOutputStream data = new ByteArrayOutputStream(); |  | ||||||
|                         byte[] buffer = new byte[1024]; |  | ||||||
|                         int length; |  | ||||||
|                         //noinspection ConstantConditions |  | ||||||
|                         while ((length = in.read(buffer)) != -1) { |  | ||||||
|                             data.write(buffer, 0, length); |  | ||||||
|                         } |  | ||||||
|                         wifData.setText(data.toString("UTF-8")); |  | ||||||
|                     } catch (IOException e) { |  | ||||||
|                         Toast.makeText( |  | ||||||
|                             getActivity(), |  | ||||||
|                             R.string.error_loading_data, |  | ||||||
|                             Toast.LENGTH_SHORT |  | ||||||
|                         ).show(); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         dialog.show(); |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										101
									
								
								app/src/main/java/ch/dissem/apps/abit/InputWifFragment.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								app/src/main/java/ch/dissem/apps/abit/InputWifFragment.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.app.Fragment | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.* | ||||||
|  | import android.widget.Toast | ||||||
|  | import com.github.angads25.filepicker.model.DialogConfigs | ||||||
|  | import com.github.angads25.filepicker.model.DialogProperties | ||||||
|  | import com.github.angads25.filepicker.view.FilePickerDialog | ||||||
|  | import kotlinx.android.synthetic.main.fragment_import_input.* | ||||||
|  | import java.io.ByteArrayOutputStream | ||||||
|  | import java.io.File | ||||||
|  | import java.io.FileInputStream | ||||||
|  | import java.io.IOException | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class InputWifFragment : Fragment() { | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         setHasOptionsMenu(true) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = | ||||||
|  |             inflater.inflate(R.layout.fragment_import_input, container, false) | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |         next.setOnClickListener { | ||||||
|  |             val bundle = Bundle() | ||||||
|  |             bundle.putString(ImportIdentitiesFragment.WIF_DATA, wif_input.text.toString()) | ||||||
|  |  | ||||||
|  |             val fragment = ImportIdentitiesFragment().apply { | ||||||
|  |                 arguments = bundle | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             fragmentManager.beginTransaction() | ||||||
|  |                     .replace(R.id.content, fragment) | ||||||
|  |                     .commit() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) = | ||||||
|  |         inflater.inflate(R.menu.import_input_data, menu) | ||||||
|  |  | ||||||
|  |     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||||
|  |         val properties = DialogProperties() | ||||||
|  |         properties.selection_mode = DialogConfigs.SINGLE_MODE | ||||||
|  |         properties.selection_type = DialogConfigs.FILE_SELECT | ||||||
|  |         properties.root = File(DialogConfigs.DEFAULT_DIR) | ||||||
|  |         properties.error_dir = File(DialogConfigs.DEFAULT_DIR) | ||||||
|  |         properties.extensions = null | ||||||
|  |         val dialog = FilePickerDialog(activity, properties) | ||||||
|  |         dialog.setTitle(getString(R.string.select_file_title)) | ||||||
|  |         dialog.setDialogSelectionListener { files -> | ||||||
|  |             if (files.isNotEmpty()) { | ||||||
|  |                 try { | ||||||
|  |                     FileInputStream(files[0]).use { inputStream -> | ||||||
|  |                         val data = ByteArrayOutputStream() | ||||||
|  |                         val buffer = ByteArray(1024) | ||||||
|  |  | ||||||
|  |                         var length: Int = inputStream.read(buffer) | ||||||
|  |  | ||||||
|  |                         while (length != -1) { | ||||||
|  |                             data.write(buffer, 0, length) | ||||||
|  |                             length = inputStream.read(buffer) | ||||||
|  |                         } | ||||||
|  |                         wif_input.setText(data.toByteArray().toString()) | ||||||
|  |                     } | ||||||
|  |                 } catch (e: IOException) { | ||||||
|  |                     Toast.makeText( | ||||||
|  |                             activity, | ||||||
|  |                             R.string.error_loading_data, | ||||||
|  |                             Toast.LENGTH_SHORT | ||||||
|  |                     ).show() | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         dialog.show() | ||||||
|  |         return true | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -14,15 +14,15 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package ch.dissem.apps.abit; | package ch.dissem.apps.abit | ||||||
| 
 |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.Label; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @author Christian Basler |  * @author Christian Basler | ||||||
|  */ |  */ | ||||||
| public interface ListHolder { | interface ListHolder<L> { | ||||||
|     void updateList(Label label); |     fun updateList(label: L) | ||||||
| 
 | 
 | ||||||
|     void setActivateOnItemClick(boolean activateOnItemClick); |     fun setActivateOnItemClick(activateOnItemClick: Boolean) | ||||||
|  | 
 | ||||||
|  |     fun showPreviousList(): Boolean | ||||||
| } | } | ||||||
| @@ -1,622 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.app.Dialog; |  | ||||||
| import android.content.DialogInterface; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.graphics.Point; |  | ||||||
| import android.graphics.drawable.ColorDrawable; |  | ||||||
| import android.os.AsyncTask; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.v4.app.Fragment; |  | ||||||
| import android.support.v7.app.AppCompatActivity; |  | ||||||
| import android.support.v7.widget.Toolbar; |  | ||||||
| import android.view.Display; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.view.Window; |  | ||||||
| import android.view.WindowManager; |  | ||||||
| import android.widget.CompoundButton; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.RelativeLayout; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import com.github.amlcurran.showcaseview.ShowcaseView; |  | ||||||
| import com.github.amlcurran.showcaseview.targets.Target; |  | ||||||
| import com.mikepenz.community_material_typeface_library.CommunityMaterial; |  | ||||||
| import com.mikepenz.google_material_typeface_library.GoogleMaterial; |  | ||||||
| import com.mikepenz.iconics.IconicsDrawable; |  | ||||||
| import com.mikepenz.materialdrawer.AccountHeader; |  | ||||||
| import com.mikepenz.materialdrawer.AccountHeaderBuilder; |  | ||||||
| import com.mikepenz.materialdrawer.Drawer; |  | ||||||
| import com.mikepenz.materialdrawer.DrawerBuilder; |  | ||||||
| import com.mikepenz.materialdrawer.interfaces.OnCheckedChangeListener; |  | ||||||
| import com.mikepenz.materialdrawer.model.DividerDrawerItem; |  | ||||||
| import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; |  | ||||||
| import com.mikepenz.materialdrawer.model.ProfileDrawerItem; |  | ||||||
| import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem; |  | ||||||
| import com.mikepenz.materialdrawer.model.SwitchDrawerItem; |  | ||||||
| import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; |  | ||||||
| import com.mikepenz.materialdrawer.model.interfaces.IProfile; |  | ||||||
| import com.mikepenz.materialdrawer.model.interfaces.Nameable; |  | ||||||
|  |  | ||||||
| import java.io.Serializable; |  | ||||||
| import java.lang.ref.WeakReference; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment; |  | ||||||
| import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; |  | ||||||
| import ch.dissem.apps.abit.listener.ActionBarListener; |  | ||||||
| import ch.dissem.apps.abit.listener.ListSelectionListener; |  | ||||||
| import ch.dissem.apps.abit.repository.AndroidMessageRepository; |  | ||||||
| import ch.dissem.apps.abit.service.BitmessageService; |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.apps.abit.synchronization.SyncAdapter; |  | ||||||
| import ch.dissem.apps.abit.util.Drawables; |  | ||||||
| import ch.dissem.apps.abit.util.Labels; |  | ||||||
| import ch.dissem.apps.abit.util.Preferences; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.Label; |  | ||||||
|  |  | ||||||
| import static android.widget.Toast.LENGTH_LONG; |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.launchReplyTo; |  | ||||||
| import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE; |  | ||||||
| import static ch.dissem.apps.abit.service.BitmessageService.isRunning; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * An activity representing a list of Messages. This activity |  | ||||||
|  * has different presentations for handset and tablet-size devices. On |  | ||||||
|  * handsets, the activity presents a list of items, which when touched, |  | ||||||
|  * lead to a {@link MessageDetailActivity} representing |  | ||||||
|  * item details. On tablets, the activity presents the list of items and |  | ||||||
|  * item details side-by-side using two vertical panes. |  | ||||||
|  * <p> |  | ||||||
|  * The activity makes heavy use of fragments. The list of items is a |  | ||||||
|  * {@link MessageListFragment} and the item details |  | ||||||
|  * (if present) is a {@link MessageDetailFragment}. |  | ||||||
|  * </p><p> |  | ||||||
|  * This activity also implements the required |  | ||||||
|  * {@link ListSelectionListener} interface |  | ||||||
|  * to listen for item selections. |  | ||||||
|  * </p> |  | ||||||
|  */ |  | ||||||
| public class MainActivity extends AppCompatActivity |  | ||||||
|     implements ListSelectionListener<Serializable>, ActionBarListener { |  | ||||||
|     public static final String EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"; |  | ||||||
|     public static final String EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"; |  | ||||||
|     public static final String EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"; |  | ||||||
|     public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; |  | ||||||
|  |  | ||||||
|     private static final int ADD_IDENTITY = 1; |  | ||||||
|     private static final int MANAGE_IDENTITY = 2; |  | ||||||
|  |  | ||||||
|     private static final long ID_NODE_SWITCH = 1; |  | ||||||
|  |  | ||||||
|     private static WeakReference<MainActivity> instance; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Whether or not the activity is in two-pane mode, i.e. running on a tablet |  | ||||||
|      * device. |  | ||||||
|      */ |  | ||||||
|     private boolean twoPane; |  | ||||||
|  |  | ||||||
|     private Label selectedLabel; |  | ||||||
|  |  | ||||||
|     private BitmessageContext bmc; |  | ||||||
|     private AccountHeader accountHeader; |  | ||||||
|  |  | ||||||
|     private Drawer drawer; |  | ||||||
|     private SwitchDrawerItem nodeSwitch; |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|         instance = new WeakReference<>(this); |  | ||||||
|         bmc = Singleton.getBitmessageContext(this); |  | ||||||
|  |  | ||||||
|         setContentView(R.layout.activity_message_list); |  | ||||||
|  |  | ||||||
|         final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); |  | ||||||
|         setSupportActionBar(toolbar); |  | ||||||
|  |  | ||||||
|         MessageListFragment listFragment = new MessageListFragment(); |  | ||||||
|         getSupportFragmentManager() |  | ||||||
|             .beginTransaction() |  | ||||||
|             .replace(R.id.item_list, listFragment) |  | ||||||
|             .commit(); |  | ||||||
|  |  | ||||||
|         if (findViewById(R.id.message_detail_container) != null) { |  | ||||||
|             // The detail container view will be present only in the |  | ||||||
|             // large-screen layouts (res/values-large and |  | ||||||
|             // res/values-sw600dp). If this view is present, then the |  | ||||||
|             // activity should be in two-pane mode. |  | ||||||
|             twoPane = true; |  | ||||||
|  |  | ||||||
|             // In two-pane mode, list items should be given the |  | ||||||
|             // 'activated' state when touched. |  | ||||||
|             listFragment.setActivateOnItemClick(true); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         createDrawer(toolbar); |  | ||||||
|  |  | ||||||
|         // handle intents |  | ||||||
|         Intent intent = getIntent(); |  | ||||||
|         if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) { |  | ||||||
|             onItemSelected(intent.getSerializableExtra(EXTRA_SHOW_MESSAGE)); |  | ||||||
|         } |  | ||||||
|         if (intent.hasExtra(EXTRA_REPLY_TO_MESSAGE)) { |  | ||||||
|             Plaintext item = (Plaintext) intent.getSerializableExtra(EXTRA_REPLY_TO_MESSAGE); |  | ||||||
|             launchReplyTo(this, item); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (Preferences.useTrustedNode(this)) { |  | ||||||
|             SyncAdapter.startSync(this); |  | ||||||
|         } else { |  | ||||||
|             SyncAdapter.stopSync(this); |  | ||||||
|         } |  | ||||||
|         if (drawer.isDrawerOpen()) { |  | ||||||
|             RelativeLayout.LayoutParams lps = new RelativeLayout.LayoutParams(ViewGroup |  | ||||||
|                 .LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); |  | ||||||
|             lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); |  | ||||||
|             lps.addRule(RelativeLayout.ALIGN_PARENT_LEFT); |  | ||||||
|             int margin = ((Number) (getResources().getDisplayMetrics().density * 12)).intValue(); |  | ||||||
|             lps.setMargins(margin, margin, margin, margin); |  | ||||||
|  |  | ||||||
|             new ShowcaseView.Builder(this) |  | ||||||
|                 .withMaterialShowcase() |  | ||||||
|                 .setStyle(R.style.CustomShowcaseTheme) |  | ||||||
|                 .setContentTitle(R.string.full_node) |  | ||||||
|                 .setContentText(R.string.full_node_description) |  | ||||||
|                 .setTarget(new Target() { |  | ||||||
|                     @Override |  | ||||||
|                     public Point getPoint() { |  | ||||||
|                         View view = drawer.getStickyFooter(); |  | ||||||
|                         int[] location = new int[2]; |  | ||||||
|                         view.getLocationInWindow(location); |  | ||||||
|                         int x = location[0] + 7 * view.getWidth() / 8; |  | ||||||
|                         int y = location[1] + view.getHeight() / 2; |  | ||||||
|                         return new Point(x, y); |  | ||||||
|                     } |  | ||||||
|                 }) |  | ||||||
|                 .replaceEndButton(R.layout.showcase_button) |  | ||||||
|                 .hideOnTouchOutside() |  | ||||||
|                 .build() |  | ||||||
|                 .setButtonPosition(lps); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private <F extends Fragment & ListHolder> void changeList(F listFragment) { |  | ||||||
|         getSupportFragmentManager() |  | ||||||
|             .beginTransaction() |  | ||||||
|             .replace(R.id.item_list, listFragment) |  | ||||||
|             .addToBackStack(null) |  | ||||||
|             .commit(); |  | ||||||
|  |  | ||||||
|         if (twoPane) { |  | ||||||
|             // In two-pane mode, list items should be given the |  | ||||||
|             // 'activated' state when touched. |  | ||||||
|             listFragment.setActivateOnItemClick(true); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void createDrawer(Toolbar toolbar) { |  | ||||||
|         final ArrayList<IProfile> profiles = new ArrayList<>(); |  | ||||||
|         profiles.add(new ProfileSettingDrawerItem() |  | ||||||
|             .withName(getString(R.string.add_identity)) |  | ||||||
|             .withDescription(getString(R.string.add_identity_summary)) |  | ||||||
|             .withIcon(new IconicsDrawable(this, GoogleMaterial.Icon.gmd_add) |  | ||||||
|                 .actionBar() |  | ||||||
|                 .paddingDp(5) |  | ||||||
|                 .colorRes(R.color.icons)) |  | ||||||
|             .withIdentifier(ADD_IDENTITY) |  | ||||||
|         ); |  | ||||||
|         profiles.add(new ProfileSettingDrawerItem() |  | ||||||
|             .withName(getString(R.string.manage_identity)) |  | ||||||
|             .withIcon(GoogleMaterial.Icon.gmd_settings) |  | ||||||
|             .withIdentifier(MANAGE_IDENTITY) |  | ||||||
|         ); |  | ||||||
|         // Create the AccountHeader |  | ||||||
|         accountHeader = new AccountHeaderBuilder() |  | ||||||
|             .withActivity(this) |  | ||||||
|             .withHeaderBackground(R.drawable.header) |  | ||||||
|             .withProfiles(profiles) |  | ||||||
|             .withOnAccountHeaderProfileImageListener(new AccountHeader.OnAccountHeaderProfileImageListener() { |  | ||||||
|                 @Override |  | ||||||
|                 public boolean onProfileImageClick(View view, IProfile profile, boolean current) { |  | ||||||
|                     if (current) { |  | ||||||
|                         //  Show QR code in modal dialog |  | ||||||
|                         final Dialog dialog = new Dialog(MainActivity.this); |  | ||||||
|                         dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); |  | ||||||
|  |  | ||||||
|                         ImageView imageView = new ImageView(MainActivity.this); |  | ||||||
|                         imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(MainActivity.this))); |  | ||||||
|                         imageView.setOnClickListener(new View.OnClickListener() { |  | ||||||
|                             @Override |  | ||||||
|                             public void onClick(View v) { |  | ||||||
|                                 dialog.dismiss(); |  | ||||||
|                             } |  | ||||||
|                         }); |  | ||||||
|                         dialog.addContentView(imageView, new RelativeLayout.LayoutParams( |  | ||||||
|                             ViewGroup.LayoutParams.MATCH_PARENT, |  | ||||||
|                             ViewGroup.LayoutParams.MATCH_PARENT)); |  | ||||||
|                         Window window = dialog.getWindow(); |  | ||||||
|                         if (window != null) { |  | ||||||
|                             Display display = window.getWindowManager().getDefaultDisplay(); |  | ||||||
|                             Point size = new Point(); |  | ||||||
|                             display.getSize(size); |  | ||||||
|                             int dim = size.x < size.y ? size.x : size.y; |  | ||||||
|  |  | ||||||
|                             WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); |  | ||||||
|                             lp.copyFrom(window.getAttributes()); |  | ||||||
|                             lp.width = dim; |  | ||||||
|                             lp.height = dim; |  | ||||||
|  |  | ||||||
|                             window.setAttributes(lp); |  | ||||||
|                         } |  | ||||||
|                         dialog.show(); |  | ||||||
|                         return true; |  | ||||||
|                     } |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 @Override |  | ||||||
|                 public boolean onProfileImageLongClick(View view, IProfile iProfile, boolean b) { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|             .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { |  | ||||||
|                 @Override |  | ||||||
|                 public boolean onProfileChanged(View view, IProfile profile, boolean current) { |  | ||||||
|                     switch ((int) profile.getIdentifier()) { |  | ||||||
|                         case ADD_IDENTITY: |  | ||||||
|                             addIdentityDialog(); |  | ||||||
|                             break; |  | ||||||
|                         case MANAGE_IDENTITY: |  | ||||||
|                             BitmessageAddress identity = Singleton.getIdentity(MainActivity.this); |  | ||||||
|                             if (identity == null) { |  | ||||||
|                                 Toast.makeText(MainActivity.this, |  | ||||||
|                                     R.string.no_identity_warning, LENGTH_LONG).show(); |  | ||||||
|                             } else { |  | ||||||
|                                 Intent show = new Intent(MainActivity.this, |  | ||||||
|                                     AddressDetailActivity.class); |  | ||||||
|                                 show.putExtra(AddressDetailFragment.ARG_ITEM, identity); |  | ||||||
|                                 startActivity(show); |  | ||||||
|                             } |  | ||||||
|                             break; |  | ||||||
|                         default: |  | ||||||
|                             if (profile instanceof ProfileDrawerItem) { |  | ||||||
|                                 Object tag = ((ProfileDrawerItem) profile).getTag(); |  | ||||||
|                                 if (tag instanceof BitmessageAddress) { |  | ||||||
|                                     Singleton.setIdentity((BitmessageAddress) tag); |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                     } |  | ||||||
|                     // false if it should close the drawer |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|             .build(); |  | ||||||
|         if (profiles.size() > 2) { // There's always the add and manage identity items |  | ||||||
|             accountHeader.setActiveProfile(profiles.get(0), true); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         final ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); |  | ||||||
|         drawerItems.add(new PrimaryDrawerItem() |  | ||||||
|             .withName(R.string.archive) |  | ||||||
|             .withTag(LABEL_ARCHIVE) |  | ||||||
|             .withIcon(CommunityMaterial.Icon.cmd_archive) |  | ||||||
|         ); |  | ||||||
|         drawerItems.add(new DividerDrawerItem()); |  | ||||||
|         drawerItems.add(new PrimaryDrawerItem() |  | ||||||
|             .withName(R.string.contacts_and_subscriptions) |  | ||||||
|             .withIcon(GoogleMaterial.Icon.gmd_contacts)); |  | ||||||
|         drawerItems.add(new PrimaryDrawerItem() |  | ||||||
|             .withName(R.string.settings) |  | ||||||
|             .withIcon(GoogleMaterial.Icon.gmd_settings)); |  | ||||||
|  |  | ||||||
|         nodeSwitch = new SwitchDrawerItem() |  | ||||||
|             .withIdentifier(ID_NODE_SWITCH) |  | ||||||
|             .withName(R.string.full_node) |  | ||||||
|             .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) |  | ||||||
|             .withChecked(isRunning()) |  | ||||||
|             .withOnCheckedChangeListener(new OnCheckedChangeListener() { |  | ||||||
|                 @Override |  | ||||||
|                 public void onCheckedChanged(IDrawerItem drawerItem, CompoundButton buttonView, |  | ||||||
|                                              boolean isChecked) { |  | ||||||
|                     if (isChecked) { |  | ||||||
|                         checkAndStartNode(); |  | ||||||
|                     } else { |  | ||||||
|                         stopService(new Intent(MainActivity.this, BitmessageService.class)); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|         drawer = new DrawerBuilder() |  | ||||||
|             .withActivity(this) |  | ||||||
|             .withToolbar(toolbar) |  | ||||||
|             .withAccountHeader(accountHeader) |  | ||||||
|             .withDrawerItems(drawerItems) |  | ||||||
|             .addStickyDrawerItems(nodeSwitch) |  | ||||||
|             .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { |  | ||||||
|                 @Override |  | ||||||
|                 public boolean onItemClick(View view, int position, IDrawerItem item) { |  | ||||||
|                     if (item.getTag() instanceof Label) { |  | ||||||
|                         selectedLabel = (Label) item.getTag(); |  | ||||||
|                         if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof |  | ||||||
|                             MessageListFragment) { |  | ||||||
|                             ((MessageListFragment) getSupportFragmentManager() |  | ||||||
|                                 .findFragmentById(R.id.item_list)).updateList(selectedLabel); |  | ||||||
|                         } else { |  | ||||||
|                             MessageListFragment listFragment = new MessageListFragment(); |  | ||||||
|                             changeList(listFragment); |  | ||||||
|                             listFragment.updateList(selectedLabel); |  | ||||||
|                         } |  | ||||||
|                         return false; |  | ||||||
|                     } else if (item instanceof Nameable<?>) { |  | ||||||
|                         Nameable<?> ni = (Nameable<?>) item; |  | ||||||
|                         switch (ni.getName().getTextRes()) { |  | ||||||
|                             case R.string.contacts_and_subscriptions: |  | ||||||
|                                 if (!(getSupportFragmentManager().findFragmentById(R.id |  | ||||||
|                                     .item_list) instanceof AddressListFragment)) { |  | ||||||
|                                     changeList(new AddressListFragment()); |  | ||||||
|                                 } else { |  | ||||||
|                                     ((AddressListFragment) getSupportFragmentManager() |  | ||||||
|                                         .findFragmentById(R.id.item_list)).updateList(); |  | ||||||
|                                 } |  | ||||||
|                                 break; |  | ||||||
|                             case R.string.settings: |  | ||||||
|                                 startActivity(new Intent(MainActivity.this, SettingsActivity |  | ||||||
|                                     .class)); |  | ||||||
|                                 break; |  | ||||||
|                             case R.string.full_node: |  | ||||||
|                                 return true; |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|             .withShowDrawerOnFirstLaunch(true) |  | ||||||
|             .build(); |  | ||||||
|  |  | ||||||
|         new AsyncTask<Void, Void, List<BitmessageAddress>>() { |  | ||||||
|             @Override |  | ||||||
|             protected List<BitmessageAddress> doInBackground(Void... params) { |  | ||||||
|                 List<BitmessageAddress> identities = bmc.addresses().getIdentities(); |  | ||||||
|                 if (identities.isEmpty()) { |  | ||||||
|                     // Create an initial identity |  | ||||||
|                     Singleton.getIdentity(MainActivity.this); |  | ||||||
|                 } |  | ||||||
|                 return identities; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             protected void onPostExecute(List<BitmessageAddress> identities) { |  | ||||||
|                 for (BitmessageAddress identity : identities) { |  | ||||||
|                     addIdentityEntry(identity); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }.execute(); |  | ||||||
|  |  | ||||||
|         new AsyncTask<Void, Void, List<Label>>() { |  | ||||||
|             @Override |  | ||||||
|             protected List<Label> doInBackground(Void... params) { |  | ||||||
|                 return bmc.messages().getLabels(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             protected void onPostExecute(List<Label> labels) { |  | ||||||
|                 if (getIntent().hasExtra(EXTRA_SHOW_LABEL)) { |  | ||||||
|                     selectedLabel = (Label) getIntent().getSerializableExtra(EXTRA_SHOW_LABEL); |  | ||||||
|                 } else if (selectedLabel == null) { |  | ||||||
|                     selectedLabel = labels.get(0); |  | ||||||
|                 } |  | ||||||
|                 for (Label label : labels) { |  | ||||||
|                     addLabelEntry(label); |  | ||||||
|                 } |  | ||||||
|                 IDrawerItem selectedDrawerItem = drawer.getDrawerItem(selectedLabel); |  | ||||||
|                 if (selectedDrawerItem != null) { |  | ||||||
|                     drawer.setSelection(selectedDrawerItem); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }.execute(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onSaveInstanceState(Bundle savedInstanceState) { |  | ||||||
|         super.onSaveInstanceState(savedInstanceState); |  | ||||||
|         savedInstanceState.putSerializable("selectedLabel", selectedLabel); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     @SuppressWarnings("unchecked") |  | ||||||
|     protected void onRestoreInstanceState(Bundle savedInstanceState) { |  | ||||||
|         selectedLabel = (Label) savedInstanceState.getSerializable("selectedLabel"); |  | ||||||
|  |  | ||||||
|         IDrawerItem selectedItem = drawer.getDrawerItem(selectedLabel); |  | ||||||
|         if (selectedItem != null) { |  | ||||||
|             drawer.setSelection(selectedItem); |  | ||||||
|         } |  | ||||||
|         super.onRestoreInstanceState(savedInstanceState); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void addIdentityDialog() { |  | ||||||
|         AddIdentityDialogFragment dialog = new AddIdentityDialogFragment(); |  | ||||||
|         dialog.show(getSupportFragmentManager(), "dialog"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onResume() { |  | ||||||
|         updateUnread(); |  | ||||||
|         updateNodeSwitch(); |  | ||||||
|         Singleton.getMessageListener(this).resetNotification(); |  | ||||||
|         super.onResume(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void addIdentityEntry(BitmessageAddress identity) { |  | ||||||
|         IProfile newProfile = new ProfileDrawerItem() |  | ||||||
|             .withIcon(new Identicon(identity)) |  | ||||||
|             .withName(identity.toString()) |  | ||||||
|             .withNameShown(true) |  | ||||||
|             .withEmail(identity.getAddress()) |  | ||||||
|             .withTag(identity); |  | ||||||
|         if (accountHeader.getProfiles() != null) { |  | ||||||
|             // we know that there are 2 setting elements. |  | ||||||
|             // Set the new profile above them ;) |  | ||||||
|             accountHeader.addProfile( |  | ||||||
|                 newProfile, accountHeader.getProfiles().size() - 2); |  | ||||||
|         } else { |  | ||||||
|             accountHeader.addProfiles(newProfile); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void addLabelEntry(Label label) { |  | ||||||
|         PrimaryDrawerItem item = new PrimaryDrawerItem() |  | ||||||
|             .withName(label.toString()) |  | ||||||
|             .withTag(label) |  | ||||||
|             .withIcon(Labels.getIcon(label)) |  | ||||||
|             .withIconColor(Labels.getColor(label)); |  | ||||||
|         drawer.addItemAtPosition(item, drawer.getDrawerItems().size() - 3); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void updateIdentityEntry(BitmessageAddress identity) { |  | ||||||
|         for (IProfile profile : accountHeader.getProfiles()) { |  | ||||||
|             if (profile instanceof ProfileDrawerItem) { |  | ||||||
|                 if (identity.equals(((ProfileDrawerItem) profile).getTag())) { |  | ||||||
|                     ((ProfileDrawerItem) profile) |  | ||||||
|                         .withName(identity.toString()) |  | ||||||
|                         .withTag(identity); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void removeIdentityEntry(BitmessageAddress identity) { |  | ||||||
|         for (IProfile profile : accountHeader.getProfiles()) { |  | ||||||
|             if (profile instanceof ProfileDrawerItem) { |  | ||||||
|                 if (identity.equals(((ProfileDrawerItem) profile).getTag())) { |  | ||||||
|                     accountHeader.removeProfile(profile); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void checkAndStartNode() { |  | ||||||
|         if (Preferences.isConnectionAllowed(MainActivity.this)) { |  | ||||||
|             startService(new Intent(this, BitmessageService.class)); |  | ||||||
|         } else { |  | ||||||
|             startActivity(new Intent(this, FullNodeDialogActivity.class)); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void updateUnread() { |  | ||||||
|         for (IDrawerItem item : drawer.getDrawerItems()) { |  | ||||||
|             if (item.getTag() instanceof Label) { |  | ||||||
|                 Label label = (Label) item.getTag(); |  | ||||||
|                 if (label != LABEL_ARCHIVE) { |  | ||||||
|                     int unread = bmc.messages().countUnread(label); |  | ||||||
|                     if (unread > 0) { |  | ||||||
|                         ((PrimaryDrawerItem) item).withBadge(String.valueOf(unread)); |  | ||||||
|                     } else { |  | ||||||
|                         ((PrimaryDrawerItem) item).withBadge((String) null); |  | ||||||
|                     } |  | ||||||
|                     drawer.updateItem(item); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static void updateNodeSwitch() { |  | ||||||
|         final MainActivity i = getInstance(); |  | ||||||
|         if (i != null) { |  | ||||||
|             i.runOnUiThread(new Runnable() { |  | ||||||
|                 @Override |  | ||||||
|                 public void run() { |  | ||||||
|                     i.nodeSwitch.withChecked(i.bmc.isRunning()); |  | ||||||
|                     i.drawer.updateStickyFooterItem(i.nodeSwitch); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Callback method from {@link ListSelectionListener} |  | ||||||
|      * indicating that the item with the given ID was selected. |  | ||||||
|      */ |  | ||||||
|     @Override |  | ||||||
|     public void onItemSelected(Serializable item) { |  | ||||||
|         if (twoPane) { |  | ||||||
|             // In two-pane mode, show the detail view in this activity by |  | ||||||
|             // adding or replacing the detail fragment using a |  | ||||||
|             // fragment transaction. |  | ||||||
|             Bundle arguments = new Bundle(); |  | ||||||
|             arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item); |  | ||||||
|             Fragment fragment; |  | ||||||
|             if (item instanceof Plaintext) |  | ||||||
|                 fragment = new MessageDetailFragment(); |  | ||||||
|             else if (item instanceof BitmessageAddress) |  | ||||||
|                 fragment = new AddressDetailFragment(); |  | ||||||
|             else |  | ||||||
|                 throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + |  | ||||||
|                     "was " |  | ||||||
|                     + item.getClass().getSimpleName()); |  | ||||||
|             fragment.setArguments(arguments); |  | ||||||
|             getSupportFragmentManager().beginTransaction() |  | ||||||
|                 .replace(R.id.message_detail_container, fragment) |  | ||||||
|                 .commit(); |  | ||||||
|         } else { |  | ||||||
|             // In single-pane mode, simply start the detail activity |  | ||||||
|             // for the selected item ID. |  | ||||||
|             Intent detailIntent; |  | ||||||
|             if (item instanceof Plaintext) { |  | ||||||
|                 detailIntent = new Intent(this, MessageDetailActivity.class); |  | ||||||
|                 detailIntent.putExtra(EXTRA_SHOW_LABEL, selectedLabel); |  | ||||||
|             } else if (item instanceof BitmessageAddress) { |  | ||||||
|                 detailIntent = new Intent(this, AddressDetailActivity.class); |  | ||||||
|             } else { |  | ||||||
|                 throw new IllegalArgumentException("Plaintext or BitmessageAddress expected, but " + |  | ||||||
|                     "was " |  | ||||||
|                     + item.getClass().getSimpleName()); |  | ||||||
|             } |  | ||||||
|             detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item); |  | ||||||
|             startActivity(detailIntent); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void updateTitle(CharSequence title) { |  | ||||||
|         if (getSupportActionBar() != null) { |  | ||||||
|             getSupportActionBar().setTitle(title); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public Label getSelectedLabel() { |  | ||||||
|         return selectedLabel; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static MainActivity getInstance() { |  | ||||||
|         if (instance == null) return null; |  | ||||||
|         return instance.get(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										505
									
								
								app/src/main/java/ch/dissem/apps/abit/MainActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										505
									
								
								app/src/main/java/ch/dissem/apps/abit/MainActivity.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,505 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.content.Intent | ||||||
|  | import android.graphics.Point | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v4.app.Fragment | ||||||
|  | import android.support.v7.app.AppCompatActivity | ||||||
|  | import android.support.v7.widget.Toolbar | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.RelativeLayout | ||||||
|  | import ch.dissem.apps.abit.drawer.ProfileImageListener | ||||||
|  | import ch.dissem.apps.abit.drawer.ProfileSelectionListener | ||||||
|  | import ch.dissem.apps.abit.listener.ListSelectionListener | ||||||
|  | import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.apps.abit.service.Singleton.currentLabel | ||||||
|  | import ch.dissem.apps.abit.synchronization.SyncAdapter | ||||||
|  | import ch.dissem.apps.abit.util.Labels | ||||||
|  | import ch.dissem.apps.abit.util.NetworkUtils | ||||||
|  | import ch.dissem.apps.abit.util.Preferences | ||||||
|  | import ch.dissem.bitmessage.BitmessageContext | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.Label | ||||||
|  | import com.github.amlcurran.showcaseview.ShowcaseView | ||||||
|  | import com.mikepenz.community_material_typeface_library.CommunityMaterial | ||||||
|  | import com.mikepenz.google_material_typeface_library.GoogleMaterial | ||||||
|  | import com.mikepenz.iconics.IconicsDrawable | ||||||
|  | import com.mikepenz.materialdrawer.AccountHeader | ||||||
|  | import com.mikepenz.materialdrawer.AccountHeaderBuilder | ||||||
|  | import com.mikepenz.materialdrawer.Drawer | ||||||
|  | import com.mikepenz.materialdrawer.DrawerBuilder | ||||||
|  | import com.mikepenz.materialdrawer.model.* | ||||||
|  | import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem | ||||||
|  | import com.mikepenz.materialdrawer.model.interfaces.IProfile | ||||||
|  | import com.mikepenz.materialdrawer.model.interfaces.Nameable | ||||||
|  | import io.github.kobakei.materialfabspeeddial.FabSpeedDial | ||||||
|  | import kotlinx.android.synthetic.main.activity_main.* | ||||||
|  | import org.jetbrains.anko.doAsync | ||||||
|  | import org.jetbrains.anko.uiThread | ||||||
|  | import java.io.Serializable | ||||||
|  | import java.lang.ref.WeakReference | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * An activity representing a list of Messages. This activity | ||||||
|  |  * has different presentations for handset and tablet-size devices. On | ||||||
|  |  * handsets, the activity presents a list of items, which when touched, | ||||||
|  |  * lead to a [MessageDetailActivity] representing | ||||||
|  |  * item details. On tablets, the activity presents the list of items and | ||||||
|  |  * item details side-by-side using two vertical panes. | ||||||
|  |  * | ||||||
|  |  * | ||||||
|  |  * The activity makes heavy use of fragments. The list of items is a | ||||||
|  |  * [MessageListFragment] and the item details | ||||||
|  |  * (if present) is a [MessageDetailFragment]. | ||||||
|  |  * | ||||||
|  |  * | ||||||
|  |  * This activity also implements the required | ||||||
|  |  * [ListSelectionListener] interface | ||||||
|  |  * to listen for item selections. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> { | ||||||
|  |  | ||||||
|  |     private var active: Boolean = false | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Whether or not the activity is in two-pane mode, i.e. running on a tablet | ||||||
|  |      * device. | ||||||
|  |      */ | ||||||
|  |     var hasDetailPane: Boolean = false | ||||||
|  |         private set | ||||||
|  |  | ||||||
|  |     private lateinit var bmc: BitmessageContext | ||||||
|  |     private lateinit var accountHeader: AccountHeader | ||||||
|  |  | ||||||
|  |     private lateinit var drawer: Drawer | ||||||
|  |     private lateinit var nodeSwitch: SwitchDrawerItem | ||||||
|  |  | ||||||
|  |     val floatingActionButton: FabSpeedDial? | ||||||
|  |         get() = findViewById(R.id.fab) | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         instance = WeakReference(this) | ||||||
|  |         bmc = Singleton.getBitmessageContext(this) | ||||||
|  |  | ||||||
|  |         setContentView(R.layout.activity_main) | ||||||
|  |         fab.hide() | ||||||
|  |  | ||||||
|  |         val toolbar = findViewById<Toolbar>(R.id.toolbar) | ||||||
|  |         setSupportActionBar(toolbar) | ||||||
|  |  | ||||||
|  |         val listFragment = MessageListFragment() | ||||||
|  |         supportFragmentManager | ||||||
|  |             .beginTransaction() | ||||||
|  |             .replace(R.id.item_list, listFragment) | ||||||
|  |             .commit() | ||||||
|  |  | ||||||
|  |         if (findViewById<View>(R.id.message_detail_container) != null) { | ||||||
|  |             // The detail container view will be present only in the | ||||||
|  |             // large-screen layouts (res/values-large and | ||||||
|  |             // res/values-sw600dp). If this view is present, then the | ||||||
|  |             // activity should be in two-pane mode. | ||||||
|  |             hasDetailPane = true | ||||||
|  |  | ||||||
|  |             // In two-pane mode, list items should be given the | ||||||
|  |             // 'activated' state when touched. | ||||||
|  |             listFragment.setActivateOnItemClick(true) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         createDrawer(toolbar) | ||||||
|  |  | ||||||
|  |         // handle intents | ||||||
|  |         val intent = intent | ||||||
|  |         if (intent.hasExtra(EXTRA_SHOW_MESSAGE)) { | ||||||
|  |             onItemSelected(intent.getSerializableExtra(EXTRA_SHOW_MESSAGE)) | ||||||
|  |         } | ||||||
|  |         if (intent.hasExtra(EXTRA_REPLY_TO_MESSAGE)) { | ||||||
|  |             val item = intent.getSerializableExtra(EXTRA_REPLY_TO_MESSAGE) as Plaintext | ||||||
|  |             ComposeMessageActivity.launchReplyTo(this, item) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (Preferences.useTrustedNode(this)) { | ||||||
|  |             SyncAdapter.startSync(this) | ||||||
|  |         } else { | ||||||
|  |             SyncAdapter.stopSync(this) | ||||||
|  |         } | ||||||
|  |         if (drawer.isDrawerOpen) { | ||||||
|  |             val lps = RelativeLayout.LayoutParams(ViewGroup | ||||||
|  |                 .LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) | ||||||
|  |             lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM) | ||||||
|  |             lps.addRule(RelativeLayout.ALIGN_PARENT_LEFT) | ||||||
|  |             val margin = ((resources.displayMetrics.density * 12) as Number).toInt() | ||||||
|  |             lps.setMargins(margin, margin, margin, margin) | ||||||
|  |  | ||||||
|  |             ShowcaseView.Builder(this) | ||||||
|  |                 .withMaterialShowcase() | ||||||
|  |                 .setStyle(R.style.CustomShowcaseTheme) | ||||||
|  |                 .setContentTitle(R.string.full_node) | ||||||
|  |                 .setContentText(R.string.full_node_description) | ||||||
|  |                 .setTarget { | ||||||
|  |                     val view = drawer.stickyFooter | ||||||
|  |                     val location = IntArray(2) | ||||||
|  |                     view.getLocationInWindow(location) | ||||||
|  |                     val x = location[0] + 7 * view.width / 8 | ||||||
|  |                     val y = location[1] + view.height / 2 | ||||||
|  |                     Point(x, y) | ||||||
|  |                 } | ||||||
|  |                 .replaceEndButton(R.layout.showcase_button) | ||||||
|  |                 .hideOnTouchOutside() | ||||||
|  |                 .build() | ||||||
|  |                 .setButtonPosition(lps) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> { | ||||||
|  |         if (active) { | ||||||
|  |             val transaction = supportFragmentManager.beginTransaction() | ||||||
|  |             transaction.replace(R.id.item_list, listFragment) | ||||||
|  |             supportFragmentManager.findFragmentById(R.id.message_detail_container)?.let { | ||||||
|  |                 transaction.remove(it) | ||||||
|  |             } | ||||||
|  |             transaction.addToBackStack(null).commit() | ||||||
|  |  | ||||||
|  |             if (hasDetailPane) { | ||||||
|  |                 // In two-pane mode, list items should be given the | ||||||
|  |                 // 'activated' state when touched. | ||||||
|  |                 listFragment.setActivateOnItemClick(true) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun createDrawer(toolbar: Toolbar) { | ||||||
|  |         val profiles = ArrayList<IProfile<*>>() | ||||||
|  |         profiles.add(ProfileSettingDrawerItem() | ||||||
|  |             .withName(getString(R.string.add_identity)) | ||||||
|  |             .withDescription(getString(R.string.add_identity_summary)) | ||||||
|  |             .withIcon(IconicsDrawable(this, GoogleMaterial.Icon.gmd_add) | ||||||
|  |                 .actionBar() | ||||||
|  |                 .paddingDp(5) | ||||||
|  |                 .colorRes(R.color.icons)) | ||||||
|  |             .withIdentifier(ADD_IDENTITY.toLong()) | ||||||
|  |         ) | ||||||
|  |         profiles.add(ProfileSettingDrawerItem() | ||||||
|  |             .withName(getString(R.string.manage_identity)) | ||||||
|  |             .withIcon(GoogleMaterial.Icon.gmd_settings) | ||||||
|  |             .withIdentifier(MANAGE_IDENTITY.toLong()) | ||||||
|  |         ) | ||||||
|  |         // Create the AccountHeader | ||||||
|  |         accountHeader = AccountHeaderBuilder() | ||||||
|  |             .withActivity(this) | ||||||
|  |             .withHeaderBackground(R.drawable.header) | ||||||
|  |             .withProfiles(profiles) | ||||||
|  |             .withOnAccountHeaderProfileImageListener(ProfileImageListener(this)) | ||||||
|  |             .withOnAccountHeaderListener(ProfileSelectionListener(this@MainActivity, supportFragmentManager)) | ||||||
|  |             .build() | ||||||
|  |         if (profiles.size > 2) { // There's always the add and manage identity items | ||||||
|  |             accountHeader.setActiveProfile(profiles[0], true) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val drawerItems = ArrayList<IDrawerItem<*, *>>() | ||||||
|  |         drawerItems.add(PrimaryDrawerItem() | ||||||
|  |             .withName(R.string.archive) | ||||||
|  |             .withTag(LABEL_ARCHIVE) | ||||||
|  |             .withIcon(CommunityMaterial.Icon.cmd_archive) | ||||||
|  |         ) | ||||||
|  |         drawerItems.add(DividerDrawerItem()) | ||||||
|  |         drawerItems.add(PrimaryDrawerItem() | ||||||
|  |             .withName(R.string.contacts_and_subscriptions) | ||||||
|  |             .withIcon(GoogleMaterial.Icon.gmd_contacts)) | ||||||
|  |         drawerItems.add(PrimaryDrawerItem() | ||||||
|  |             .withName(R.string.settings) | ||||||
|  |             .withIcon(GoogleMaterial.Icon.gmd_settings)) | ||||||
|  |  | ||||||
|  |         nodeSwitch = SwitchDrawerItem() | ||||||
|  |             .withIdentifier(ID_NODE_SWITCH) | ||||||
|  |             .withName(R.string.full_node) | ||||||
|  |             .withIcon(CommunityMaterial.Icon.cmd_cloud_outline) | ||||||
|  |             .withChecked(Preferences.isFullNodeActive(this)) | ||||||
|  |             .withOnCheckedChangeListener { _, _, isChecked -> | ||||||
|  |                 if (isChecked) { | ||||||
|  |                     NetworkUtils.enableNode(this@MainActivity) | ||||||
|  |                 } else { | ||||||
|  |                     NetworkUtils.disableNode(this@MainActivity) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |         drawer = DrawerBuilder() | ||||||
|  |             .withActivity(this) | ||||||
|  |             .withToolbar(toolbar) | ||||||
|  |             .withAccountHeader(accountHeader) | ||||||
|  |             .withDrawerItems(drawerItems) | ||||||
|  |             .addStickyDrawerItems(nodeSwitch) | ||||||
|  |             .withOnDrawerItemClickListener(DrawerItemClickListener()) | ||||||
|  |             .withShowDrawerOnFirstLaunch(true) | ||||||
|  |             .build() | ||||||
|  |  | ||||||
|  |         loadDrawerItemsAsynchronously() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun loadDrawerItemsAsynchronously() { | ||||||
|  |         doAsync { | ||||||
|  |             val identities = bmc.addresses.getIdentities() | ||||||
|  |             if (identities.isEmpty()) { | ||||||
|  |                 // Create an initial identity | ||||||
|  |                 Singleton.getIdentity(this@MainActivity) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             uiThread { | ||||||
|  |                 for (identity in identities) { | ||||||
|  |                     addIdentityEntry(identity) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         doAsync { | ||||||
|  |             val labels = bmc.labels.getLabels() | ||||||
|  |  | ||||||
|  |             uiThread { | ||||||
|  |                 if (intent.hasExtra(EXTRA_SHOW_LABEL)) { | ||||||
|  |                     currentLabel.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label | ||||||
|  |                 } else if (currentLabel.value == null) { | ||||||
|  |                     currentLabel.value = labels[0] | ||||||
|  |                 } | ||||||
|  |                 for (label in labels) { | ||||||
|  |                     addLabelEntry(label) | ||||||
|  |                 } | ||||||
|  |                 currentLabel.value?.let { | ||||||
|  |                     drawer.setSelection(it.id as Long) | ||||||
|  |                 } | ||||||
|  |                 updateUnread() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onBackPressed() { | ||||||
|  |         val listFragment = supportFragmentManager.findFragmentById(R.id.item_list) | ||||||
|  |         if (listFragment !is ListHolder<*> || !listFragment.showPreviousList()) { | ||||||
|  |             super.onBackPressed() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private inner class DrawerItemClickListener : Drawer.OnDrawerItemClickListener { | ||||||
|  |         override fun onItemClick(view: View?, position: Int, item: IDrawerItem<*, *>): Boolean { | ||||||
|  |             val itemList = supportFragmentManager.findFragmentById(R.id.item_list) | ||||||
|  |             val tag = item.tag | ||||||
|  |             if (tag is Label) { | ||||||
|  |                 currentLabel.value = tag | ||||||
|  |                 if (itemList !is MessageListFragment) { | ||||||
|  |                     changeList(MessageListFragment()) | ||||||
|  |                 } | ||||||
|  |                 return false | ||||||
|  |             } else if (item is Nameable<*>) { | ||||||
|  |                 when (item.name.textRes) { | ||||||
|  |                     R.string.contacts_and_subscriptions -> { | ||||||
|  |                         if (itemList is AddressListFragment) { | ||||||
|  |                             itemList.updateList() | ||||||
|  |                         } else { | ||||||
|  |                             changeList(AddressListFragment()) | ||||||
|  |                         } | ||||||
|  |                         return false | ||||||
|  |                     } | ||||||
|  |                     R.string.settings -> { | ||||||
|  |                         supportFragmentManager | ||||||
|  |                             .beginTransaction() | ||||||
|  |                             .replace(R.id.item_list, SettingsFragment()) | ||||||
|  |                             .addToBackStack(null) | ||||||
|  |                             .commit() | ||||||
|  |                         return false | ||||||
|  |                     } | ||||||
|  |                     R.string.full_node -> return true | ||||||
|  |                     else -> return false | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onResume() { | ||||||
|  |         updateUnread() | ||||||
|  |         if (Preferences.isFullNodeActive(this) && Preferences.isConnectionAllowed(this@MainActivity)) { | ||||||
|  |             NetworkUtils.enableNode(this, false) | ||||||
|  |         } | ||||||
|  |         Singleton.getMessageListener(this).resetNotification() | ||||||
|  |         currentLabel.addObserver(this) { label -> | ||||||
|  |             if (label != null) { | ||||||
|  |                 drawer.setSelection(label.id as Long) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         active = true | ||||||
|  |         super.onResume() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onPause() { | ||||||
|  |         currentLabel.removeObserver(this) | ||||||
|  |         super.onPause() | ||||||
|  |         active = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun addIdentityEntry(identity: BitmessageAddress) { | ||||||
|  |         val newProfile = ProfileDrawerItem() | ||||||
|  |             .withIcon(Identicon(identity)) | ||||||
|  |             .withName(identity.toString()) | ||||||
|  |             .withNameShown(true) | ||||||
|  |             .withEmail(identity.address) | ||||||
|  |             .withTag(identity) | ||||||
|  |         if (accountHeader.profiles != null) { | ||||||
|  |             // we know that there are 2 setting elements. | ||||||
|  |             // Set the new profile above them ;) | ||||||
|  |             accountHeader.addProfile( | ||||||
|  |                 newProfile, accountHeader.profiles.size - 2) | ||||||
|  |         } else { | ||||||
|  |             accountHeader.addProfiles(newProfile) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun addLabelEntry(label: Label) { | ||||||
|  |         val item = PrimaryDrawerItem() | ||||||
|  |             .withIdentifier(label.id as Long) | ||||||
|  |             .withName(label.toString()) | ||||||
|  |             .withTag(label) | ||||||
|  |             .withIcon(Labels.getIcon(label)) | ||||||
|  |             .withIconColor(Labels.getColor(label)) | ||||||
|  |         drawer.addItemAtPosition(item, drawer.drawerItems.size - 3) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun updateIdentityEntry(identity: BitmessageAddress) { | ||||||
|  |         for (profile in accountHeader.profiles) { | ||||||
|  |             if (profile is ProfileDrawerItem) { | ||||||
|  |                 if (identity == profile.tag) { | ||||||
|  |                     profile | ||||||
|  |                         .withName(identity.toString()) | ||||||
|  |                         .withTag(identity) | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun removeIdentityEntry(identity: BitmessageAddress) { | ||||||
|  |         for (profile in accountHeader.profiles) { | ||||||
|  |             if (profile is ProfileDrawerItem) { | ||||||
|  |                 if (identity == profile.tag) { | ||||||
|  |                     accountHeader.removeProfile(profile) | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun updateUnread() { | ||||||
|  |         for (item in drawer.drawerItems) { | ||||||
|  |             if (item.tag is Label) { | ||||||
|  |                 val label = item.tag as Label | ||||||
|  |                 if (label !== LABEL_ARCHIVE) { | ||||||
|  |                     val unread = bmc.messages.countUnread(label) | ||||||
|  |                     if (unread > 0) { | ||||||
|  |                         (item as PrimaryDrawerItem).withBadge(unread.toString()) | ||||||
|  |                     } else { | ||||||
|  |                         (item as PrimaryDrawerItem).withBadge(null as String?) | ||||||
|  |                     } | ||||||
|  |                     drawer.updateItem(item) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Callback method from [ListSelectionListener] | ||||||
|  |      * indicating that the item with the given ID was selected. | ||||||
|  |      */ | ||||||
|  |     override fun onItemSelected(item: Serializable) { | ||||||
|  |         if (hasDetailPane) { | ||||||
|  |             // In two-pane mode, show the detail view in this activity by | ||||||
|  |             // adding or replacing the detail fragment using a | ||||||
|  |             // fragment transaction. | ||||||
|  |             val arguments = Bundle() | ||||||
|  |             arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item) | ||||||
|  |             val fragment = when (item) { | ||||||
|  |                 is Plaintext -> MessageDetailFragment() | ||||||
|  |                 is BitmessageAddress -> AddressDetailFragment() | ||||||
|  |                 else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}") | ||||||
|  |             } | ||||||
|  |             fragment.arguments = arguments | ||||||
|  |             supportFragmentManager.beginTransaction() | ||||||
|  |                 .replace(R.id.message_detail_container, fragment) | ||||||
|  |                 .commit() | ||||||
|  |         } else { | ||||||
|  |             // In single-pane mode, simply start the detail activity | ||||||
|  |             // for the selected item ID. | ||||||
|  |             val detailIntent = when (item) { | ||||||
|  |                 is Plaintext -> { | ||||||
|  |                     Intent(this, MessageDetailActivity::class.java) | ||||||
|  |                 } | ||||||
|  |                 is BitmessageAddress -> Intent(this, AddressDetailActivity::class.java) | ||||||
|  |                 else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}") | ||||||
|  |             } | ||||||
|  |             detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item) | ||||||
|  |             startActivity(detailIntent) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun setDetailView(fragment: Fragment) { | ||||||
|  |         if (hasDetailPane) { | ||||||
|  |             supportFragmentManager.beginTransaction() | ||||||
|  |                 .replace(R.id.message_detail_container, fragment) | ||||||
|  |                 .commit() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun updateTitle(title: CharSequence) { | ||||||
|  |         supportActionBar?.title = title | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage" | ||||||
|  |         val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel" | ||||||
|  |         val EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage" | ||||||
|  |         val ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox" | ||||||
|  |  | ||||||
|  |         val ADD_IDENTITY = 1 | ||||||
|  |         val MANAGE_IDENTITY = 2 | ||||||
|  |  | ||||||
|  |         private val ID_NODE_SWITCH: Long = 1 | ||||||
|  |  | ||||||
|  |         private var instance: WeakReference<MainActivity>? = null | ||||||
|  |  | ||||||
|  |         fun updateNodeSwitch() { | ||||||
|  |             apply { | ||||||
|  |                 runOnUiThread { | ||||||
|  |                     nodeSwitch.withChecked(Preferences.isFullNodeActive(this)) | ||||||
|  |                     drawer.updateStickyFooterItem(nodeSwitch) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Runs the given code in the main activity context, if it currently exists. Otherwise, | ||||||
|  |          * it's ignored. | ||||||
|  |          */ | ||||||
|  |         fun apply(run: MainActivity.() -> Unit) { | ||||||
|  |             instance?.get()?.let { run.invoke(it) } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,63 +0,0 @@ | |||||||
| package ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.v4.app.NavUtils; |  | ||||||
| import android.view.MenuItem; |  | ||||||
|  |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.Label; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * An activity representing a single Message detail screen. This |  | ||||||
|  * activity is only used on handset devices. On tablet-size devices, |  | ||||||
|  * item details are presented side-by-side with a list of items |  | ||||||
|  * in a {@link MainActivity}. |  | ||||||
|  * <p/> |  | ||||||
|  * This activity is mostly just a 'shell' activity containing nothing |  | ||||||
|  * more than a {@link MessageDetailFragment}. |  | ||||||
|  */ |  | ||||||
| public class MessageDetailActivity extends DetailActivity { |  | ||||||
|     private Label label; |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|  |  | ||||||
|         // savedInstanceState is non-null when there is fragment state |  | ||||||
|         // saved from previous configurations of this activity |  | ||||||
|         // (e.g. when rotating the screen from portrait to landscape). |  | ||||||
|         // In this case, the fragment will automatically be re-added |  | ||||||
|         // to its container so we don't need to manually add it. |  | ||||||
|         // For more information, see the Fragments API guide at: |  | ||||||
|         // |  | ||||||
|         // http://developer.android.com/guide/components/fragments.html |  | ||||||
|         // |  | ||||||
|         if (savedInstanceState == null) { |  | ||||||
|             label = (Label) getIntent().getSerializableExtra(MainActivity.EXTRA_SHOW_LABEL); |  | ||||||
|             // Create the detail fragment and add it to the activity |  | ||||||
|             // using a fragment transaction. |  | ||||||
|             Bundle arguments = new Bundle(); |  | ||||||
|             arguments.putSerializable(MessageDetailFragment.ARG_ITEM, |  | ||||||
|                 getIntent().getSerializableExtra(MessageDetailFragment.ARG_ITEM)); |  | ||||||
|             MessageDetailFragment fragment = new MessageDetailFragment(); |  | ||||||
|             fragment.setArguments(arguments); |  | ||||||
|             getSupportFragmentManager().beginTransaction() |  | ||||||
|                 .add(R.id.content, fragment) |  | ||||||
|                 .commit(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public boolean onOptionsItemSelected(MenuItem item) { |  | ||||||
|         switch (item.getItemId()) { |  | ||||||
|             case android.R.id.home: |  | ||||||
|                 Intent parentIntent = new Intent(this, MainActivity.class); |  | ||||||
|                 parentIntent.putExtra(MainActivity.EXTRA_SHOW_LABEL, label); |  | ||||||
|                 NavUtils.navigateUpTo(this, parentIntent); |  | ||||||
|                 return true; |  | ||||||
|             default: |  | ||||||
|                 return super.onOptionsItemSelected(item); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | package ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v4.app.NavUtils | ||||||
|  | import android.view.MenuItem | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * An activity representing a single Message detail screen. This | ||||||
|  |  * activity is only used on handset devices. On tablet-size devices, | ||||||
|  |  * item details are presented side-by-side with a list of items | ||||||
|  |  * in a [MainActivity]. | ||||||
|  |  * | ||||||
|  |  * This activity is mostly just a 'shell' activity containing nothing | ||||||
|  |  * more than a [MessageDetailFragment]. | ||||||
|  |  */ | ||||||
|  | class MessageDetailActivity : DetailActivity() { | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |  | ||||||
|  |         // savedInstanceState is non-null when there is fragment state | ||||||
|  |         // saved from previous configurations of this activity | ||||||
|  |         // (e.g. when rotating the screen from portrait to landscape). | ||||||
|  |         // In this case, the fragment will automatically be re-added | ||||||
|  |         // to its container so we don't need to manually add it. | ||||||
|  |         // For more information, see the Fragments API guide at: | ||||||
|  |         // | ||||||
|  |         // http://developer.android.com/guide/components/fragments.html | ||||||
|  |         // | ||||||
|  |         if (savedInstanceState == null) { | ||||||
|  |             // Create the detail fragment and add it to the activity | ||||||
|  |             // using a fragment transaction. | ||||||
|  |             val arguments = Bundle() | ||||||
|  |             arguments.putSerializable(MessageDetailFragment.ARG_ITEM, | ||||||
|  |                     intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM)) | ||||||
|  |             val fragment = MessageDetailFragment() | ||||||
|  |             fragment.arguments = arguments | ||||||
|  |             supportFragmentManager.beginTransaction() | ||||||
|  |                     .add(R.id.content, fragment) | ||||||
|  |                     .commit() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { | ||||||
|  |         android.R.id.home -> { | ||||||
|  |             NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java)) | ||||||
|  |             true | ||||||
|  |         } | ||||||
|  |         else -> super.onOptionsItemSelected(item) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,367 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.annotation.IdRes; |  | ||||||
| import android.support.v4.app.Fragment; |  | ||||||
| import android.support.v7.widget.GridLayoutManager; |  | ||||||
| import android.support.v7.widget.LinearLayoutManager; |  | ||||||
| import android.support.v7.widget.RecyclerView; |  | ||||||
| import android.text.util.Linkify; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.Menu; |  | ||||||
| import android.view.MenuInflater; |  | ||||||
| import android.view.MenuItem; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.TextView; |  | ||||||
|  |  | ||||||
| import com.mikepenz.google_material_typeface_library.GoogleMaterial; |  | ||||||
| import com.mikepenz.iconics.view.IconicsImageView; |  | ||||||
|  |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Iterator; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Set; |  | ||||||
| import java.util.regex.Matcher; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.listener.ActionBarListener; |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.apps.abit.util.Assets; |  | ||||||
| import ch.dissem.apps.abit.util.Drawables; |  | ||||||
| import ch.dissem.apps.abit.util.Labels; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.InventoryVector; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.Label; |  | ||||||
| import ch.dissem.bitmessage.ports.MessageRepository; |  | ||||||
|  |  | ||||||
| import static android.text.util.Linkify.WEB_URLS; |  | ||||||
| import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN; |  | ||||||
| import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA; |  | ||||||
| import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A fragment representing a single Message detail screen. |  | ||||||
|  * This fragment is either contained in a {@link MainActivity} |  | ||||||
|  * in two-pane mode (on tablets) or a {@link MessageDetailActivity} |  | ||||||
|  * on handsets. |  | ||||||
|  */ |  | ||||||
| public class MessageDetailFragment extends Fragment { |  | ||||||
|     /** |  | ||||||
|      * The fragment argument representing the item ID that this fragment |  | ||||||
|      * represents. |  | ||||||
|      */ |  | ||||||
|     public static final String ARG_ITEM = "item"; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * The content this fragment is presenting. |  | ||||||
|      */ |  | ||||||
|     private Plaintext item; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Mandatory empty constructor for the fragment manager to instantiate the |  | ||||||
|      * fragment (e.g. upon screen orientation changes). |  | ||||||
|      */ |  | ||||||
|     public MessageDetailFragment() { |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|  |  | ||||||
|         if (getArguments().containsKey(ARG_ITEM)) { |  | ||||||
|             // Load the dummy content specified by the fragment |  | ||||||
|             // arguments. In a real-world scenario, use a Loader |  | ||||||
|             // to load content from a content provider. |  | ||||||
|             item = (Plaintext) getArguments().getSerializable(ARG_ITEM); |  | ||||||
|         } |  | ||||||
|         setHasOptionsMenu(true); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, |  | ||||||
|                              Bundle savedInstanceState) { |  | ||||||
|         View rootView = inflater.inflate(R.layout.fragment_message_detail, container, false); |  | ||||||
|  |  | ||||||
|         // Show the dummy content as text in a TextView. |  | ||||||
|         if (item != null) { |  | ||||||
|             ((TextView) rootView.findViewById(R.id.subject)).setText(item.getSubject()); |  | ||||||
|             ImageView status = (ImageView) rootView.findViewById(R.id.status); |  | ||||||
|             status.setImageResource(Assets.getStatusDrawable(item.getStatus())); |  | ||||||
|             status.setContentDescription(getString(Assets.getStatusString(item.getStatus()))); |  | ||||||
|             BitmessageAddress sender = item.getFrom(); |  | ||||||
|             ((ImageView) rootView.findViewById(R.id.avatar)) |  | ||||||
|                 .setImageDrawable(new Identicon(sender)); |  | ||||||
|             ((TextView) rootView.findViewById(R.id.sender)).setText(sender.toString()); |  | ||||||
|             if (item.getTo() != null) { |  | ||||||
|                 ((TextView) rootView.findViewById(R.id.recipient)).setText(item.getTo().toString()); |  | ||||||
|             } else if (item.getType() == Plaintext.Type.BROADCAST) { |  | ||||||
|                 ((TextView) rootView.findViewById(R.id.recipient)).setText(R.string.broadcast); |  | ||||||
|             } |  | ||||||
|             RecyclerView labelView = (RecyclerView) rootView.findViewById(R.id.labels); |  | ||||||
|             LabelAdapter labelAdapter = new LabelAdapter(getActivity(), item.getLabels()); |  | ||||||
|             labelView.setAdapter(labelAdapter); |  | ||||||
|             labelView.setLayoutManager(new GridLayoutManager(getActivity(), 2)); |  | ||||||
|  |  | ||||||
|             TextView messageBody = (TextView) rootView.findViewById(R.id.text); |  | ||||||
|             messageBody.setText(item.getText()); |  | ||||||
|  |  | ||||||
|             Linkify.addLinks(messageBody, WEB_URLS); |  | ||||||
|             Linkify.addLinks(messageBody, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null, |  | ||||||
|                 new Linkify.TransformFilter() { |  | ||||||
|                     @Override |  | ||||||
|                     public String transformUrl(Matcher match, String url) { |  | ||||||
|                         return match.group(); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             ); |  | ||||||
|  |  | ||||||
|             messageBody.setLinksClickable(true); |  | ||||||
|             messageBody.setTextIsSelectable(true); |  | ||||||
|  |  | ||||||
|             boolean removed = false; |  | ||||||
|             Iterator<Label> labels = item.getLabels().iterator(); |  | ||||||
|             while (labels.hasNext()) { |  | ||||||
|                 if (labels.next().getType() == Label.Type.UNREAD) { |  | ||||||
|                     labels.remove(); |  | ||||||
|                     removed = true; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             MessageRepository messageRepo = Singleton.getMessageRepository(inflater.getContext()); |  | ||||||
|             if (removed) { |  | ||||||
|                 if (getActivity() instanceof ActionBarListener) { |  | ||||||
|                     ((ActionBarListener) getActivity()).updateUnread(); |  | ||||||
|                 } |  | ||||||
|                 messageRepo.save(item); |  | ||||||
|             } |  | ||||||
|             List<Plaintext> parents = new ArrayList<>(item.getParents().size()); |  | ||||||
|             for (InventoryVector parentIV : item.getParents()) { |  | ||||||
|                 Plaintext parent = messageRepo.getMessage(parentIV); |  | ||||||
|                 if (parent != null) { |  | ||||||
|                     parents.add(parent); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             showRelatedMessages(rootView, R.id.parents, parents); |  | ||||||
|             showRelatedMessages(rootView, R.id.responses, messageRepo.findResponses(item)); |  | ||||||
|         } |  | ||||||
|         return rootView; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void showRelatedMessages(View rootView, @IdRes int id, List<Plaintext> messages) { |  | ||||||
|         RecyclerView recyclerView = (RecyclerView) rootView.findViewById(id); |  | ||||||
|         RelatedMessageAdapter adapter = new RelatedMessageAdapter(getActivity(), messages); |  | ||||||
|         recyclerView.setAdapter(adapter); |  | ||||||
|         recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |  | ||||||
|         inflater.inflate(R.menu.message, menu); |  | ||||||
|  |  | ||||||
|         Drawables.addIcon(getActivity(), menu, R.id.reply, GoogleMaterial.Icon.gmd_reply); |  | ||||||
|         Drawables.addIcon(getActivity(), menu, R.id.delete, GoogleMaterial.Icon.gmd_delete); |  | ||||||
|         Drawables.addIcon(getActivity(), menu, R.id.mark_unread, GoogleMaterial.Icon |  | ||||||
|             .gmd_markunread); |  | ||||||
|         Drawables.addIcon(getActivity(), menu, R.id.archive, GoogleMaterial.Icon.gmd_archive); |  | ||||||
|  |  | ||||||
|         super.onCreateOptionsMenu(menu, inflater); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public boolean onOptionsItemSelected(MenuItem menuItem) { |  | ||||||
|         MessageRepository messageRepo = Singleton.getMessageRepository(getContext()); |  | ||||||
|         switch (menuItem.getItemId()) { |  | ||||||
|             case R.id.reply: |  | ||||||
|                 ComposeMessageActivity.launchReplyTo(this, item); |  | ||||||
|                 return true; |  | ||||||
|             case R.id.delete: |  | ||||||
|                 if (isInTrash(item)) { |  | ||||||
|                     messageRepo.remove(item); |  | ||||||
|                 } else { |  | ||||||
|                     item.getLabels().clear(); |  | ||||||
|                     item.addLabels(messageRepo.getLabels(Label.Type.TRASH)); |  | ||||||
|                     messageRepo.save(item); |  | ||||||
|                 } |  | ||||||
|                 getActivity().onBackPressed(); |  | ||||||
|                 return true; |  | ||||||
|             case R.id.mark_unread: |  | ||||||
|                 item.addLabels(messageRepo.getLabels(Label.Type.UNREAD)); |  | ||||||
|                 messageRepo.save(item); |  | ||||||
|                 if (getActivity() instanceof ActionBarListener) { |  | ||||||
|                     ((ActionBarListener) getActivity()).updateUnread(); |  | ||||||
|                 } |  | ||||||
|                 return true; |  | ||||||
|             case R.id.archive: |  | ||||||
|                 if (item.isUnread() && getActivity() instanceof ActionBarListener) { |  | ||||||
|                     ((ActionBarListener) getActivity()).updateUnread(); |  | ||||||
|                 } |  | ||||||
|                 item.getLabels().clear(); |  | ||||||
|                 messageRepo.save(item); |  | ||||||
|                 return true; |  | ||||||
|             default: |  | ||||||
|                 return false; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static boolean isInTrash(Plaintext item) { |  | ||||||
|         for (Label label : item.getLabels()) { |  | ||||||
|             if (label.getType() == Label.Type.TRASH) { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static class RelatedMessageAdapter extends RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder> { |  | ||||||
|         private final List<Plaintext> messages; |  | ||||||
|         private final Context ctx; |  | ||||||
|  |  | ||||||
|         private RelatedMessageAdapter(Context ctx, List<Plaintext> messages) { |  | ||||||
|             this.messages = messages; |  | ||||||
|             this.ctx = ctx; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         public RelatedMessageAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |  | ||||||
|             Context context = parent.getContext(); |  | ||||||
|             LayoutInflater inflater = LayoutInflater.from(context); |  | ||||||
|  |  | ||||||
|             // Inflate the custom layout |  | ||||||
|             View contactView = inflater.inflate(R.layout.item_message_minimized, parent, false); |  | ||||||
|  |  | ||||||
|             // Return a new holder instance |  | ||||||
|             return new RelatedMessageAdapter.ViewHolder(contactView); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Involves populating data into the item through holder |  | ||||||
|         @Override |  | ||||||
|         public void onBindViewHolder(RelatedMessageAdapter.ViewHolder viewHolder, int position) { |  | ||||||
|             // Get the data model based on position |  | ||||||
|             Plaintext message = messages.get(position); |  | ||||||
|  |  | ||||||
|             viewHolder.avatar.setImageDrawable(new Identicon(message.getFrom())); |  | ||||||
|             viewHolder.status.setImageResource(Assets.getStatusDrawable(message.getStatus())); |  | ||||||
|             viewHolder.sender.setText(message.getFrom().toString()); |  | ||||||
|             viewHolder.extract.setText(normalizeWhitespaces(message.getText())); |  | ||||||
|             viewHolder.item = message; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Returns the total count of items in the list |  | ||||||
|         @Override |  | ||||||
|         public int getItemCount() { |  | ||||||
|             return messages.size(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         class ViewHolder extends RecyclerView.ViewHolder { |  | ||||||
|             private final ImageView avatar; |  | ||||||
|             private final ImageView status; |  | ||||||
|             private final TextView sender; |  | ||||||
|             private final TextView extract; |  | ||||||
|             private Plaintext item; |  | ||||||
|  |  | ||||||
|             ViewHolder(final View itemView) { |  | ||||||
|                 super(itemView); |  | ||||||
|                 avatar = (ImageView) itemView.findViewById(R.id.avatar); |  | ||||||
|                 status = (ImageView) itemView.findViewById(R.id.status); |  | ||||||
|                 sender = (TextView) itemView.findViewById(R.id.sender); |  | ||||||
|                 extract = (TextView) itemView.findViewById(R.id.text); |  | ||||||
|                 itemView.setOnClickListener(new View.OnClickListener() { |  | ||||||
|                     @Override |  | ||||||
|                     public void onClick(View v) { |  | ||||||
|                         if (ctx instanceof MainActivity) { |  | ||||||
|                             ((MainActivity) ctx).onItemSelected(item); |  | ||||||
|                         } else { |  | ||||||
|                             Intent detailIntent; |  | ||||||
|                             detailIntent = new Intent(ctx, MessageDetailActivity.class); |  | ||||||
|                             detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item); |  | ||||||
|                             ctx.startActivity(detailIntent); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static class LabelAdapter extends |  | ||||||
|         RecyclerView.Adapter<LabelAdapter.ViewHolder> { |  | ||||||
|  |  | ||||||
|         private final List<Label> labels; |  | ||||||
|         private final Context ctx; |  | ||||||
|  |  | ||||||
|         private LabelAdapter(Context ctx, Set<Label> labels) { |  | ||||||
|             this.labels = new ArrayList<>(labels); |  | ||||||
|             this.ctx = ctx; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         public LabelAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |  | ||||||
|             Context context = parent.getContext(); |  | ||||||
|             LayoutInflater inflater = LayoutInflater.from(context); |  | ||||||
|  |  | ||||||
|             // Inflate the custom layout |  | ||||||
|             View contactView = inflater.inflate(R.layout.item_label, parent, false); |  | ||||||
|  |  | ||||||
|             // Return a new holder instance |  | ||||||
|             return new ViewHolder(contactView); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Involves populating data into the item through holder |  | ||||||
|         @Override |  | ||||||
|         public void onBindViewHolder(LabelAdapter.ViewHolder viewHolder, int position) { |  | ||||||
|             // Get the data model based on position |  | ||||||
|             Label label = labels.get(position); |  | ||||||
|  |  | ||||||
|             viewHolder.icon.setColor(Labels.getColor(label)); |  | ||||||
|             viewHolder.icon.setIcon(Labels.getIcon(label)); |  | ||||||
|             viewHolder.label.setText(Labels.getText(label, ctx)); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Returns the total count of items in the list |  | ||||||
|         @Override |  | ||||||
|         public int getItemCount() { |  | ||||||
|             return labels.size(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // Provide a direct reference to each of the views within a data item |  | ||||||
|         // Used to cache the views within the item layout for fast access |  | ||||||
|         static class ViewHolder extends RecyclerView.ViewHolder { |  | ||||||
|             // Your holder should contain a member variable |  | ||||||
|             // for any view that will be set as you render a row |  | ||||||
|             public IconicsImageView icon; |  | ||||||
|             public TextView label; |  | ||||||
|  |  | ||||||
|             // We also create a constructor that accepts the entire item row |  | ||||||
|             // and does the view lookups to find each subview |  | ||||||
|             ViewHolder(View itemView) { |  | ||||||
|                 // Stores the itemView in a public final member variable that can be used |  | ||||||
|                 // to access the context from any ViewHolder instance. |  | ||||||
|                 super(itemView); |  | ||||||
|  |  | ||||||
|                 icon = (IconicsImageView) itemView.findViewById(R.id.icon); |  | ||||||
|                 label = (TextView) itemView.findViewById(R.id.label); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										281
									
								
								app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										281
									
								
								app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,281 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.annotation.IdRes | ||||||
|  | import android.support.v4.app.Fragment | ||||||
|  | import android.support.v7.widget.GridLayoutManager | ||||||
|  | import android.support.v7.widget.LinearLayoutManager | ||||||
|  | import android.support.v7.widget.RecyclerView | ||||||
|  | import android.text.util.Linkify | ||||||
|  | import android.text.util.Linkify.WEB_URLS | ||||||
|  | import android.view.* | ||||||
|  | import android.widget.ImageView | ||||||
|  | import android.widget.TextView | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.apps.abit.util.Assets | ||||||
|  | import ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN | ||||||
|  | import ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA | ||||||
|  | import ch.dissem.apps.abit.util.Drawables | ||||||
|  | import ch.dissem.apps.abit.util.Labels | ||||||
|  | import ch.dissem.apps.abit.util.Strings.prepareMessageExtract | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.Label | ||||||
|  | import com.mikepenz.google_material_typeface_library.GoogleMaterial | ||||||
|  | import com.mikepenz.iconics.view.IconicsImageView | ||||||
|  | import kotlinx.android.synthetic.main.fragment_message_detail.* | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A fragment representing a single Message detail screen. | ||||||
|  |  * This fragment is either contained in a [MainActivity] | ||||||
|  |  * in two-pane mode (on tablets) or a [MessageDetailActivity] | ||||||
|  |  * on handsets. | ||||||
|  |  */ | ||||||
|  | class MessageDetailFragment : Fragment() { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The content this fragment is presenting. | ||||||
|  |      */ | ||||||
|  |     private var item: Plaintext? = null | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |  | ||||||
|  |         arguments?.let { arguments -> | ||||||
|  |             if (arguments.containsKey(ARG_ITEM)) { | ||||||
|  |                 // Load the dummy content specified by the fragment | ||||||
|  |                 // arguments. In a real-world scenario, use a Loader | ||||||
|  |                 // to load content from a content provider. | ||||||
|  |                 item = arguments.getSerializable(ARG_ITEM) as Plaintext | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         setHasOptionsMenu(true) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = | ||||||
|  |         inflater.inflate(R.layout.fragment_message_detail, container, false) | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|  |         val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity") | ||||||
|  |  | ||||||
|  |         // Show the dummy content as text in a TextView. | ||||||
|  |         item?.let { item -> | ||||||
|  |             subject.text = item.subject | ||||||
|  |             status.setImageResource(Assets.getStatusDrawable(item.status)) | ||||||
|  |             status.contentDescription = getString(Assets.getStatusString(item.status)) | ||||||
|  |             avatar.setImageDrawable(Identicon(item.from)) | ||||||
|  |             sender.text = item.from.toString() | ||||||
|  |             item.to?.let { to -> | ||||||
|  |                 recipient.text = to.toString() | ||||||
|  |             } ?: { | ||||||
|  |                 if (item.type == Plaintext.Type.BROADCAST) { | ||||||
|  |                     recipient.setText(R.string.broadcast) | ||||||
|  |                 } | ||||||
|  |             }.invoke() | ||||||
|  |             val labelAdapter = LabelAdapter(ctx, item.labels) | ||||||
|  |             labels.adapter = labelAdapter | ||||||
|  |             labels.layoutManager = GridLayoutManager(activity, 2) | ||||||
|  |  | ||||||
|  |             text.text = item.text | ||||||
|  |  | ||||||
|  |             Linkify.addLinks(text, WEB_URLS) | ||||||
|  |             Linkify.addLinks(text, BITMESSAGE_ADDRESS_PATTERN, BITMESSAGE_URL_SCHEMA, null, | ||||||
|  |                 Linkify.TransformFilter { match, _ -> match.group() } | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             text.linksClickable = true | ||||||
|  |             text.setTextIsSelectable(true) | ||||||
|  |  | ||||||
|  |             val messageRepo = Singleton.getMessageRepository(ctx) | ||||||
|  |             if (item.isUnread()) { | ||||||
|  |                 Singleton.labeler.markAsRead(item) | ||||||
|  |                 (activity as? MainActivity)?.updateUnread() | ||||||
|  |                 messageRepo.save(item) | ||||||
|  |             } | ||||||
|  |             val parents = ArrayList<Plaintext>(item.parents.size) | ||||||
|  |             for (parentIV in item.parents) { | ||||||
|  |                 val parent = messageRepo.getMessage(parentIV) | ||||||
|  |                 if (parent != null) { | ||||||
|  |                     parents.add(parent) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             showRelatedMessages(ctx, view, R.id.parents, parents) | ||||||
|  |             showRelatedMessages(ctx, view, R.id.responses, messageRepo.findResponses(item)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun showRelatedMessages(ctx: Context, rootView: View, @IdRes id: Int, messages: List<Plaintext>) { | ||||||
|  |         val recyclerView = rootView.findViewById<RecyclerView>(id) | ||||||
|  |         val adapter = RelatedMessageAdapter(ctx, messages) | ||||||
|  |         recyclerView.adapter = adapter | ||||||
|  |         recyclerView.layoutManager = LinearLayoutManager(activity) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||||
|  |         inflater.inflate(R.menu.message, menu) | ||||||
|  |         activity?.let { activity -> | ||||||
|  |             Drawables.addIcon(activity, menu, R.id.reply, GoogleMaterial.Icon.gmd_reply) | ||||||
|  |             Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete) | ||||||
|  |             Drawables.addIcon(activity, menu, R.id.mark_unread, GoogleMaterial.Icon | ||||||
|  |                 .gmd_markunread) | ||||||
|  |             Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         super.onCreateOptionsMenu(menu, inflater) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onOptionsItemSelected(menuItem: MenuItem): Boolean { | ||||||
|  |         val messageRepo = Singleton.getMessageRepository( | ||||||
|  |             context ?: throw IllegalStateException("No context available") | ||||||
|  |         ) | ||||||
|  |         item?.let { item -> | ||||||
|  |             when (menuItem.itemId) { | ||||||
|  |                 R.id.reply -> { | ||||||
|  |                     ComposeMessageActivity.launchReplyTo(this, item) | ||||||
|  |                     return true | ||||||
|  |                 } | ||||||
|  |                 R.id.delete -> { | ||||||
|  |                     if (isInTrash(item)) { | ||||||
|  |                         Singleton.labeler.delete(item) | ||||||
|  |                         messageRepo.remove(item) | ||||||
|  |                     } else { | ||||||
|  |                         Singleton.labeler.delete(item) | ||||||
|  |                         messageRepo.save(item) | ||||||
|  |                     } | ||||||
|  |                     (activity as? MainActivity)?.updateUnread() | ||||||
|  |                     activity?.onBackPressed() | ||||||
|  |                     return true | ||||||
|  |                 } | ||||||
|  |                 R.id.mark_unread -> { | ||||||
|  |                     Singleton.labeler.markAsUnread(item) | ||||||
|  |                     messageRepo.save(item) | ||||||
|  |                     (activity as? MainActivity)?.updateUnread() | ||||||
|  |                     return true | ||||||
|  |                 } | ||||||
|  |                 R.id.archive -> { | ||||||
|  |                     if (item.isUnread() && activity is MainActivity) { | ||||||
|  |                         (activity as MainActivity).updateUnread() | ||||||
|  |                     } | ||||||
|  |                     Singleton.labeler.archive(item) | ||||||
|  |                     messageRepo.save(item) | ||||||
|  |                     (activity as? MainActivity)?.updateUnread() | ||||||
|  |                     return true | ||||||
|  |                 } | ||||||
|  |                 else -> return false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private class RelatedMessageAdapter internal constructor(private val ctx: Context, private val messages: List<Plaintext>) : RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder>() { | ||||||
|  |  | ||||||
|  |         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RelatedMessageAdapter.ViewHolder { | ||||||
|  |             val context = parent.context | ||||||
|  |             val inflater = LayoutInflater.from(context) | ||||||
|  |  | ||||||
|  |             // Inflate the custom layout | ||||||
|  |             val contactView = inflater.inflate(R.layout.item_message_minimized, parent, false) | ||||||
|  |  | ||||||
|  |             // Return a new holder instance | ||||||
|  |             return ViewHolder(contactView) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Involves populating data into the item through holder | ||||||
|  |         override fun onBindViewHolder(viewHolder: RelatedMessageAdapter.ViewHolder, position: Int) { | ||||||
|  |             // Get the data model based on position | ||||||
|  |             val message = messages[position] | ||||||
|  |  | ||||||
|  |             viewHolder.avatar.setImageDrawable(Identicon(message.from)) | ||||||
|  |             viewHolder.status.setImageResource(Assets.getStatusDrawable(message.status)) | ||||||
|  |             viewHolder.sender.text = message.from.toString() | ||||||
|  |             viewHolder.extract.text = prepareMessageExtract(message.text) | ||||||
|  |             viewHolder.item = message | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Returns the total count of items in the list | ||||||
|  |         override fun getItemCount() = messages.size | ||||||
|  |  | ||||||
|  |         internal inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { | ||||||
|  |             internal val avatar = itemView.findViewById<ImageView>(R.id.avatar) | ||||||
|  |             internal val status = itemView.findViewById<ImageView>(R.id.status) | ||||||
|  |             internal val sender = itemView.findViewById<TextView>(R.id.sender) | ||||||
|  |             internal val extract = itemView.findViewById<TextView>(R.id.text) | ||||||
|  |             internal var item: Plaintext? = null | ||||||
|  |  | ||||||
|  |             init { | ||||||
|  |                 itemView.setOnClickListener { | ||||||
|  |                     if (ctx is MainActivity) { | ||||||
|  |                         item?.let { ctx.onItemSelected(it) } | ||||||
|  |                     } else { | ||||||
|  |                         val detailIntent = Intent(ctx, MessageDetailActivity::class.java) | ||||||
|  |                         detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item) | ||||||
|  |                         ctx.startActivity(detailIntent) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private class LabelAdapter internal constructor(private val ctx: Context, labels: Set<Label>) : RecyclerView.Adapter<LabelAdapter.ViewHolder>() { | ||||||
|  |  | ||||||
|  |         private val labels = labels.toMutableList() | ||||||
|  |  | ||||||
|  |         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder { | ||||||
|  |             val context = parent.context | ||||||
|  |             val inflater = LayoutInflater.from(context) | ||||||
|  |  | ||||||
|  |             // Inflate the custom layout | ||||||
|  |             val contactView = inflater.inflate(R.layout.item_label, parent, false) | ||||||
|  |  | ||||||
|  |             // Return a new holder instance | ||||||
|  |             return ViewHolder(contactView) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Involves populating data into the item through holder | ||||||
|  |         override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) { | ||||||
|  |             // Get the data model based on position | ||||||
|  |             val label = labels[position] | ||||||
|  |  | ||||||
|  |             viewHolder.icon.icon?.color(Labels.getColor(label)) | ||||||
|  |             viewHolder.icon.icon?.icon(Labels.getIcon(label)) | ||||||
|  |             viewHolder.label.text = Labels.getText(label, ctx) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override fun getItemCount() = labels.size | ||||||
|  |  | ||||||
|  |         internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { | ||||||
|  |             var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!! | ||||||
|  |             var label = itemView.findViewById<TextView>(R.id.label)!! | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * The fragment argument representing the item ID that this fragment | ||||||
|  |          * represents. | ||||||
|  |          */ | ||||||
|  |         val ARG_ITEM = "item" | ||||||
|  |  | ||||||
|  |         fun isInTrash(item: Plaintext?) = item?.labels?.any { it.type == Label.Type.TRASH } == true | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,336 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.AsyncTask; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.v4.app.Fragment; |  | ||||||
| import android.support.v4.content.ContextCompat; |  | ||||||
| import android.support.v7.widget.LinearLayoutManager; |  | ||||||
| import android.support.v7.widget.RecyclerView; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.Menu; |  | ||||||
| import android.view.MenuInflater; |  | ||||||
| import android.view.MenuItem; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils; |  | ||||||
|  |  | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Objects; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter; |  | ||||||
| import ch.dissem.apps.abit.listener.ActionBarListener; |  | ||||||
| import ch.dissem.apps.abit.listener.ListSelectionListener; |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.Label; |  | ||||||
| import ch.dissem.bitmessage.ports.MessageRepository; |  | ||||||
| import io.github.yavski.fabspeeddial.FabSpeedDial; |  | ||||||
| import io.github.yavski.fabspeeddial.SimpleMenuListenerAdapter; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_BROADCAST; |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_IDENTITY; |  | ||||||
| import static ch.dissem.apps.abit.MessageDetailFragment.isInTrash; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A list fragment representing a list of Messages. This fragment |  | ||||||
|  * also supports tablet devices by allowing list items to be given an |  | ||||||
|  * 'activated' state upon selection. This helps indicate which item is |  | ||||||
|  * currently being viewed in a {@link MessageDetailFragment}. |  | ||||||
|  * <p/> |  | ||||||
|  * Activities containing this fragment MUST implement the {@link ListSelectionListener} |  | ||||||
|  * interface. |  | ||||||
|  */ |  | ||||||
| public class MessageListFragment extends Fragment implements ListHolder { |  | ||||||
|  |  | ||||||
|     private RecyclerView recyclerView; |  | ||||||
|     private RecyclerView.LayoutManager layoutManager; |  | ||||||
|     private SwipeableMessageAdapter adapter; |  | ||||||
|     private RecyclerView.Adapter wrappedAdapter; |  | ||||||
|     private RecyclerViewSwipeManager recyclerViewSwipeManager; |  | ||||||
|     private RecyclerViewTouchActionGuardManager recyclerViewTouchActionGuardManager; |  | ||||||
|  |  | ||||||
|     private Label currentLabel; |  | ||||||
|     private MenuItem emptyTrashMenuItem; |  | ||||||
|     private MessageRepository messageRepo; |  | ||||||
|     private boolean activateOnItemClick; |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Mandatory empty constructor for the fragment manager to instantiate the |  | ||||||
|      * fragment (e.g. upon screen orientation changes). |  | ||||||
|      */ |  | ||||||
|     public MessageListFragment() { |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|  |  | ||||||
|         setHasOptionsMenu(true); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onResume() { |  | ||||||
|         super.onResume(); |  | ||||||
|         MainActivity activity = (MainActivity) getActivity(); |  | ||||||
|         messageRepo = Singleton.getMessageRepository(activity); |  | ||||||
|  |  | ||||||
|         doUpdateList(activity.getSelectedLabel()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void updateList(Label label) { |  | ||||||
|         if (!isResumed()) { |  | ||||||
|             currentLabel = label; |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!Objects.equals(currentLabel, label)) { |  | ||||||
|             adapter.setData(label, Collections.<Plaintext>emptyList()); |  | ||||||
|             adapter.notifyDataSetChanged(); |  | ||||||
|         } |  | ||||||
|         doUpdateList(label); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void doUpdateList(final Label label) { |  | ||||||
|         if (label == null) { |  | ||||||
|             if (getActivity() instanceof ActionBarListener) { |  | ||||||
|                 ((ActionBarListener) getActivity()).updateTitle(getString(R.string.app_name)); |  | ||||||
|             } |  | ||||||
|             adapter.setData(null, Collections.<Plaintext>emptyList()); |  | ||||||
|             adapter.notifyDataSetChanged(); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         currentLabel = label; |  | ||||||
|         if (emptyTrashMenuItem != null) { |  | ||||||
|             emptyTrashMenuItem.setVisible(label.getType() == Label.Type.TRASH); |  | ||||||
|         } |  | ||||||
|         if (getActivity() instanceof ActionBarListener) { |  | ||||||
|             ActionBarListener actionBarListener = (ActionBarListener) getActivity(); |  | ||||||
|             if ("archive".equals(label.toString())) { |  | ||||||
|                 actionBarListener.updateTitle(getString(R.string.archive)); |  | ||||||
|             } else { |  | ||||||
|                 actionBarListener.updateTitle(label.toString()); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         new AsyncTask<Void, Void, List<Plaintext>>() { |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             protected List<Plaintext> doInBackground(Void... params) { |  | ||||||
|                 return messageRepo.findMessages(label); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             protected void onPostExecute(List<Plaintext> messages) { |  | ||||||
|                 if (adapter != null) { |  | ||||||
|                     adapter.setData(label, messages); |  | ||||||
|                     adapter.notifyDataSetChanged(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }.execute(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle |  | ||||||
|         savedInstanceState) { |  | ||||||
|         View rootView = inflater.inflate(R.layout.fragment_message_list, container, false); |  | ||||||
|         recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view); |  | ||||||
|         layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); |  | ||||||
|  |  | ||||||
|         // Show the dummy content as text in a TextView. |  | ||||||
|         FabSpeedDial fab = (FabSpeedDial) rootView.findViewById(R.id |  | ||||||
|             .fab_compose_message); |  | ||||||
|         fab.setMenuListener(new SimpleMenuListenerAdapter() { |  | ||||||
|             @Override |  | ||||||
|             public boolean onMenuItemSelected(MenuItem menuItem) { |  | ||||||
|                 BitmessageAddress identity = Singleton.getIdentity(getActivity()); |  | ||||||
|                 if (identity == null) { |  | ||||||
|                     Toast.makeText(getActivity(), R.string.no_identity_warning, |  | ||||||
|                         Toast.LENGTH_LONG).show(); |  | ||||||
|                     return false; |  | ||||||
|                 } else { |  | ||||||
|                     switch (menuItem.getItemId()) { |  | ||||||
|                         case R.id.action_compose_message: { |  | ||||||
|                             Intent intent = new Intent(getActivity(), ComposeMessageActivity.class); |  | ||||||
|                             intent.putExtra(EXTRA_IDENTITY, identity); |  | ||||||
|                             startActivity(intent); |  | ||||||
|                             return true; |  | ||||||
|                         } |  | ||||||
|                         case R.id.action_compose_broadcast: { |  | ||||||
|                             Intent intent = new Intent(getActivity(), ComposeMessageActivity.class); |  | ||||||
|                             intent.putExtra(EXTRA_IDENTITY, identity); |  | ||||||
|                             intent.putExtra(EXTRA_BROADCAST, true); |  | ||||||
|                             startActivity(intent); |  | ||||||
|                             return true; |  | ||||||
|                         } |  | ||||||
|                         default: |  | ||||||
|                             return false; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         // touch guard manager  (this class is required to suppress scrolling while swipe-dismiss |  | ||||||
|         // animation is running) |  | ||||||
|         recyclerViewTouchActionGuardManager = new RecyclerViewTouchActionGuardManager(); |  | ||||||
|         recyclerViewTouchActionGuardManager.setInterceptVerticalScrollingWhileAnimationRunning |  | ||||||
|             (true); |  | ||||||
|         recyclerViewTouchActionGuardManager.setEnabled(true); |  | ||||||
|  |  | ||||||
|         // swipe manager |  | ||||||
|         recyclerViewSwipeManager = new RecyclerViewSwipeManager(); |  | ||||||
|  |  | ||||||
|         //adapter |  | ||||||
|         adapter = new SwipeableMessageAdapter(); |  | ||||||
|         adapter.setActivateOnItemClick(activateOnItemClick); |  | ||||||
|         adapter.setEventListener(new SwipeableMessageAdapter.EventListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onItemDeleted(Plaintext item) { |  | ||||||
|                 if (isInTrash(item)) { |  | ||||||
|                     messageRepo.remove(item); |  | ||||||
|                 } else { |  | ||||||
|                     item.getLabels().clear(); |  | ||||||
|                     item.addLabels(messageRepo.getLabels(Label.Type.TRASH)); |  | ||||||
|                     messageRepo.save(item); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             public void onItemArchived(Plaintext item) { |  | ||||||
|                 item.getLabels().clear(); |  | ||||||
|                 messageRepo.save(item); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             @Override |  | ||||||
|             public void onItemViewClicked(View v) { |  | ||||||
|                 int position = recyclerView.getChildAdapterPosition(v); |  | ||||||
|                 adapter.setSelectedPosition(position); |  | ||||||
|                 if (position != RecyclerView.NO_POSITION) { |  | ||||||
|                     Plaintext item = adapter.getItem(position); |  | ||||||
|                     ((MainActivity) getActivity()).onItemSelected(item); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         // wrap for swiping |  | ||||||
|         wrappedAdapter = recyclerViewSwipeManager.createWrappedAdapter(adapter); |  | ||||||
|  |  | ||||||
|         final GeneralItemAnimator animator = new SwipeDismissItemAnimator(); |  | ||||||
|  |  | ||||||
|         // Change animations are enabled by default since support-v7-recyclerview v22. |  | ||||||
|         // Disable the change animation in order to make turning back animation of swiped item |  | ||||||
|         // works properly. |  | ||||||
|         animator.setSupportsChangeAnimations(false); |  | ||||||
|  |  | ||||||
|         recyclerView.setLayoutManager(layoutManager); |  | ||||||
|         recyclerView.setAdapter(wrappedAdapter);  // requires *wrapped* adapter |  | ||||||
|         recyclerView.setItemAnimator(animator); |  | ||||||
|  |  | ||||||
|         recyclerView.addItemDecoration(new SimpleListDividerDecorator( |  | ||||||
|             ContextCompat.getDrawable(getContext(), R.drawable.list_divider_h), true)); |  | ||||||
|  |  | ||||||
|         // NOTE: |  | ||||||
|         // The initialization order is very important! This order determines the priority of |  | ||||||
|         // touch event handling. |  | ||||||
|         // |  | ||||||
|         // priority: TouchActionGuard > Swipe > DragAndDrop |  | ||||||
|         recyclerViewTouchActionGuardManager.attachRecyclerView(recyclerView); |  | ||||||
|         recyclerViewSwipeManager.attachRecyclerView(recyclerView); |  | ||||||
|  |  | ||||||
|         return rootView; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onDestroyView() { |  | ||||||
|         if (recyclerViewSwipeManager != null) { |  | ||||||
|             recyclerViewSwipeManager.release(); |  | ||||||
|             recyclerViewSwipeManager = null; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (recyclerViewTouchActionGuardManager != null) { |  | ||||||
|             recyclerViewTouchActionGuardManager.release(); |  | ||||||
|             recyclerViewTouchActionGuardManager = null; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (recyclerView != null) { |  | ||||||
|             recyclerView.setItemAnimator(null); |  | ||||||
|             recyclerView.setAdapter(null); |  | ||||||
|             recyclerView = null; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (wrappedAdapter != null) { |  | ||||||
|             WrapperAdapterUtils.releaseAll(wrappedAdapter); |  | ||||||
|             wrappedAdapter = null; |  | ||||||
|         } |  | ||||||
|         adapter = null; |  | ||||||
|         layoutManager = null; |  | ||||||
|  |  | ||||||
|         super.onDestroyView(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |  | ||||||
|         inflater.inflate(R.menu.message_list, menu); |  | ||||||
|         emptyTrashMenuItem = menu.findItem(R.id.empty_trash); |  | ||||||
|         super.onCreateOptionsMenu(menu, inflater); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public boolean onOptionsItemSelected(MenuItem item) { |  | ||||||
|         switch (item.getItemId()) { |  | ||||||
|             case R.id.empty_trash: |  | ||||||
|                 if (currentLabel.getType() != Label.Type.TRASH) return true; |  | ||||||
|  |  | ||||||
|                 new AsyncTask<Void, Void, Void>() { |  | ||||||
|                     @Override |  | ||||||
|                     protected Void doInBackground(Void... params) { |  | ||||||
|                         for (Plaintext message : messageRepo.findMessages(currentLabel)) { |  | ||||||
|                             messageRepo.remove(message); |  | ||||||
|                         } |  | ||||||
|                         return null; |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     @Override |  | ||||||
|                     protected void onPostExecute(Void aVoid) { |  | ||||||
|                         updateList(currentLabel); |  | ||||||
|                     } |  | ||||||
|                 }.execute(); |  | ||||||
|                 return true; |  | ||||||
|             default: |  | ||||||
|                 return false; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void setActivateOnItemClick(boolean activateOnItemClick) { |  | ||||||
|         if (adapter != null) { |  | ||||||
|             adapter.setActivateOnItemClick(activateOnItemClick); |  | ||||||
|         } |  | ||||||
|         this.activateOnItemClick = activateOnItemClick; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										324
									
								
								app/src/main/java/ch/dissem/apps/abit/MessageListFragment.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								app/src/main/java/ch/dissem/apps/abit/MessageListFragment.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,324 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v4.app.Fragment | ||||||
|  | import android.support.v4.content.ContextCompat | ||||||
|  | import android.support.v7.widget.LinearLayoutManager | ||||||
|  | import android.support.v7.widget.RecyclerView | ||||||
|  | import android.support.v7.widget.RecyclerView.OnScrollListener | ||||||
|  | import android.view.* | ||||||
|  | import android.widget.Toast | ||||||
|  | import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST | ||||||
|  | import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY | ||||||
|  | import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter | ||||||
|  | import ch.dissem.apps.abit.listener.ListSelectionListener | ||||||
|  | import ch.dissem.apps.abit.repository.AndroidMessageRepository | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.apps.abit.service.Singleton.currentLabel | ||||||
|  | import ch.dissem.apps.abit.util.FabUtils | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.Label | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils | ||||||
|  | import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu | ||||||
|  | import kotlinx.android.synthetic.main.fragment_message_list.* | ||||||
|  | import org.jetbrains.anko.doAsync | ||||||
|  | import org.jetbrains.anko.support.v4.onUiThread | ||||||
|  | import org.jetbrains.anko.uiThread | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | private const val PAGE_SIZE = 15 | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A list fragment representing a list of Messages. This fragment | ||||||
|  |  * also supports tablet devices by allowing list items to be given an | ||||||
|  |  * 'activated' state upon selection. This helps indicate which item is | ||||||
|  |  * currently being viewed in a [MessageDetailFragment]. | ||||||
|  |  * | ||||||
|  |  * | ||||||
|  |  * Activities containing this fragment MUST implement the [ListSelectionListener] | ||||||
|  |  * interface. | ||||||
|  |  */ | ||||||
|  | class MessageListFragment : Fragment(), ListHolder<Label> { | ||||||
|  |  | ||||||
|  |     private var isLoading = false | ||||||
|  |     private var isLastPage = false | ||||||
|  |  | ||||||
|  |     private var layoutManager: LinearLayoutManager? = null | ||||||
|  |     private var swipeableMessageAdapter: SwipeableMessageAdapter? = null | ||||||
|  |     private var wrappedAdapter: RecyclerView.Adapter<*>? = null | ||||||
|  |     private var recyclerViewSwipeManager: RecyclerViewSwipeManager? = null | ||||||
|  |     private var recyclerViewTouchActionGuardManager: RecyclerViewTouchActionGuardManager? = null | ||||||
|  |  | ||||||
|  |     private val recyclerViewOnScrollListener = object : OnScrollListener() { | ||||||
|  |         override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) { | ||||||
|  |             layoutManager?.let { layoutManager -> | ||||||
|  |                 val visibleItemCount = layoutManager.childCount | ||||||
|  |                 val totalItemCount = layoutManager.itemCount | ||||||
|  |                 val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() | ||||||
|  |  | ||||||
|  |                 if (!isLoading && !isLastPage) { | ||||||
|  |                     if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5 | ||||||
|  |                         && firstVisibleItemPosition >= 0) { | ||||||
|  |                         loadMoreItems() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private var emptyTrashMenuItem: MenuItem? = null | ||||||
|  |     private lateinit var messageRepo: AndroidMessageRepository | ||||||
|  |     private var activateOnItemClick: Boolean = false | ||||||
|  |  | ||||||
|  |     private val backStack = Stack<Label>() | ||||||
|  |  | ||||||
|  |     fun loadMoreItems() { | ||||||
|  |         isLoading = true | ||||||
|  |         swipeableMessageAdapter?.let { messageAdapter -> | ||||||
|  |             doAsync { | ||||||
|  |                 val messages = messageRepo.findMessages(currentLabel.value, messageAdapter.itemCount, PAGE_SIZE) | ||||||
|  |                 onUiThread { | ||||||
|  |                     messageAdapter.addAll(messages) | ||||||
|  |                     isLoading = false | ||||||
|  |                     isLastPage = messages.size < PAGE_SIZE | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |  | ||||||
|  |         setHasOptionsMenu(true) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onResume() { | ||||||
|  |         super.onResume() | ||||||
|  |         val activity = activity as MainActivity | ||||||
|  |         initFab(activity) | ||||||
|  |         messageRepo = Singleton.getMessageRepository(activity) | ||||||
|  |  | ||||||
|  |         currentLabel.addObserver(this) { new -> doUpdateList(new) } | ||||||
|  |         doUpdateList(currentLabel.value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onPause() { | ||||||
|  |         currentLabel.removeObserver(this) | ||||||
|  |         super.onPause() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun doUpdateList(label: Label?) { | ||||||
|  |         val mainActivity = activity as? MainActivity | ||||||
|  |         swipeableMessageAdapter?.clear(label) | ||||||
|  |         if (label == null) { | ||||||
|  |             mainActivity?.updateTitle(getString(R.string.app_name)) | ||||||
|  |             swipeableMessageAdapter?.notifyDataSetChanged() | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH | ||||||
|  |         mainActivity?.apply { | ||||||
|  |             if ("archive" == label.toString()) { | ||||||
|  |                 updateTitle(getString(R.string.archive)) | ||||||
|  |             } else { | ||||||
|  |                 updateTitle(label.toString()) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         loadMoreItems() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = | ||||||
|  |         inflater.inflate(R.layout.fragment_message_list, container, false) | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|  |         val context = context ?: throw IllegalStateException("No context available") | ||||||
|  |  | ||||||
|  |         layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) | ||||||
|  |  | ||||||
|  |         // touch guard manager  (this class is required to suppress scrolling while swipe-dismiss | ||||||
|  |         // animation is running) | ||||||
|  |         val touchActionGuardManager = RecyclerViewTouchActionGuardManager().apply { | ||||||
|  |             setInterceptVerticalScrollingWhileAnimationRunning(true) | ||||||
|  |             isEnabled = true | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // swipe manager | ||||||
|  |         val swipeManager = RecyclerViewSwipeManager() | ||||||
|  |  | ||||||
|  |         //swipeableMessageAdapter | ||||||
|  |         val adapter = SwipeableMessageAdapter().apply { | ||||||
|  |             setActivateOnItemClick(activateOnItemClick) | ||||||
|  |         } | ||||||
|  |         adapter.eventListener = object : SwipeableMessageAdapter.EventListener { | ||||||
|  |             override fun onItemDeleted(item: Plaintext) { | ||||||
|  |                 if (MessageDetailFragment.isInTrash(item)) { | ||||||
|  |                     Singleton.labeler.delete(item) | ||||||
|  |                     messageRepo.remove(item) | ||||||
|  |                 } else { | ||||||
|  |                     Singleton.labeler.delete(item) | ||||||
|  |                     messageRepo.save(item) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             override fun onItemArchived(item: Plaintext) { | ||||||
|  |                 Singleton.labeler.archive(item) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             override fun onItemViewClicked(v: View?) { | ||||||
|  |                 val position = recycler_view.getChildAdapterPosition(v) | ||||||
|  |                 adapter.setSelectedPosition(position) | ||||||
|  |                 if (position != RecyclerView.NO_POSITION) { | ||||||
|  |                     val item = adapter.getItem(position) | ||||||
|  |                     (activity as MainActivity).onItemSelected(item) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // wrap for swiping | ||||||
|  |         wrappedAdapter = swipeManager.createWrappedAdapter(adapter) | ||||||
|  |  | ||||||
|  |         val animator = SwipeDismissItemAnimator() | ||||||
|  |  | ||||||
|  |         // Change animations are enabled by default since support-v7-recyclerview v22. | ||||||
|  |         // Disable the change animation in order to make turning back animation of swiped item | ||||||
|  |         // works properly. | ||||||
|  |         animator.supportsChangeAnimations = false | ||||||
|  |  | ||||||
|  |         recycler_view.layoutManager = layoutManager | ||||||
|  |         recycler_view.adapter = wrappedAdapter  // requires *wrapped* swipeableMessageAdapter | ||||||
|  |         recycler_view.itemAnimator = animator | ||||||
|  |         recycler_view.addOnScrollListener(recyclerViewOnScrollListener) | ||||||
|  |  | ||||||
|  |         recycler_view.addItemDecoration(SimpleListDividerDecorator( | ||||||
|  |             ContextCompat.getDrawable(context, R.drawable.list_divider_h), true)) | ||||||
|  |  | ||||||
|  |         // NOTE: | ||||||
|  |         // The initialization order is very important! This order determines the priority of | ||||||
|  |         // touch event handling. | ||||||
|  |         // | ||||||
|  |         // priority: TouchActionGuard > Swipe > DragAndDrop | ||||||
|  |         touchActionGuardManager.attachRecyclerView(recycler_view) | ||||||
|  |         swipeManager.attachRecyclerView(recycler_view) | ||||||
|  |  | ||||||
|  |         recyclerViewTouchActionGuardManager = touchActionGuardManager | ||||||
|  |         recyclerViewSwipeManager = swipeManager | ||||||
|  |         this.swipeableMessageAdapter = adapter | ||||||
|  |  | ||||||
|  |         Singleton.updateMessageListAdapterInListener(adapter) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun initFab(context: MainActivity) { | ||||||
|  |         val menu = FabSpeedDialMenu(context) | ||||||
|  |         menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast) | ||||||
|  |         menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal) | ||||||
|  |         FabUtils.initFab(context, R.drawable.ic_action_compose_message, menu) | ||||||
|  |             .addOnMenuItemClickListener { _, _, itemId -> | ||||||
|  |                 val identity = Singleton.getIdentity(context) | ||||||
|  |                 if (identity == null) { | ||||||
|  |                     Toast.makeText(activity, R.string.no_identity_warning, | ||||||
|  |                         Toast.LENGTH_LONG).show() | ||||||
|  |                 } else { | ||||||
|  |                     when (itemId) { | ||||||
|  |                         1 -> { | ||||||
|  |                             val intent = Intent(activity, ComposeMessageActivity::class.java) | ||||||
|  |                             intent.putExtra(EXTRA_IDENTITY, identity) | ||||||
|  |                             intent.putExtra(EXTRA_BROADCAST, true) | ||||||
|  |                             startActivity(intent) | ||||||
|  |                         } | ||||||
|  |                         2 -> { | ||||||
|  |                             val intent = Intent(activity, ComposeMessageActivity::class.java) | ||||||
|  |                             intent.putExtra(EXTRA_IDENTITY, identity) | ||||||
|  |                             startActivity(intent) | ||||||
|  |                         } | ||||||
|  |                         else -> { | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onDestroyView() { | ||||||
|  |         recyclerViewSwipeManager?.release() | ||||||
|  |         recyclerViewSwipeManager = null | ||||||
|  |  | ||||||
|  |         recyclerViewTouchActionGuardManager?.release() | ||||||
|  |         recyclerViewTouchActionGuardManager = null | ||||||
|  |  | ||||||
|  |         recycler_view.itemAnimator = null | ||||||
|  |         recycler_view.adapter = null | ||||||
|  |  | ||||||
|  |         wrappedAdapter?.let { WrapperAdapterUtils.releaseAll(it) } | ||||||
|  |         wrappedAdapter = null | ||||||
|  |  | ||||||
|  |         swipeableMessageAdapter = null | ||||||
|  |         layoutManager = null | ||||||
|  |  | ||||||
|  |         super.onDestroyView() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { | ||||||
|  |         inflater.inflate(R.menu.message_list, menu) | ||||||
|  |         emptyTrashMenuItem = menu.findItem(R.id.empty_trash) | ||||||
|  |         super.onCreateOptionsMenu(menu, inflater) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||||||
|  |         when (item.itemId) { | ||||||
|  |             R.id.empty_trash -> { | ||||||
|  |                 currentLabel.value?.let { label -> | ||||||
|  |                     if (label.type != Label.Type.TRASH) return true | ||||||
|  |  | ||||||
|  |                     doAsync { | ||||||
|  |                         for (message in messageRepo.findMessages(label)) { | ||||||
|  |                             messageRepo.remove(message) | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         uiThread { doUpdateList(label) } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 return true | ||||||
|  |             } | ||||||
|  |             else -> return false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun updateList(label: Label) { | ||||||
|  |         currentLabel.value = label | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun setActivateOnItemClick(activateOnItemClick: Boolean) { | ||||||
|  |         swipeableMessageAdapter?.setActivateOnItemClick(activateOnItemClick) | ||||||
|  |         this.activateOnItemClick = activateOnItemClick | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun showPreviousList() = if (backStack.isEmpty()) { | ||||||
|  |         false | ||||||
|  |     } else { | ||||||
|  |         currentLabel.value = backStack.pop() | ||||||
|  |         true | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| package ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.os.Bundle; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public class SettingsActivity extends DetailActivity { |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|  |  | ||||||
|         // Display the fragment as the main content. |  | ||||||
|         getFragmentManager().beginTransaction() |  | ||||||
|                 .replace(R.id.content, new SettingsFragment()) |  | ||||||
|                 .commit(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,140 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.content.SharedPreferences; |  | ||||||
| import android.os.AsyncTask; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.preference.Preference; |  | ||||||
| import android.preference.PreferenceFragment; |  | ||||||
| import android.preference.PreferenceManager; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import com.mikepenz.aboutlibraries.Libs; |  | ||||||
| import com.mikepenz.aboutlibraries.LibsBuilder; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.repository.AndroidNodeRegistry; |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.apps.abit.synchronization.SyncAdapter; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; |  | ||||||
| import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public class SettingsFragment |  | ||||||
|     extends PreferenceFragment |  | ||||||
|     implements SharedPreferences.OnSharedPreferenceChangeListener { |  | ||||||
|     @Override |  | ||||||
|     public void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|  |  | ||||||
|         // Load the preferences from an XML resource |  | ||||||
|         addPreferencesFromResource(R.xml.preferences); |  | ||||||
|  |  | ||||||
|         Preference about = findPreference("about"); |  | ||||||
|         about.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public boolean onPreferenceClick(Preference preference) { |  | ||||||
|                 new LibsBuilder() |  | ||||||
|                     .withActivityTitle(getActivity().getString(R.string.about)) |  | ||||||
|                     .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) |  | ||||||
|                     .withAboutIconShown(true) |  | ||||||
|                     .withAboutVersionShown(true) |  | ||||||
|                     .withAboutDescription(getString(R.string.about_app)) |  | ||||||
|                     .start(getActivity()); |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         final Preference cleanup = findPreference("cleanup"); |  | ||||||
|         cleanup.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public boolean onPreferenceClick(Preference preference) { |  | ||||||
|                 new AsyncTask<Void, Void, Void>() { |  | ||||||
|                     private Context ctx = getActivity().getApplicationContext(); |  | ||||||
|  |  | ||||||
|                     @Override |  | ||||||
|                     protected void onPreExecute() { |  | ||||||
|                         cleanup.setEnabled(false); |  | ||||||
|                         Toast.makeText(ctx, R.string.cleanup_notification_start, Toast |  | ||||||
|                             .LENGTH_SHORT).show(); |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     @Override |  | ||||||
|                     protected Void doInBackground(Void... voids) { |  | ||||||
|                         BitmessageContext bmc = Singleton.getBitmessageContext(ctx); |  | ||||||
|                         bmc.cleanup(); |  | ||||||
|                         bmc.internals().getNodeRegistry().clear(); |  | ||||||
|                         return null; |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     @Override |  | ||||||
|                     protected void onPostExecute(Void aVoid) { |  | ||||||
|                         Toast.makeText( |  | ||||||
|                             ctx, |  | ||||||
|                             R.string.cleanup_notification_end, |  | ||||||
|                             Toast.LENGTH_LONG |  | ||||||
|                         ).show(); |  | ||||||
|                         cleanup.setEnabled(true); |  | ||||||
|                     } |  | ||||||
|                 }.execute(); |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         Preference status = findPreference("status"); |  | ||||||
|         status.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public boolean onPreferenceClick(Preference preference) { |  | ||||||
|                 startActivity(new Intent(getActivity(), StatusActivity.class)); |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onAttach(Context ctx) { |  | ||||||
|         super.onAttach(ctx); |  | ||||||
|         PreferenceManager.getDefaultSharedPreferences(ctx) |  | ||||||
|             .registerOnSharedPreferenceChangeListener(this); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { |  | ||||||
|         switch (key) { |  | ||||||
|             case PREFERENCE_TRUSTED_NODE: |  | ||||||
|                 String node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null); |  | ||||||
|                 if (node != null) { |  | ||||||
|                     SyncAdapter.startSync(getActivity()); |  | ||||||
|                 } else { |  | ||||||
|                     SyncAdapter.stopSync(getActivity()); |  | ||||||
|                 } |  | ||||||
|                 break; |  | ||||||
|             case PREFERENCE_SERVER_POW: |  | ||||||
|                 if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) { |  | ||||||
|                     SyncAdapter.startPowSync(getActivity()); |  | ||||||
|                 } else { |  | ||||||
|                     SyncAdapter.stopPowSync(getActivity()); |  | ||||||
|                 } |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										200
									
								
								app/src/main/java/ch/dissem/apps/abit/SettingsFragment.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								app/src/main/java/ch/dissem/apps/abit/SettingsFragment.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,200 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.app.Activity | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.content.SharedPreferences | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.preference.PreferenceManager | ||||||
|  | import android.support.v4.content.FileProvider.getUriForFile | ||||||
|  | import android.support.v7.preference.Preference | ||||||
|  | import android.support.v7.preference.PreferenceFragmentCompat | ||||||
|  | import android.widget.Toast | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.apps.abit.synchronization.SyncAdapter | ||||||
|  | import ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW | ||||||
|  | import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE | ||||||
|  | import ch.dissem.apps.abit.util.Exports | ||||||
|  | import ch.dissem.apps.abit.util.Preferences | ||||||
|  | import com.mikepenz.aboutlibraries.Libs | ||||||
|  | import com.mikepenz.aboutlibraries.LibsBuilder | ||||||
|  | import org.jetbrains.anko.doAsync | ||||||
|  | import org.jetbrains.anko.support.v4.indeterminateProgressDialog | ||||||
|  | import org.jetbrains.anko.support.v4.startActivity | ||||||
|  | import org.jetbrains.anko.uiThread | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { | ||||||
|  |  | ||||||
|  |     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { | ||||||
|  |         addPreferencesFromResource(R.xml.preferences) | ||||||
|  |  | ||||||
|  |         findPreference("about")?.onPreferenceClickListener = aboutClickListener() | ||||||
|  |         val cleanup = findPreference("cleanup") | ||||||
|  |         cleanup?.onPreferenceClickListener = cleanupClickListener(cleanup) | ||||||
|  |         findPreference("export")?.onPreferenceClickListener = exportClickListener() | ||||||
|  |         findPreference("import")?.onPreferenceClickListener = importClickListener() | ||||||
|  |         findPreference("status").onPreferenceClickListener = statusClickListener() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun aboutClickListener() = Preference.OnPreferenceClickListener { | ||||||
|  |         (activity as? MainActivity)?.let { activity -> | ||||||
|  |             val libsBuilder = LibsBuilder() | ||||||
|  |                 .withActivityTitle(activity.getString(R.string.about)) | ||||||
|  |                 .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) | ||||||
|  |                 .withAboutIconShown(true) | ||||||
|  |                 .withAboutVersionShown(true) | ||||||
|  |                 .withAboutDescription(getString(R.string.about_app)) | ||||||
|  |             if (activity.hasDetailPane) { | ||||||
|  |                 activity.setDetailView(libsBuilder.supportFragment()) | ||||||
|  |             } else { | ||||||
|  |                 libsBuilder.start(activity) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return@OnPreferenceClickListener true | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener { | ||||||
|  |         val ctx = activity?.applicationContext ?: throw IllegalStateException("Context not available") | ||||||
|  |         cleanup.isEnabled = false | ||||||
|  |         Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show() | ||||||
|  |  | ||||||
|  |         doAsync { | ||||||
|  |             val bmc = Singleton.getBitmessageContext(ctx) | ||||||
|  |             bmc.internals.nodeRegistry.clear() | ||||||
|  |             bmc.cleanup() | ||||||
|  |             Preferences.cleanupExportDirectory(ctx) | ||||||
|  |  | ||||||
|  |             uiThread { | ||||||
|  |                 Toast.makeText( | ||||||
|  |                     ctx, | ||||||
|  |                     R.string.cleanup_notification_end, | ||||||
|  |                     Toast.LENGTH_LONG | ||||||
|  |                 ).show() | ||||||
|  |                 cleanup.isEnabled = true | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return@OnPreferenceClickListener true | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun exportClickListener() = Preference.OnPreferenceClickListener { | ||||||
|  |         val ctx = context ?: throw IllegalStateException("No context available") | ||||||
|  |  | ||||||
|  |         indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply { | ||||||
|  |             doAsync { | ||||||
|  |                 val exportDirectory = Preferences.getExportDirectory(ctx) | ||||||
|  |                 exportDirectory.mkdirs() | ||||||
|  |                 val file = Exports.exportData(exportDirectory, ctx) | ||||||
|  |                 val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", file) | ||||||
|  |                 val intent = Intent(android.content.Intent.ACTION_SEND) | ||||||
|  |                 intent.type = "application/zip" | ||||||
|  |                 intent.putExtra(Intent.EXTRA_SUBJECT, "abit-export.zip") | ||||||
|  |                 intent.putExtra(Intent.EXTRA_STREAM, contentUri) | ||||||
|  |                 startActivityForResult(Intent.createChooser(intent, ""), WRITE_EXPORT_REQUEST_CODE) | ||||||
|  |                 uiThread { | ||||||
|  |                     dismiss() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return@OnPreferenceClickListener true | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun importClickListener() = Preference.OnPreferenceClickListener { | ||||||
|  |         val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) | ||||||
|  |         intent.addCategory(Intent.CATEGORY_OPENABLE) | ||||||
|  |         intent.type = "application/zip" | ||||||
|  |  | ||||||
|  |         startActivityForResult(intent, READ_IMPORT_REQUEST_CODE) | ||||||
|  |         return@OnPreferenceClickListener true | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun statusClickListener() = Preference.OnPreferenceClickListener { | ||||||
|  |         val activity = activity as MainActivity | ||||||
|  |         if (activity.hasDetailPane) { | ||||||
|  |             activity.setDetailView(StatusFragment()) | ||||||
|  |         } else { | ||||||
|  |             startActivity<StatusActivity>() | ||||||
|  |         } | ||||||
|  |         return@OnPreferenceClickListener true | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | ||||||
|  |         val ctx = context ?: throw IllegalStateException("No context available") | ||||||
|  |         when (requestCode) { | ||||||
|  |             WRITE_EXPORT_REQUEST_CODE -> Preferences.cleanupExportDirectory(ctx) | ||||||
|  |             READ_IMPORT_REQUEST_CODE -> { | ||||||
|  |                 if (resultCode == Activity.RESULT_OK && data?.data != null) { | ||||||
|  |                     indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply { | ||||||
|  |                         doAsync { | ||||||
|  |                             Exports.importData(data.data, ctx) | ||||||
|  |                             uiThread { | ||||||
|  |                                 dismiss() | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onAttach(ctx: Context?) { | ||||||
|  |         super.onAttach(ctx) | ||||||
|  |         (ctx as? MainActivity)?.floatingActionButton?.hide() | ||||||
|  |         PreferenceManager.getDefaultSharedPreferences(ctx) | ||||||
|  |             .registerOnSharedPreferenceChangeListener(this) | ||||||
|  |  | ||||||
|  |         (ctx as? MainActivity)?.updateTitle(getString(R.string.settings)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { | ||||||
|  |         when (key) { | ||||||
|  |             PREFERENCE_TRUSTED_NODE -> toggleSyncTrustedNode(sharedPreferences) | ||||||
|  |             PREFERENCE_SERVER_POW -> toggleSyncServerPOW(sharedPreferences) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun toggleSyncTrustedNode(sharedPreferences: SharedPreferences) { | ||||||
|  |         val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null) | ||||||
|  |         val ctx = context ?: throw IllegalStateException("No context available") | ||||||
|  |         if (node != null) { | ||||||
|  |             SyncAdapter.startSync(ctx) | ||||||
|  |         } else { | ||||||
|  |             SyncAdapter.stopSync(ctx) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun toggleSyncServerPOW(sharedPreferences: SharedPreferences) { | ||||||
|  |         val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null) | ||||||
|  |         if (node != null) { | ||||||
|  |             val ctx = context ?: throw IllegalStateException("No context available") | ||||||
|  |             if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) { | ||||||
|  |                 SyncAdapter.startPowSync(ctx) | ||||||
|  |             } else { | ||||||
|  |                 SyncAdapter.stopPowSync(ctx) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         const val WRITE_EXPORT_REQUEST_CODE = 1 | ||||||
|  |         const val READ_IMPORT_REQUEST_CODE = 2 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,61 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit; |  | ||||||
|  |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.v7.app.AppCompatActivity; |  | ||||||
| import android.support.v7.widget.Toolbar; |  | ||||||
| import android.widget.TextView; |  | ||||||
|  |  | ||||||
| import com.mikepenz.materialize.MaterializeBuilder; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
|  |  | ||||||
| public class StatusActivity extends AppCompatActivity { |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|         setContentView(R.layout.activity_status); |  | ||||||
|  |  | ||||||
|         Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); |  | ||||||
|         setSupportActionBar(toolbar); |  | ||||||
|  |  | ||||||
|         //noinspection ConstantConditions |  | ||||||
|         getSupportActionBar().setDisplayHomeAsUpEnabled(true); |  | ||||||
|         getSupportActionBar().setHomeButtonEnabled(false); |  | ||||||
|  |  | ||||||
|         new MaterializeBuilder() |  | ||||||
|             .withActivity(this) |  | ||||||
|             .withStatusBarColorRes(R.color.colorPrimaryDark) |  | ||||||
|             .withTranslucentStatusBarProgrammatically(true) |  | ||||||
|             .withStatusBarPadding(true) |  | ||||||
|             .build(); |  | ||||||
|  |  | ||||||
|         BitmessageContext bmc = Singleton.getBitmessageContext(this); |  | ||||||
|         StringBuilder status = new StringBuilder(); |  | ||||||
|         for (BitmessageAddress address : bmc.addresses().getIdentities()) { |  | ||||||
|             status.append(address.getAddress()).append('\n'); |  | ||||||
|         } |  | ||||||
|         status.append('\n'); |  | ||||||
|         status.append(bmc.status()); |  | ||||||
|         ((TextView) findViewById(R.id.content)).setText(status); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| } |  | ||||||
							
								
								
									
										54
									
								
								app/src/main/java/ch/dissem/apps/abit/StatusActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/src/main/java/ch/dissem/apps/abit/StatusActivity.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v7.app.AppCompatActivity | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import com.mikepenz.materialize.MaterializeBuilder | ||||||
|  | import kotlinx.android.synthetic.main.activity_status.* | ||||||
|  |  | ||||||
|  | class StatusActivity : AppCompatActivity() { | ||||||
|  |  | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         setContentView(R.layout.activity_status) | ||||||
|  |  | ||||||
|  |         setSupportActionBar(toolbar) | ||||||
|  |         supportActionBar?.apply { | ||||||
|  |             setDisplayHomeAsUpEnabled(true) | ||||||
|  |             setHomeButtonEnabled(false) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         MaterializeBuilder() | ||||||
|  |                 .withActivity(this) | ||||||
|  |                 .withStatusBarColorRes(R.color.colorPrimaryDark) | ||||||
|  |                 .withTranslucentStatusBarProgrammatically(true) | ||||||
|  |                 .withStatusBarPadding(true) | ||||||
|  |                 .build() | ||||||
|  |  | ||||||
|  |         val bmc = Singleton.getBitmessageContext(this) | ||||||
|  |         val status = StringBuilder() | ||||||
|  |         for (address in bmc.addresses.getIdentities()) { | ||||||
|  |             status.append(address.address).append('\n') | ||||||
|  |         } | ||||||
|  |         status.append('\n') | ||||||
|  |         status.append(bmc.status()) | ||||||
|  |         content.text = status | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								app/src/main/java/ch/dissem/apps/abit/StatusFragment.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/src/main/java/ch/dissem/apps/abit/StatusFragment.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v4.app.Fragment | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.TextView | ||||||
|  |  | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  |  | ||||||
|  | class StatusFragment : Fragment() { | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = | ||||||
|  |         inflater.inflate(R.layout.fragment_status, container, false) | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |  | ||||||
|  |         val bmc = Singleton.getBitmessageContext( | ||||||
|  |             context ?: throw IllegalStateException("No context available") | ||||||
|  |         ) | ||||||
|  |         val status = StringBuilder() | ||||||
|  |         for (address in bmc.addresses.getIdentities()) { | ||||||
|  |             status.append(address.address).append('\n') | ||||||
|  |         } | ||||||
|  |         status.append('\n') | ||||||
|  |         status.append(bmc.status()) | ||||||
|  |         view.findViewById<TextView>(R.id.content).text = status | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,99 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.adapter; |  | ||||||
|  |  | ||||||
| import android.support.v7.widget.RecyclerView; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.CheckBox; |  | ||||||
| import android.widget.CompoundButton; |  | ||||||
| import android.widget.TextView; |  | ||||||
|  |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public class AddressSelectorAdapter |  | ||||||
|     extends RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder> { |  | ||||||
|  |  | ||||||
|     private final List<Selectable<BitmessageAddress>> data; |  | ||||||
|  |  | ||||||
|     public AddressSelectorAdapter(List<BitmessageAddress> identities) { |  | ||||||
|         data = new ArrayList<>(identities.size()); |  | ||||||
|         for (BitmessageAddress identity : identities) { |  | ||||||
|             data.add(new Selectable<>(identity)); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |  | ||||||
|         final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); |  | ||||||
|         final View v = inflater.inflate(R.layout.select_identity_row, parent, false); |  | ||||||
|         return new ViewHolder(v); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onBindViewHolder(ViewHolder holder, int position) { |  | ||||||
|         Selectable<BitmessageAddress> selectable = data.get(position); |  | ||||||
|         holder.data = selectable; |  | ||||||
|         holder.checkbox.setChecked(selectable.selected); |  | ||||||
|         holder.checkbox.setText(selectable.data.toString()); |  | ||||||
|         holder.address.setText(selectable.data.getAddress()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public int getItemCount() { |  | ||||||
|         return data.size(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     static class ViewHolder extends RecyclerView.ViewHolder { |  | ||||||
|         public Selectable<BitmessageAddress> data; |  | ||||||
|         public final CheckBox checkbox; |  | ||||||
|         public final TextView address; |  | ||||||
|  |  | ||||||
|         private ViewHolder(View v) { |  | ||||||
|             super(v); |  | ||||||
|             checkbox = (CheckBox) v.findViewById(R.id.checkbox); |  | ||||||
|             address = (TextView) v.findViewById(R.id.address); |  | ||||||
|             checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { |  | ||||||
|                 @Override |  | ||||||
|                 public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { |  | ||||||
|                     if (data != null) { |  | ||||||
|                         data.selected = isChecked; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public List<BitmessageAddress> getSelected() { |  | ||||||
|         List<BitmessageAddress> result = new LinkedList<>(); |  | ||||||
|         for (Selectable<BitmessageAddress> selectable : data) { |  | ||||||
|             if (selectable.selected) { |  | ||||||
|                 result.add(selectable.data); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,70 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.adapter | ||||||
|  |  | ||||||
|  | import android.support.v7.widget.RecyclerView | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.CheckBox | ||||||
|  | import android.widget.TextView | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder>() { | ||||||
|  |  | ||||||
|  |     private val data = identities.map { Selectable(it) }.toMutableList() | ||||||
|  |  | ||||||
|  |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||||||
|  |         val inflater = LayoutInflater.from(parent.context) | ||||||
|  |         val v = inflater.inflate(R.layout.select_identity_row, parent, false) | ||||||
|  |         return ViewHolder(v) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||||||
|  |         val selectable = data[position] | ||||||
|  |         holder.data = selectable | ||||||
|  |         holder.checkbox.isChecked = selectable.selected | ||||||
|  |         holder.checkbox.text = selectable.data.toString() | ||||||
|  |         holder.address.text = selectable.data.address | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getItemCount() = data.size | ||||||
|  |  | ||||||
|  |     class ViewHolder internal constructor(v: View) : RecyclerView.ViewHolder(v) { | ||||||
|  |         var data: Selectable<BitmessageAddress>? = null | ||||||
|  |         val checkbox = v.findViewById<CheckBox>(R.id.checkbox)!! | ||||||
|  |         val address = v.findViewById<TextView>(R.id.address)!! | ||||||
|  |  | ||||||
|  |         init { | ||||||
|  |             checkbox.setOnCheckedChangeListener { _, isChecked -> | ||||||
|  |                 data?.selected = isChecked | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val selected: List<BitmessageAddress> | ||||||
|  |         get() { | ||||||
|  |             return data | ||||||
|  |                 .filter { it.selected } | ||||||
|  |                 .mapTo(LinkedList()) { it.data } | ||||||
|  |         } | ||||||
|  | } | ||||||
| @@ -14,16 +14,16 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package ch.dissem.apps.abit.adapter; | package ch.dissem.apps.abit.adapter | ||||||
| 
 | 
 | ||||||
| import ch.dissem.apps.abit.util.PRNGFixes; | import ch.dissem.apps.abit.util.PRNGFixes | ||||||
| import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography; | import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @author Christian Basler |  * @author Christian Basler | ||||||
|  */ |  */ | ||||||
| public class AndroidCryptography extends SpongyCryptography { | class AndroidCryptography : SpongyCryptography() { | ||||||
|     public AndroidCryptography() { |     init { | ||||||
|         PRNGFixes.apply(); |         PRNGFixes.apply() | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -1,140 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.adapter; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.BaseAdapter; |  | ||||||
| import android.widget.Filter; |  | ||||||
| import android.widget.Filterable; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.TextView; |  | ||||||
|  |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.Identicon; |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * An adapter for contacts. Can be filtered by alias or address. |  | ||||||
|  */ |  | ||||||
| public class ContactAdapter extends BaseAdapter implements Filterable { |  | ||||||
|     private final LayoutInflater inflater; |  | ||||||
|     private final List<BitmessageAddress> originalData; |  | ||||||
|     private List<BitmessageAddress> data; |  | ||||||
|  |  | ||||||
|     public ContactAdapter(Context ctx) { |  | ||||||
|         inflater = LayoutInflater.from(ctx); |  | ||||||
|         originalData = Singleton.getAddressRepository(ctx).getContacts(); |  | ||||||
|         data = originalData; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public int getCount() { |  | ||||||
|         return data.size(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public BitmessageAddress getItem(int position) { |  | ||||||
|         return data.get(position); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public long getItemId(int position) { |  | ||||||
|         return position; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public View getView(int position, View convertView, ViewGroup parent) { |  | ||||||
|         if (convertView == null) { |  | ||||||
|             convertView = inflater.inflate(R.layout.contact_row, parent, false); |  | ||||||
|         } |  | ||||||
|         BitmessageAddress item = getItem(position); |  | ||||||
|         ((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item)); |  | ||||||
|         ((TextView) convertView.findViewById(R.id.name)).setText(item.toString()); |  | ||||||
|         ((TextView) convertView.findViewById(R.id.address)).setText(item.getAddress()); |  | ||||||
|         return convertView; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public Filter getFilter() { |  | ||||||
|         return new ContactFilter(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private class ContactFilter extends Filter { |  | ||||||
|         @Override |  | ||||||
|         protected FilterResults performFiltering(CharSequence prefix) { |  | ||||||
|             FilterResults results = new FilterResults(); |  | ||||||
|  |  | ||||||
|             if (prefix == null || prefix.length() == 0) { |  | ||||||
|                 results.values = originalData; |  | ||||||
|                 results.count = originalData.size(); |  | ||||||
|             } else { |  | ||||||
|                 String prefixString = prefix.toString().toLowerCase(); |  | ||||||
|  |  | ||||||
|                 final ArrayList<BitmessageAddress> newValues = new ArrayList<>(); |  | ||||||
|  |  | ||||||
|                 for (int i = 0; i < originalData.size(); i++) { |  | ||||||
|                     final BitmessageAddress value = originalData.get(i); |  | ||||||
|  |  | ||||||
|                     // First match against the whole, non-splitted value |  | ||||||
|                     if (value.getAlias() != null) { |  | ||||||
|                         String alias = value.getAlias().toLowerCase(); |  | ||||||
|                         if (alias.startsWith(prefixString)) { |  | ||||||
|                             newValues.add(value); |  | ||||||
|                         } else { |  | ||||||
|                             final String[] words = alias.split(" "); |  | ||||||
|  |  | ||||||
|                             for (String word : words) { |  | ||||||
|                                 if (word.startsWith(prefixString)) { |  | ||||||
|                                     newValues.add(value); |  | ||||||
|                                     break; |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     } else { |  | ||||||
|                         String address = value.getAddress().toLowerCase(); |  | ||||||
|                         if (address.contains(prefixString)) { |  | ||||||
|                             newValues.add(value); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 results.values = newValues; |  | ||||||
|                 results.count = newValues.size(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             return results; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         protected void publishResults(CharSequence constraint, FilterResults results) { |  | ||||||
|             //noinspection unchecked |  | ||||||
|             data = (List<BitmessageAddress>) results.values; |  | ||||||
|             if (results.count > 0) { |  | ||||||
|                 notifyDataSetChanged(); |  | ||||||
|             } else { |  | ||||||
|                 notifyDataSetInvalidated(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										128
									
								
								app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								app/src/main/java/ch/dissem/apps/abit/adapter/ContactAdapter.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.adapter | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.BaseAdapter | ||||||
|  | import android.widget.Filter | ||||||
|  | import android.widget.Filterable | ||||||
|  | import android.widget.ImageView | ||||||
|  | import android.widget.TextView | ||||||
|  |  | ||||||
|  | import java.util.ArrayList | ||||||
|  |  | ||||||
|  | import ch.dissem.apps.abit.Identicon | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * An adapter for contacts. Can be filtered by alias or address. | ||||||
|  |  */ | ||||||
|  | class ContactAdapter(ctx: Context) : BaseAdapter(), Filterable { | ||||||
|  |     private val inflater = LayoutInflater.from(ctx) | ||||||
|  |     private val originalData = Singleton.getAddressRepository(ctx).getContacts() | ||||||
|  |     private var data: List<BitmessageAddress> = originalData | ||||||
|  |  | ||||||
|  |     override fun getCount() = data.size | ||||||
|  |  | ||||||
|  |     override fun getItem(position: Int) = data[position] | ||||||
|  |  | ||||||
|  |     override fun getItemId(position: Int) = position.toLong() | ||||||
|  |  | ||||||
|  |     override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { | ||||||
|  |         val viewHolder = if (convertView == null) { | ||||||
|  |             ViewHolder(inflater.inflate(R.layout.contact_row, parent, false)) | ||||||
|  |         } else { | ||||||
|  |             convertView.tag as ViewHolder | ||||||
|  |         } | ||||||
|  |         val item = getItem(position) | ||||||
|  |         viewHolder.avatar.setImageDrawable(Identicon(item)) | ||||||
|  |         viewHolder.name.text = item.toString() | ||||||
|  |         viewHolder.address.text = item.address | ||||||
|  |         return viewHolder.view | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getFilter(): Filter = ContactFilter() | ||||||
|  |  | ||||||
|  |     private inner class ViewHolder(val view: View) { | ||||||
|  |         val avatar = view.findViewById<ImageView>(R.id.avatar)!! | ||||||
|  |         val name = view.findViewById<TextView>(R.id.name)!! | ||||||
|  |         val address = view.findViewById<TextView>(R.id.address)!! | ||||||
|  |  | ||||||
|  |         init { | ||||||
|  |             view.tag = this | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private inner class ContactFilter : Filter() { | ||||||
|  |         override fun performFiltering(prefix: CharSequence?): Filter.FilterResults { | ||||||
|  |             val results = Filter.FilterResults() | ||||||
|  |  | ||||||
|  |             if (prefix?.isEmpty() == false) { | ||||||
|  |                 val prefixString = prefix.toString().toLowerCase() | ||||||
|  |  | ||||||
|  |                 val newValues = ArrayList<BitmessageAddress>() | ||||||
|  |  | ||||||
|  |                 originalData | ||||||
|  |                         .forEach { value -> | ||||||
|  |                             value.alias?.toLowerCase()?.let { alias -> | ||||||
|  |                                 if (alias.startsWith(prefixString)) { | ||||||
|  |                                     newValues.add(value) | ||||||
|  |                                 } else { | ||||||
|  |                                     val words = alias.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() | ||||||
|  |  | ||||||
|  |                                     for (word in words) { | ||||||
|  |                                         if (word.startsWith(prefixString)) { | ||||||
|  |                                             newValues.add(value) | ||||||
|  |                                             break | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } ?: { | ||||||
|  |                                 val address = value.address.toLowerCase() | ||||||
|  |                                 if (address.contains(prefixString)) { | ||||||
|  |                                     newValues.add(value) | ||||||
|  |                                 } | ||||||
|  |                             }.invoke() | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                 results.values = newValues | ||||||
|  |                 results.count = newValues.size | ||||||
|  |             } else { | ||||||
|  |                 results.values = originalData | ||||||
|  |                 results.count = originalData.size | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return results | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) { | ||||||
|  |             @Suppress("UNCHECKED_CAST") | ||||||
|  |             data = results.values as List<BitmessageAddress> | ||||||
|  |             if (results.count > 0) { | ||||||
|  |                 notifyDataSetChanged() | ||||||
|  |             } else { | ||||||
|  |                 notifyDataSetInvalidated() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -14,16 +14,11 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package ch.dissem.apps.abit.adapter; | package ch.dissem.apps.abit.adapter | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @author Christian Basler |  * @author Christian Basler | ||||||
|  */ |  */ | ||||||
| class Selectable<T> { | class Selectable<out T>(val data: T) { | ||||||
|     final T data; |     var selected = false | ||||||
|     boolean selected = false; |  | ||||||
| 
 |  | ||||||
|     Selectable(T data) { |  | ||||||
|         this.data = data; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| @@ -1,329 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Haruki Hasegawa |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.adapter; |  | ||||||
|  |  | ||||||
| import android.annotation.SuppressLint; |  | ||||||
| import android.graphics.Typeface; |  | ||||||
| import android.support.v7.widget.RecyclerView; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.FrameLayout; |  | ||||||
| import android.widget.ImageView; |  | ||||||
| import android.widget.TextView; |  | ||||||
|  |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.swipeable.action |  | ||||||
|     .SwipeResultActionMoveToSwipedDirection; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder; |  | ||||||
| import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils; |  | ||||||
|  |  | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.Identicon; |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
| import ch.dissem.apps.abit.util.Assets; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.Label; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE; |  | ||||||
| import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Adapted from the basic swipeable example by Haruki Hasegawa. See |  | ||||||
|  * |  | ||||||
|  * @author Christian Basler |  | ||||||
|  * @see <a href="https://github.com/h6ah4i/android-advancedrecyclerview"> |  | ||||||
|  * https://github.com/h6ah4i/android-advancedrecyclerview</a> |  | ||||||
|  */ |  | ||||||
| public class SwipeableMessageAdapter |  | ||||||
|     extends RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder> |  | ||||||
|     implements SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants { |  | ||||||
|  |  | ||||||
|     private List<Plaintext> data = Collections.emptyList(); |  | ||||||
|     private EventListener eventListener; |  | ||||||
|     private final View.OnClickListener itemViewOnClickListener; |  | ||||||
|     private final View.OnClickListener swipeableViewContainerOnClickListener; |  | ||||||
|  |  | ||||||
|     private Label label; |  | ||||||
|     private int selectedPosition; |  | ||||||
|     private boolean activateOnItemClick; |  | ||||||
|  |  | ||||||
|     public void setActivateOnItemClick(boolean activateOnItemClick) { |  | ||||||
|         this.activateOnItemClick = activateOnItemClick; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public interface EventListener { |  | ||||||
|         void onItemDeleted(Plaintext item); |  | ||||||
|  |  | ||||||
|         void onItemArchived(Plaintext item); |  | ||||||
|  |  | ||||||
|         void onItemViewClicked(View v); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @SuppressWarnings("WeakerAccess") |  | ||||||
|     static class ViewHolder extends AbstractSwipeableItemViewHolder { |  | ||||||
|         public final FrameLayout container; |  | ||||||
|         public final ImageView avatar; |  | ||||||
|         public final ImageView status; |  | ||||||
|         public final TextView sender; |  | ||||||
|         public final TextView subject; |  | ||||||
|         public final TextView extract; |  | ||||||
|  |  | ||||||
|         ViewHolder(View v) { |  | ||||||
|             super(v); |  | ||||||
|             container = (FrameLayout) v.findViewById(R.id.container); |  | ||||||
|             avatar = (ImageView) v.findViewById(R.id.avatar); |  | ||||||
|             status = (ImageView) v.findViewById(R.id.status); |  | ||||||
|             sender = (TextView) v.findViewById(R.id.sender); |  | ||||||
|             subject = (TextView) v.findViewById(R.id.subject); |  | ||||||
|             extract = (TextView) v.findViewById(R.id.text); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         public View getSwipeableContainerView() { |  | ||||||
|             return container; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public SwipeableMessageAdapter() { |  | ||||||
|         itemViewOnClickListener = new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 onItemViewClick(view); |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|         swipeableViewContainerOnClickListener = new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 onSwipeableViewContainerClick(view); |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         // SwipeableItemAdapter requires stable ID, and also |  | ||||||
|         // have to implement the getItemId() method appropriately. |  | ||||||
|         setHasStableIds(true); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void setData(Label label, List<Plaintext> data) { |  | ||||||
|         this.label = label; |  | ||||||
|         this.data = data; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void onItemViewClick(View v) { |  | ||||||
|         if (eventListener != null) { |  | ||||||
|             eventListener.onItemViewClicked(v); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void onSwipeableViewContainerClick(View v) { |  | ||||||
|         if (eventListener != null) { |  | ||||||
|             eventListener.onItemViewClicked( |  | ||||||
|                 RecyclerViewAdapterUtils.getParentViewHolderItemView(v)); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public Plaintext getItem(int position) { |  | ||||||
|         return data.get(position); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public long getItemId(int position) { |  | ||||||
|         return (long) data.get(position).getId(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |  | ||||||
|         final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); |  | ||||||
|         final View v = inflater.inflate(R.layout.message_row, parent, false); |  | ||||||
|         return new ViewHolder(v); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onBindViewHolder(ViewHolder holder, int position) { |  | ||||||
|         final Plaintext item = data.get(position); |  | ||||||
|  |  | ||||||
|         if (activateOnItemClick) { |  | ||||||
|             holder.container.setBackgroundResource( |  | ||||||
|                 position == selectedPosition |  | ||||||
|                     ? R.drawable.bg_item_selected_state |  | ||||||
|                     : R.drawable.bg_item_normal_state |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // set listeners |  | ||||||
|         // (if the item is *pinned*, click event comes to the itemView) |  | ||||||
|         holder.itemView.setOnClickListener(itemViewOnClickListener); |  | ||||||
|         // (if the item is *not pinned*, click event comes to the container) |  | ||||||
|         holder.container.setOnClickListener(swipeableViewContainerOnClickListener); |  | ||||||
|  |  | ||||||
|         // set data |  | ||||||
|         holder.avatar.setImageDrawable(new Identicon(item.getFrom())); |  | ||||||
|         holder.status.setImageResource(Assets.getStatusDrawable(item.getStatus())); |  | ||||||
|         holder.status.setContentDescription( |  | ||||||
|             holder.status.getContext().getString(Assets.getStatusString(item.getStatus()))); |  | ||||||
|         holder.sender.setText(item.getFrom().toString()); |  | ||||||
|         holder.subject.setText(normalizeWhitespaces(item.getSubject())); |  | ||||||
|         holder.extract.setText(normalizeWhitespaces(item.getText())); |  | ||||||
|         if (item.isUnread()) { |  | ||||||
|             holder.sender.setTypeface(Typeface.DEFAULT_BOLD); |  | ||||||
|             holder.subject.setTypeface(Typeface.DEFAULT_BOLD); |  | ||||||
|         } else { |  | ||||||
|             holder.sender.setTypeface(Typeface.DEFAULT); |  | ||||||
|             holder.subject.setTypeface(Typeface.DEFAULT); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public int getItemCount() { |  | ||||||
|         return data.size(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public int onGetSwipeReactionType(ViewHolder holder, int position, int x, int y) { |  | ||||||
|         if (label == LABEL_ARCHIVE || label.getType() == Label.Type.TRASH) { |  | ||||||
|             return REACTION_CAN_SWIPE_LEFT | REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT; |  | ||||||
|         } |  | ||||||
|         return REACTION_CAN_SWIPE_BOTH_H; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     @SuppressLint("SwitchIntDef") |  | ||||||
|     public void onSetSwipeBackground(ViewHolder holder, int position, int type) { |  | ||||||
|         int bgRes = 0; |  | ||||||
|         switch (type) { |  | ||||||
|             case DRAWABLE_SWIPE_NEUTRAL_BACKGROUND: |  | ||||||
|                 bgRes = R.drawable.bg_swipe_item_neutral; |  | ||||||
|                 break; |  | ||||||
|             case DRAWABLE_SWIPE_LEFT_BACKGROUND: |  | ||||||
|                 bgRes = R.drawable.bg_swipe_item_left; |  | ||||||
|                 break; |  | ||||||
|             case DRAWABLE_SWIPE_RIGHT_BACKGROUND: |  | ||||||
|                 if (label == LABEL_ARCHIVE || label.getType() == Label.Type.TRASH) { |  | ||||||
|                     bgRes = R.drawable.bg_swipe_item_neutral; |  | ||||||
|                 } else { |  | ||||||
|                     bgRes = R.drawable.bg_swipe_item_right; |  | ||||||
|                 } |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|         holder.itemView.setBackgroundResource(bgRes); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     @SuppressLint("SwitchIntDef") |  | ||||||
|     public SwipeResultAction onSwipeItem(ViewHolder holder, final int position, int result) { |  | ||||||
|         switch (result) { |  | ||||||
|             // swipe right |  | ||||||
|             case RESULT_SWIPED_RIGHT: |  | ||||||
|                 return new SwipeRightResultAction(this, position); |  | ||||||
|             case RESULT_SWIPED_LEFT: |  | ||||||
|                 return new SwipeLeftResultAction(this, position); |  | ||||||
|             // other --- do nothing |  | ||||||
|             case RESULT_CANCELED: |  | ||||||
|             default: |  | ||||||
|                 return null; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void setEventListener(EventListener eventListener) { |  | ||||||
|         this.eventListener = eventListener; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void setSelectedPosition(int selectedPosition) { |  | ||||||
|         int oldPosition = this.selectedPosition; |  | ||||||
|         this.selectedPosition = selectedPosition; |  | ||||||
|         notifyItemChanged(oldPosition); |  | ||||||
|         notifyItemChanged(selectedPosition); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static class SwipeLeftResultAction extends SwipeResultActionMoveToSwipedDirection { |  | ||||||
|         private SwipeableMessageAdapter adapter; |  | ||||||
|         private final int position; |  | ||||||
|         private final Plaintext item; |  | ||||||
|  |  | ||||||
|         SwipeLeftResultAction(SwipeableMessageAdapter adapter, int position) { |  | ||||||
|             this.adapter = adapter; |  | ||||||
|             this.position = position; |  | ||||||
|             this.item = adapter.data.get(position); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         protected void onPerformAction() { |  | ||||||
|             super.onPerformAction(); |  | ||||||
|  |  | ||||||
|             adapter.data.remove(position); |  | ||||||
|             adapter.notifyItemRemoved(position); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         protected void onSlideAnimationEnd() { |  | ||||||
|             super.onSlideAnimationEnd(); |  | ||||||
|  |  | ||||||
|             if (adapter.eventListener != null) { |  | ||||||
|                 adapter.eventListener.onItemDeleted(item); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         protected void onCleanUp() { |  | ||||||
|             super.onCleanUp(); |  | ||||||
|             // clear the references |  | ||||||
|             adapter = null; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static class SwipeRightResultAction extends SwipeResultActionRemoveItem { |  | ||||||
|         private SwipeableMessageAdapter adapter; |  | ||||||
|         private final int position; |  | ||||||
|         private final Plaintext item; |  | ||||||
|  |  | ||||||
|         SwipeRightResultAction(SwipeableMessageAdapter adapter, int position) { |  | ||||||
|             this.adapter = adapter; |  | ||||||
|             this.position = position; |  | ||||||
|             this.item = adapter.data.get(position); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         protected void onPerformAction() { |  | ||||||
|             super.onPerformAction(); |  | ||||||
|  |  | ||||||
|             adapter.data.remove(position); |  | ||||||
|             adapter.notifyItemRemoved(position); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         protected void onSlideAnimationEnd() { |  | ||||||
|             super.onSlideAnimationEnd(); |  | ||||||
|  |  | ||||||
|             if (adapter.eventListener != null) { |  | ||||||
|                 adapter.eventListener.onItemArchived(item); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         protected void onCleanUp() { |  | ||||||
|             super.onCleanUp(); |  | ||||||
|             // clear the references |  | ||||||
|             adapter = null; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,250 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Haruki Hasegawa | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.adapter | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.graphics.Typeface | ||||||
|  | import android.support.v7.widget.RecyclerView | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.FrameLayout | ||||||
|  | import android.widget.ImageView | ||||||
|  | import android.widget.TextView | ||||||
|  | import ch.dissem.apps.abit.Identicon | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE | ||||||
|  | import ch.dissem.apps.abit.util.Assets | ||||||
|  | import ch.dissem.apps.abit.util.Strings.prepareMessageExtract | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.Label | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants.* | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionMoveToSwipedDirection | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder | ||||||
|  | import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Adapted from the basic swipeable example by Haruki Hasegawa. See | ||||||
|  |  * | ||||||
|  |  * @author Christian Basler | ||||||
|  |  * @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview) | ||||||
|  |  */ | ||||||
|  | class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>(), SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants { | ||||||
|  |  | ||||||
|  |     private val data = LinkedList<Plaintext>() | ||||||
|  |     var eventListener: EventListener? = null | ||||||
|  |     private val itemViewOnClickListener: View.OnClickListener | ||||||
|  |     private val swipeableViewContainerOnClickListener: View.OnClickListener | ||||||
|  |  | ||||||
|  |     private var label: Label? = null | ||||||
|  |     private var selectedPosition = -1 | ||||||
|  |     private var activateOnItemClick: Boolean = false | ||||||
|  |  | ||||||
|  |     fun setActivateOnItemClick(activateOnItemClick: Boolean) { | ||||||
|  |         this.activateOnItemClick = activateOnItemClick | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     interface EventListener { | ||||||
|  |         fun onItemDeleted(item: Plaintext) | ||||||
|  |  | ||||||
|  |         fun onItemArchived(item: Plaintext) | ||||||
|  |  | ||||||
|  |         fun onItemViewClicked(v: View?) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     class ViewHolder(v: View) : AbstractSwipeableItemViewHolder(v) { | ||||||
|  |         val container = v.findViewById<FrameLayout>(R.id.container)!! | ||||||
|  |         val avatar = v.findViewById<ImageView>(R.id.avatar)!! | ||||||
|  |         val status = v.findViewById<ImageView>(R.id.status)!! | ||||||
|  |         val sender = v.findViewById<TextView>(R.id.sender)!! | ||||||
|  |         val subject = v.findViewById<TextView>(R.id.subject)!! | ||||||
|  |         val extract = v.findViewById<TextView>(R.id.text)!! | ||||||
|  |  | ||||||
|  |         override fun getSwipeableContainerView() = container | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) } | ||||||
|  |         swipeableViewContainerOnClickListener = View.OnClickListener { view -> onSwipeableViewContainerClick(view) } | ||||||
|  |  | ||||||
|  |         // SwipeableItemAdapter requires stable ID, and also | ||||||
|  |         // have to implement the getItemId() method appropriately. | ||||||
|  |         setHasStableIds(true) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun add(item: Plaintext) { | ||||||
|  |         data.add(item) | ||||||
|  |         notifyDataSetChanged() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun addFirst(item: Plaintext) { | ||||||
|  |         val index = data.size | ||||||
|  |         data.addFirst(item) | ||||||
|  |         notifyItemInserted(index) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun addAll(items: Collection<Plaintext>) { | ||||||
|  |         val index = data.size | ||||||
|  |         data.addAll(items) | ||||||
|  |         notifyItemRangeInserted(index, items.size) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun remove(item: Plaintext) { | ||||||
|  |         val index = data.indexOf(item) | ||||||
|  |         data.removeAll { it.id == item.id } | ||||||
|  |         notifyItemRemoved(index) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun update(item: Plaintext) { | ||||||
|  |         val index = data.indexOfFirst { it.id == item.id } | ||||||
|  |         if (index >= 0) { | ||||||
|  |             data[index] = item | ||||||
|  |             notifyItemChanged(index) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun clear(newLabel: Label?) { | ||||||
|  |         label = newLabel | ||||||
|  |         data.clear() | ||||||
|  |         notifyDataSetChanged() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun onItemViewClick(v: View) { | ||||||
|  |         eventListener?.onItemViewClicked(v) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun onSwipeableViewContainerClick(v: View) { | ||||||
|  |         eventListener?.onItemViewClicked( | ||||||
|  |             RecyclerViewAdapterUtils.getParentViewHolderItemView(v)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getItem(position: Int) = data[position] | ||||||
|  |  | ||||||
|  |     override fun getItemId(position: Int) = data[position].id as Long | ||||||
|  |  | ||||||
|  |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||||||
|  |         val inflater = LayoutInflater.from(parent.context) | ||||||
|  |         val v = inflater.inflate(R.layout.message_row, parent, false) | ||||||
|  |         return ViewHolder(v) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||||||
|  |         val item = data[position] | ||||||
|  |  | ||||||
|  |         holder.apply { | ||||||
|  |             if (activateOnItemClick) { | ||||||
|  |                 container.setBackgroundResource( | ||||||
|  |                     if (position == selectedPosition) | ||||||
|  |                         R.drawable.bg_item_selected_state | ||||||
|  |                     else | ||||||
|  |                         R.drawable.bg_item_normal_state | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // set listeners | ||||||
|  |             // (if the item is *pinned*, click event comes to the itemView) | ||||||
|  |             itemView.setOnClickListener(itemViewOnClickListener) | ||||||
|  |             // (if the item is *not pinned*, click event comes to the container) | ||||||
|  |             container.setOnClickListener(swipeableViewContainerOnClickListener) | ||||||
|  |  | ||||||
|  |             // set data | ||||||
|  |             avatar.setImageDrawable(Identicon(item.from)) | ||||||
|  |             status.setImageResource(Assets.getStatusDrawable(item.status)) | ||||||
|  |             status.contentDescription = holder.status.context.getString(Assets.getStatusString(item.status)) | ||||||
|  |             sender.text = item.from.toString() | ||||||
|  |             subject.text = prepareMessageExtract(item.subject) | ||||||
|  |             extract.text = prepareMessageExtract(item.text) | ||||||
|  |             if (item.isUnread()) { | ||||||
|  |                 sender.typeface = Typeface.DEFAULT_BOLD | ||||||
|  |                 subject.typeface = Typeface.DEFAULT_BOLD | ||||||
|  |             } else { | ||||||
|  |                 sender.typeface = Typeface.DEFAULT | ||||||
|  |                 subject.typeface = Typeface.DEFAULT | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getItemCount() = data.size | ||||||
|  |  | ||||||
|  |     override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int = | ||||||
|  |         if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) { | ||||||
|  |             REACTION_CAN_SWIPE_LEFT or REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT | ||||||
|  |         } else { | ||||||
|  |             REACTION_CAN_SWIPE_BOTH_H | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     @SuppressLint("SwitchIntDef") | ||||||
|  |     override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) = | ||||||
|  |         holder.itemView.setBackgroundResource(when (type) { | ||||||
|  |             DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral | ||||||
|  |             DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left | ||||||
|  |             DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) { | ||||||
|  |                 R.drawable.bg_swipe_item_neutral | ||||||
|  |             } else { | ||||||
|  |                 R.drawable.bg_swipe_item_right | ||||||
|  |             } | ||||||
|  |             else -> R.drawable.bg_swipe_item_neutral | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |     @SuppressLint("SwitchIntDef") | ||||||
|  |     override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) = | ||||||
|  |         when (result) { | ||||||
|  |             RESULT_SWIPED_RIGHT -> SwipeRightResultAction(this, position) | ||||||
|  |             RESULT_SWIPED_LEFT -> SwipeLeftResultAction(this, position) | ||||||
|  |             else -> null | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     override fun onSwipeItemStarted(holder: ViewHolder?, position: Int) = Unit | ||||||
|  |  | ||||||
|  |     fun setSelectedPosition(selectedPosition: Int) { | ||||||
|  |         val oldPosition = this.selectedPosition | ||||||
|  |         this.selectedPosition = selectedPosition | ||||||
|  |         notifyItemChanged(oldPosition) | ||||||
|  |         notifyItemChanged(selectedPosition) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private class SwipeLeftResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionMoveToSwipedDirection() { | ||||||
|  |         private var adapter: SwipeableMessageAdapter? = adapter | ||||||
|  |         private val item = adapter.data[position] | ||||||
|  |  | ||||||
|  |         override fun onPerformAction() { | ||||||
|  |             adapter?.eventListener?.onItemDeleted(item) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override fun onCleanUp() { | ||||||
|  |             adapter = null | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private class SwipeRightResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionRemoveItem() { | ||||||
|  |         private var adapter: SwipeableMessageAdapter? = adapter | ||||||
|  |         private val item = adapter.data[position] | ||||||
|  |  | ||||||
|  |         override fun onPerformAction() { | ||||||
|  |             adapter?.eventListener?.onItemArchived(item) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override fun onCleanUp() { | ||||||
|  |             adapter = null | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,65 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.adapter; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.SharedPreferences; |  | ||||||
| import android.preference.PreferenceManager; |  | ||||||
|  |  | ||||||
| import java.util.Arrays; |  | ||||||
|  |  | ||||||
| import ch.dissem.bitmessage.InternalContext; |  | ||||||
| import ch.dissem.bitmessage.ports.ProofOfWorkEngine; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Switches between two {@link ProofOfWorkEngine}s depending on the configuration. |  | ||||||
|  * |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public class SwitchingProofOfWorkEngine implements ProofOfWorkEngine, InternalContext.ContextHolder { |  | ||||||
|     private final Context ctx; |  | ||||||
|     private final String preference; |  | ||||||
|     private final ProofOfWorkEngine option; |  | ||||||
|     private final ProofOfWorkEngine fallback; |  | ||||||
|  |  | ||||||
|     public SwitchingProofOfWorkEngine(Context ctx, String preference, |  | ||||||
|                                       ProofOfWorkEngine option, ProofOfWorkEngine fallback) { |  | ||||||
|         this.ctx = ctx; |  | ||||||
|         this.preference = preference; |  | ||||||
|         this.option = option; |  | ||||||
|         this.fallback = fallback; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { |  | ||||||
|         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); |  | ||||||
|         if (preferences.getBoolean(preference, false)) { |  | ||||||
|             option.calculateNonce(initialHash, target, callback); |  | ||||||
|         } else { |  | ||||||
|             fallback.calculateNonce(initialHash, target, callback); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void setContext(InternalContext context) { |  | ||||||
|         for (ProofOfWorkEngine e : Arrays.asList(option, fallback)) { |  | ||||||
|             if (e instanceof InternalContext.ContextHolder) { |  | ||||||
|                 ((InternalContext.ContextHolder) e).setContext(context); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,48 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.adapter | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.preference.PreferenceManager | ||||||
|  | import ch.dissem.bitmessage.InternalContext | ||||||
|  | import ch.dissem.bitmessage.ports.ProofOfWorkEngine | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Switches between two [ProofOfWorkEngine]s depending on the configuration. | ||||||
|  |  * | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class SwitchingProofOfWorkEngine( | ||||||
|  |         private val ctx: Context, | ||||||
|  |         private val preference: String, | ||||||
|  |         private val option: ProofOfWorkEngine, | ||||||
|  |         private val fallback: ProofOfWorkEngine | ||||||
|  | ) : ProofOfWorkEngine, InternalContext.ContextHolder { | ||||||
|  |  | ||||||
|  |     override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) { | ||||||
|  |         val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) | ||||||
|  |         if (preferences.getBoolean(preference, false)) { | ||||||
|  |             option.calculateNonce(initialHash, target, callback) | ||||||
|  |         } else { | ||||||
|  |             fallback.calculateNonce(initialHash, target, callback) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun setContext(context: InternalContext) = listOf(option, fallback) | ||||||
|  |             .filterIsInstance<InternalContext.ContextHolder>() | ||||||
|  |             .forEach { it.setContext(context) } | ||||||
|  | } | ||||||
| @@ -1,162 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.dialog; |  | ||||||
|  |  | ||||||
| import android.annotation.SuppressLint; |  | ||||||
| import android.app.AlertDialog; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.DialogInterface; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.AsyncTask; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.annotation.Nullable; |  | ||||||
| import android.support.v4.app.FragmentActivity; |  | ||||||
| import android.support.v7.app.AppCompatDialogFragment; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.RadioGroup; |  | ||||||
| import android.widget.TextView; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.ImportIdentityActivity; |  | ||||||
| import ch.dissem.apps.abit.MainActivity; |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.Pubkey; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| public class AddIdentityDialogFragment extends AppCompatDialogFragment { |  | ||||||
|     private BitmessageContext bmc; |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onAttach(Context context) { |  | ||||||
|         super.onAttach(context); |  | ||||||
|         bmc = Singleton.getBitmessageContext(context); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle |  | ||||||
|         savedInstanceState) { |  | ||||||
|         getDialog().setTitle(R.string.add_identity); |  | ||||||
|         View view = inflater.inflate(R.layout.dialog_add_identity, container, false); |  | ||||||
|         final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroup); |  | ||||||
|         view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 final Context ctx = getActivity().getBaseContext(); |  | ||||||
|                 switch (radioGroup.getCheckedRadioButtonId()) { |  | ||||||
|                     case R.id.create_identity: |  | ||||||
|                         Toast.makeText(ctx, |  | ||||||
|                             R.string.toast_long_running_operation, |  | ||||||
|                             Toast.LENGTH_SHORT).show(); |  | ||||||
|                         new AsyncTask<Void, Void, BitmessageAddress>() { |  | ||||||
|                             @Override |  | ||||||
|                             protected BitmessageAddress doInBackground(Void... args) { |  | ||||||
|                                 return bmc.createIdentity(false, Pubkey.Feature.DOES_ACK); |  | ||||||
|                             } |  | ||||||
|  |  | ||||||
|                             @Override |  | ||||||
|                             protected void onPostExecute(BitmessageAddress chan) { |  | ||||||
|                                 Toast.makeText(ctx, |  | ||||||
|                                     R.string.toast_identity_created, |  | ||||||
|                                     Toast.LENGTH_SHORT).show(); |  | ||||||
|                                 MainActivity mainActivity = MainActivity.getInstance(); |  | ||||||
|                                 if (mainActivity != null) { |  | ||||||
|                                     mainActivity.addIdentityEntry(chan); |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         }.execute(); |  | ||||||
|                         break; |  | ||||||
|                     case R.id.import_identity: |  | ||||||
|                         startActivity(new Intent(ctx, ImportIdentityActivity.class)); |  | ||||||
|                         break; |  | ||||||
|                     case R.id.add_chan: |  | ||||||
|                         addChanDialog(); |  | ||||||
|                         break; |  | ||||||
|                     case R.id.add_deterministic_address: |  | ||||||
|                         new DeterministicIdentityDialogFragment().show(getFragmentManager(), |  | ||||||
|                             "dialog"); |  | ||||||
|                         break; |  | ||||||
|                     default: |  | ||||||
|                         return; |  | ||||||
|                 } |  | ||||||
|                 dismiss(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 dismiss(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         return view; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void addChanDialog() { |  | ||||||
|         FragmentActivity activity = getActivity(); |  | ||||||
|         final Context ctx = activity.getBaseContext(); |  | ||||||
|         @SuppressLint("InflateParams") |  | ||||||
|         final View dialogView = activity.getLayoutInflater() |  | ||||||
|             .inflate(R.layout.dialog_input_passphrase, null); |  | ||||||
|         new AlertDialog.Builder(activity) |  | ||||||
|             .setTitle(R.string.add_chan) |  | ||||||
|             .setView(dialogView) |  | ||||||
|             .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |  | ||||||
|                 @Override |  | ||||||
|                 public void onClick(DialogInterface dialogInterface, int i) { |  | ||||||
|                     TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); |  | ||||||
|                     Toast.makeText(ctx, R.string.toast_long_running_operation, |  | ||||||
|                         Toast.LENGTH_SHORT).show(); |  | ||||||
|                     new AsyncTask<String, Void, BitmessageAddress>() { |  | ||||||
|                         @Override |  | ||||||
|                         protected BitmessageAddress doInBackground(String... args) { |  | ||||||
|                             String pass = args[0]; |  | ||||||
|                             BitmessageAddress chan = bmc.createChan(pass); |  | ||||||
|                             chan.setAlias(pass); |  | ||||||
|                             bmc.addresses().save(chan); |  | ||||||
|                             return chan; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         @Override |  | ||||||
|                         protected void onPostExecute(BitmessageAddress chan) { |  | ||||||
|                             Toast.makeText(ctx, |  | ||||||
|                                 R.string.toast_chan_created, |  | ||||||
|                                 Toast.LENGTH_SHORT).show(); |  | ||||||
|                             MainActivity mainActivity = MainActivity.getInstance(); |  | ||||||
|                             if (mainActivity != null) { |  | ||||||
|                                 mainActivity.addIdentityEntry(chan); |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     }.execute(passphrase.getText().toString()); |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|             .setNegativeButton(R.string.cancel, null) |  | ||||||
|             .show(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public int getTheme() { |  | ||||||
|         return R.style.FixedDialog; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,119 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.dialog | ||||||
|  |  | ||||||
|  | import android.app.AlertDialog | ||||||
|  | import android.content.Context | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v7.app.AppCompatDialogFragment | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.TextView | ||||||
|  | import android.widget.Toast | ||||||
|  | import ch.dissem.apps.abit.ImportIdentityActivity | ||||||
|  | import ch.dissem.apps.abit.MainActivity | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.bitmessage.BitmessageContext | ||||||
|  | import ch.dissem.bitmessage.entity.payload.Pubkey | ||||||
|  | import kotlinx.android.synthetic.main.dialog_add_identity.* | ||||||
|  | import org.jetbrains.anko.doAsync | ||||||
|  | import org.jetbrains.anko.support.v4.startActivity | ||||||
|  | import org.jetbrains.anko.uiThread | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | class AddIdentityDialogFragment : AppCompatDialogFragment() { | ||||||
|  |     private lateinit var bmc: BitmessageContext | ||||||
|  |     private var parent: ViewGroup? = null | ||||||
|  |  | ||||||
|  |     override fun onAttach(context: Context?) { | ||||||
|  |         super.onAttach(context) | ||||||
|  |         bmc = Singleton.getBitmessageContext(context!!) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { | ||||||
|  |         dialog.setTitle(R.string.add_identity) | ||||||
|  |         parent = container | ||||||
|  |         return inflater.inflate(R.layout.dialog_add_identity, container, false) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |         ok.setOnClickListener(View.OnClickListener { | ||||||
|  |             val ctx = activity?.baseContext ?: throw IllegalStateException("No context available") | ||||||
|  |  | ||||||
|  |             when (radioGroup.checkedRadioButtonId) { | ||||||
|  |                 R.id.create_identity -> { | ||||||
|  |                     Toast.makeText(ctx, | ||||||
|  |                             R.string.toast_long_running_operation, | ||||||
|  |                             Toast.LENGTH_SHORT).show() | ||||||
|  |                     doAsync { | ||||||
|  |                         val identity = bmc.createIdentity(false, Pubkey.Feature.DOES_ACK) | ||||||
|  |                         uiThread { | ||||||
|  |                             Toast.makeText(ctx, | ||||||
|  |                                     R.string.toast_identity_created, | ||||||
|  |                                     Toast.LENGTH_SHORT).show() | ||||||
|  |                             MainActivity.apply { | ||||||
|  |                                 addIdentityEntry(identity) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 R.id.import_identity -> startActivity<ImportIdentityActivity>() | ||||||
|  |                 R.id.add_chan -> addChanDialog() | ||||||
|  |                 R.id.add_deterministic_address -> DeterministicIdentityDialogFragment().show(fragmentManager, "dialog") | ||||||
|  |                 else -> return@OnClickListener | ||||||
|  |             } | ||||||
|  |             dismiss() | ||||||
|  |         }) | ||||||
|  |         dismiss.setOnClickListener { dismiss() } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun addChanDialog() { | ||||||
|  |         val activity = activity ?: throw IllegalStateException("No activity available") | ||||||
|  |         val ctx = activity.baseContext ?: throw IllegalStateException("No context available") | ||||||
|  |         val dialogView = activity.layoutInflater.inflate(R.layout.dialog_input_passphrase, parent) | ||||||
|  |         AlertDialog.Builder(activity) | ||||||
|  |                 .setTitle(R.string.add_chan) | ||||||
|  |                 .setView(dialogView) | ||||||
|  |                 .setPositiveButton(R.string.ok) { _, _ -> | ||||||
|  |                     val passphrase = dialogView.findViewById<TextView>(R.id.passphrase) | ||||||
|  |                     Toast.makeText(ctx, R.string.toast_long_running_operation, | ||||||
|  |                             Toast.LENGTH_SHORT).show() | ||||||
|  |                     val pass = passphrase.text.toString() | ||||||
|  |                     doAsync { | ||||||
|  |                         val chan = bmc.createChan(pass) | ||||||
|  |                         chan.alias = pass | ||||||
|  |                         bmc.addresses.save(chan) | ||||||
|  |                         uiThread { | ||||||
|  |                             Toast.makeText(ctx, | ||||||
|  |                                     R.string.toast_chan_created, | ||||||
|  |                                     Toast.LENGTH_SHORT).show() | ||||||
|  |                             MainActivity.apply { addIdentityEntry(chan) } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .setNegativeButton(R.string.cancel, null) | ||||||
|  |                 .show() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getTheme() = R.style.FixedDialog | ||||||
|  | } | ||||||
| @@ -1,136 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.dialog; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.os.AsyncTask; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.annotation.Nullable; |  | ||||||
| import android.support.v7.app.AppCompatDialogFragment; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.Switch; |  | ||||||
| import android.widget.TextView; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.MainActivity; |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.Pubkey; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public class DeterministicIdentityDialogFragment extends AppCompatDialogFragment { |  | ||||||
|     private BitmessageContext bmc; |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onAttach(Context context) { |  | ||||||
|         super.onAttach(context); |  | ||||||
|         bmc = Singleton.getBitmessageContext(context); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle |  | ||||||
|         savedInstanceState) { |  | ||||||
|         getDialog().setTitle(R.string.add_deterministic_address); |  | ||||||
|         View view = inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false); |  | ||||||
|         view.findViewById(R.id.ok) |  | ||||||
|             .setOnClickListener(new View.OnClickListener() { |  | ||||||
|                 @Override |  | ||||||
|                 public void onClick(View view) { |  | ||||||
|                     dismiss(); |  | ||||||
|                     final Context context = getActivity().getBaseContext(); |  | ||||||
|                     View dialogView = getView(); |  | ||||||
|                     assert dialogView != null; |  | ||||||
|                     TextView label = (TextView) dialogView.findViewById(R.id.label); |  | ||||||
|                     TextView passphrase = (TextView) dialogView.findViewById(R.id.passphrase); |  | ||||||
|                     TextView numberOfAddresses = (TextView) dialogView.findViewById(R.id |  | ||||||
|                         .number_of_identities); |  | ||||||
|                     Switch shorter = (Switch) dialogView.findViewById(R.id.shorter); |  | ||||||
|  |  | ||||||
|                     Toast.makeText(context, R.string.toast_long_running_operation, |  | ||||||
|                         Toast.LENGTH_SHORT).show(); |  | ||||||
|                     new AsyncTask<Object, Void, List<BitmessageAddress>>() { |  | ||||||
|                         @Override |  | ||||||
|                         protected List<BitmessageAddress> doInBackground(Object... args) { |  | ||||||
|                             String label = (String) args[0]; |  | ||||||
|                             String pass = (String) args[1]; |  | ||||||
|                             int numberOfAddresses = (int) args[2]; |  | ||||||
|                             boolean shorter = (boolean) args[3]; |  | ||||||
|                             List<BitmessageAddress> identities = bmc.createDeterministicAddresses |  | ||||||
|                                 (pass, |  | ||||||
|                                     numberOfAddresses, Pubkey.LATEST_VERSION, 1L, shorter); |  | ||||||
|                             int i = 0; |  | ||||||
|                             for (BitmessageAddress identity : identities) { |  | ||||||
|                                 i++; |  | ||||||
|                                 if (identities.size() == 1) { |  | ||||||
|                                     identity.setAlias(label); |  | ||||||
|                                 } else { |  | ||||||
|                                     identity.setAlias(label + " (" + i + ")"); |  | ||||||
|                                 } |  | ||||||
|                                 bmc.addresses().save(identity); |  | ||||||
|                             } |  | ||||||
|                             return identities; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         @Override |  | ||||||
|                         protected void onPostExecute(List<BitmessageAddress> identities) { |  | ||||||
|                             int messageRes; |  | ||||||
|                             if (identities.size() == 1) { |  | ||||||
|                                 messageRes = R.string.toast_identity_created; |  | ||||||
|                             } else { |  | ||||||
|                                 messageRes = R.string.toast_identities_created; |  | ||||||
|                             } |  | ||||||
|                             Toast.makeText(context, |  | ||||||
|                                 messageRes, |  | ||||||
|                                 Toast.LENGTH_SHORT).show(); |  | ||||||
|                             MainActivity mainActivity = MainActivity.getInstance(); |  | ||||||
|                             if (mainActivity != null) { |  | ||||||
|                                 for (BitmessageAddress identity : identities) { |  | ||||||
|                                     mainActivity.addIdentityEntry(identity); |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     }.execute( |  | ||||||
|                         label.getText().toString(), |  | ||||||
|                         passphrase.getText().toString(), |  | ||||||
|                         Integer.valueOf(numberOfAddresses.getText().toString()), |  | ||||||
|                         shorter.isChecked() |  | ||||||
|                     ); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 dismiss(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         return view; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public int getTheme() { |  | ||||||
|         return R.style.FixedDialog; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,96 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.dialog | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v7.app.AppCompatDialogFragment | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.Toast | ||||||
|  | import ch.dissem.apps.abit.MainActivity | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.bitmessage.BitmessageContext | ||||||
|  | import ch.dissem.bitmessage.entity.payload.Pubkey | ||||||
|  | import kotlinx.android.synthetic.main.dialog_add_deterministic_identity.* | ||||||
|  | import org.jetbrains.anko.doAsync | ||||||
|  | import org.jetbrains.anko.uiThread | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class DeterministicIdentityDialogFragment : AppCompatDialogFragment() { | ||||||
|  |     private lateinit var bmc: BitmessageContext | ||||||
|  |  | ||||||
|  |     override fun onAttach(context: Context?) { | ||||||
|  |         super.onAttach(context) | ||||||
|  |         bmc = Singleton.getBitmessageContext(context!!) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { | ||||||
|  |         dialog.setTitle(R.string.add_deterministic_address) | ||||||
|  |         return inflater.inflate(R.layout.dialog_add_deterministic_identity, container, false) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(dialogView: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(dialogView, savedInstanceState) | ||||||
|  |         ok.setOnClickListener { | ||||||
|  |             dismiss() | ||||||
|  |             val context = activity?.baseContext ?: throw IllegalStateException("No context available") | ||||||
|  |             val passphraseText = passphrase.text.toString() | ||||||
|  |  | ||||||
|  |             Toast.makeText(context, R.string.toast_long_running_operation, Toast.LENGTH_SHORT).show() | ||||||
|  |             doAsync { | ||||||
|  |                 val identities = bmc.createDeterministicAddresses( | ||||||
|  |                         passphraseText, | ||||||
|  |                         number_of_identities.text.toString().toInt(), | ||||||
|  |                         Pubkey.LATEST_VERSION, | ||||||
|  |                         1L, | ||||||
|  |                         shorter.isChecked | ||||||
|  |                 ) | ||||||
|  |                 for ((i, identity) in identities.withIndex()) { | ||||||
|  |                     if (identities.size == 1) { | ||||||
|  |                         identity.alias = label.text.toString() | ||||||
|  |                     } else { | ||||||
|  |                         identity.alias = "${label.text} (${i + 1})" | ||||||
|  |                     } | ||||||
|  |                     bmc.addresses.save(identity) | ||||||
|  |                 } | ||||||
|  |                 uiThread { | ||||||
|  |                     val messageRes = if (identities.size == 1) { | ||||||
|  |                         R.string.toast_identity_created | ||||||
|  |                     } else { | ||||||
|  |                         R.string.toast_identities_created | ||||||
|  |                     } | ||||||
|  |                     Toast.makeText(context, | ||||||
|  |                             messageRes, | ||||||
|  |                             Toast.LENGTH_SHORT).show() | ||||||
|  |                     MainActivity.apply { | ||||||
|  |                         identities.forEach { identity -> | ||||||
|  |                             addIdentityEntry(identity) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         dismiss.setOnClickListener { dismiss() } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getTheme() = R.style.FixedDialog | ||||||
|  | } | ||||||
| @@ -1,53 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.dialog; |  | ||||||
|  |  | ||||||
| import android.app.Activity; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.view.View; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
| import ch.dissem.apps.abit.service.BitmessageService; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| public class FullNodeDialogActivity extends Activity { |  | ||||||
|     @Override |  | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |  | ||||||
|         super.onCreate(savedInstanceState); |  | ||||||
|         setContentView(R.layout.dialog_full_node); |  | ||||||
|         findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 startService(new Intent(FullNodeDialogActivity.this, BitmessageService.class)); |  | ||||||
|                 updateNodeSwitch(); |  | ||||||
|                 finish(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 finish(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,46 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.dialog | ||||||
|  |  | ||||||
|  | import android.app.Activity | ||||||
|  | import android.os.Build | ||||||
|  | import android.os.Bundle | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.apps.abit.util.NetworkUtils | ||||||
|  | import ch.dissem.apps.abit.util.Preferences | ||||||
|  | import kotlinx.android.synthetic.main.dialog_full_node.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class FullNodeDialogActivity : Activity() { | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         setContentView(R.layout.dialog_full_node) | ||||||
|  |         ok.setOnClickListener { | ||||||
|  |             Preferences.setWifiOnly(this@FullNodeDialogActivity, false) | ||||||
|  |             NetworkUtils.enableNode(applicationContext) | ||||||
|  |             finish() | ||||||
|  |         } | ||||||
|  |         dismiss.setOnClickListener { | ||||||
|  |             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||||
|  |                 NetworkUtils.scheduleNodeStart(applicationContext) | ||||||
|  |             } | ||||||
|  |             finish() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,98 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.dialog; |  | ||||||
|  |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Bundle; |  | ||||||
| import android.support.annotation.Nullable; |  | ||||||
| import android.support.v7.app.AppCompatDialogFragment; |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| import android.widget.RadioGroup; |  | ||||||
|  |  | ||||||
| import org.slf4j.Logger; |  | ||||||
| import org.slf4j.LoggerFactory; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
|  |  | ||||||
| import static android.app.Activity.RESULT_OK; |  | ||||||
| import static ch.dissem.apps.abit.ComposeMessageActivity.EXTRA_ENCODING; |  | ||||||
| import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED; |  | ||||||
| import static ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| public class SelectEncodingDialogFragment extends AppCompatDialogFragment { |  | ||||||
|     private static final Logger LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment.class); |  | ||||||
|     private Plaintext.Encoding encoding; |  | ||||||
|  |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |  | ||||||
|         if (getArguments() != null && getArguments().containsKey(EXTRA_ENCODING)) { |  | ||||||
|             encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING); |  | ||||||
|         } |  | ||||||
|         if (encoding == null) { |  | ||||||
|             encoding = SIMPLE; |  | ||||||
|         } |  | ||||||
|         getDialog().setTitle(R.string.select_encoding_title); |  | ||||||
|         View view = inflater.inflate(R.layout.dialog_select_message_encoding, container, false); |  | ||||||
|         final RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroup); |  | ||||||
|         switch (encoding) { |  | ||||||
|             case SIMPLE: |  | ||||||
|                 radioGroup.check(R.id.simple); |  | ||||||
|                 break; |  | ||||||
|             case EXTENDED: |  | ||||||
|                 radioGroup.check(R.id.extended); |  | ||||||
|                 break; |  | ||||||
|             default: |  | ||||||
|                 LOG.warn("Unexpected encoding: " + encoding); |  | ||||||
|                 break; |  | ||||||
|         } |  | ||||||
|         view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 switch (radioGroup.getCheckedRadioButtonId()) { |  | ||||||
|                     case R.id.extended: |  | ||||||
|                         encoding = EXTENDED; |  | ||||||
|                         break; |  | ||||||
|                     case R.id.simple: |  | ||||||
|                         encoding = SIMPLE; |  | ||||||
|                         break; |  | ||||||
|                     default: |  | ||||||
|                         dismiss(); |  | ||||||
|                         return; |  | ||||||
|                 } |  | ||||||
|                 Intent result = new Intent(); |  | ||||||
|                 result.putExtra(EXTRA_ENCODING, encoding); |  | ||||||
|                 getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, result); |  | ||||||
|                 dismiss(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         view.findViewById(R.id.dismiss).setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 dismiss(); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         return view; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,74 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.dialog | ||||||
|  |  | ||||||
|  | import android.app.Activity.RESULT_OK | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.support.v7.app.AppCompatDialogFragment | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE | ||||||
|  | import kotlinx.android.synthetic.main.dialog_select_message_encoding.* | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | class SelectEncodingDialogFragment : AppCompatDialogFragment() { | ||||||
|  |     private lateinit var encoding: Plaintext.Encoding | ||||||
|  |  | ||||||
|  |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { | ||||||
|  |         encoding = (arguments?.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding) ?: SIMPLE | ||||||
|  |         dialog.setTitle(R.string.select_encoding_title) | ||||||
|  |         return inflater.inflate(R.layout.dialog_select_message_encoding, container, false) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |         when (encoding) { | ||||||
|  |             SIMPLE -> radioGroup.check(R.id.simple) | ||||||
|  |             EXTENDED -> radioGroup.check(R.id.extended) | ||||||
|  |             else -> LOG.warn("Unexpected encoding: " + encoding) | ||||||
|  |         } | ||||||
|  |         ok.setOnClickListener(View.OnClickListener { | ||||||
|  |             encoding = when (radioGroup.checkedRadioButtonId) { | ||||||
|  |                 R.id.extended -> EXTENDED | ||||||
|  |                 R.id.simple -> SIMPLE | ||||||
|  |                 else -> { | ||||||
|  |                     dismiss() | ||||||
|  |                     return@OnClickListener | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             val result = Intent() | ||||||
|  |             result.putExtra(EXTRA_ENCODING, encoding) | ||||||
|  |             targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, result) | ||||||
|  |             dismiss() | ||||||
|  |         }) | ||||||
|  |         dismiss.setOnClickListener { dismiss() } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val LOG = LoggerFactory.getLogger(SelectEncodingDialogFragment::class.java) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,56 @@ | |||||||
|  | package ch.dissem.apps.abit.drawer | ||||||
|  |  | ||||||
|  | import android.app.Dialog | ||||||
|  | import android.content.Context | ||||||
|  | import android.graphics.Point | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.view.Window | ||||||
|  | import android.view.WindowManager | ||||||
|  | import android.widget.ImageView | ||||||
|  | import android.widget.RelativeLayout | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.apps.abit.util.Drawables | ||||||
|  | import com.mikepenz.materialdrawer.AccountHeader | ||||||
|  | import com.mikepenz.materialdrawer.model.interfaces.IProfile | ||||||
|  |  | ||||||
|  | class ProfileImageListener(private val ctx: Context) : AccountHeader.OnAccountHeaderProfileImageListener { | ||||||
|  |  | ||||||
|  |     override fun onProfileImageClick(view: View, profile: IProfile<*>, current: Boolean): Boolean { | ||||||
|  |         if (current) { | ||||||
|  |             //  Show QR code in modal dialog | ||||||
|  |             val dialog = Dialog(ctx) | ||||||
|  |             dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) | ||||||
|  |  | ||||||
|  |             val imageView = ImageView(ctx) | ||||||
|  |             imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(ctx))) | ||||||
|  |             imageView.setOnClickListener { dialog.dismiss() } | ||||||
|  |             dialog.addContentView( | ||||||
|  |                     imageView, | ||||||
|  |                     RelativeLayout.LayoutParams( | ||||||
|  |                             ViewGroup.LayoutParams.MATCH_PARENT, | ||||||
|  |                             ViewGroup.LayoutParams.MATCH_PARENT | ||||||
|  |                     ) | ||||||
|  |             ) | ||||||
|  |             val window = dialog.window | ||||||
|  |             if (window != null) { | ||||||
|  |                 val display = window.windowManager.defaultDisplay | ||||||
|  |                 val size = Point() | ||||||
|  |                 display.getSize(size) | ||||||
|  |                 val dim = if (size.x < size.y) size.x else size.y | ||||||
|  |  | ||||||
|  |                 val lp = WindowManager.LayoutParams() | ||||||
|  |                 lp.copyFrom(window.attributes) | ||||||
|  |                 lp.width = dim | ||||||
|  |                 lp.height = dim | ||||||
|  |  | ||||||
|  |                 window.attributes = lp | ||||||
|  |             } | ||||||
|  |             dialog.show() | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onProfileImageLongClick(view: View, iProfile: IProfile<*>, b: Boolean) = false | ||||||
|  | } | ||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | package ch.dissem.apps.abit.drawer | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.support.v4.app.FragmentManager | ||||||
|  | import android.view.View | ||||||
|  | import android.widget.Toast | ||||||
|  |  | ||||||
|  | import com.mikepenz.materialdrawer.AccountHeader | ||||||
|  | import com.mikepenz.materialdrawer.model.ProfileDrawerItem | ||||||
|  | import com.mikepenz.materialdrawer.model.interfaces.IProfile | ||||||
|  |  | ||||||
|  | import ch.dissem.apps.abit.AddressDetailActivity | ||||||
|  | import ch.dissem.apps.abit.AddressDetailFragment | ||||||
|  | import ch.dissem.apps.abit.MainActivity | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  |  | ||||||
|  | import android.widget.Toast.LENGTH_LONG | ||||||
|  |  | ||||||
|  | class ProfileSelectionListener( | ||||||
|  |         private val ctx: Context, | ||||||
|  |         private val fragmentManager: FragmentManager | ||||||
|  | ) : AccountHeader.OnAccountHeaderListener { | ||||||
|  |  | ||||||
|  |     override fun onProfileChanged(view: View, profile: IProfile<*>, current: Boolean): Boolean { | ||||||
|  |         when (profile.identifier.toInt()) { | ||||||
|  |             MainActivity.ADD_IDENTITY -> addIdentityDialog() | ||||||
|  |             MainActivity.MANAGE_IDENTITY -> { | ||||||
|  |                 val identity = Singleton.getIdentity(ctx) | ||||||
|  |                 if (identity == null) { | ||||||
|  |                     Toast.makeText(ctx, R.string.no_identity_warning, LENGTH_LONG).show() | ||||||
|  |                 } else { | ||||||
|  |                     val show = Intent(ctx, AddressDetailActivity::class.java) | ||||||
|  |                     show.putExtra(AddressDetailFragment.ARG_ITEM, identity) | ||||||
|  |                     ctx.startActivity(show) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             else -> if (profile is ProfileDrawerItem) { | ||||||
|  |                 val tag = profile.tag | ||||||
|  |                 if (tag is BitmessageAddress) { | ||||||
|  |                     Singleton.setIdentity(tag) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         // false if it should close the drawer | ||||||
|  |         return false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun addIdentityDialog() = AddIdentityDialogFragment().show(fragmentManager, "dialog") | ||||||
|  | } | ||||||
| @@ -1,26 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.listener; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public interface ActionBarListener { |  | ||||||
|     void updateTitle(CharSequence title); |  | ||||||
|  |  | ||||||
|     void updateUnread(); |  | ||||||
| } |  | ||||||
| @@ -14,16 +14,16 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package ch.dissem.apps.abit.listener; | package ch.dissem.apps.abit.listener | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * A callback interface that all activities containing this fragment must |  * A callback interface that all activities containing this fragment must | ||||||
|  * implement. This mechanism allows activities to be notified of item |  * implement. This mechanism allows activities to be notified of item | ||||||
|  * selections. |  * selections. | ||||||
|  */ |  */ | ||||||
| public interface ListSelectionListener<T> { | interface ListSelectionListener<in T> { | ||||||
|     /** |     /** | ||||||
|      * Callback for when an item has been selected. |      * Callback for when an item has been selected. | ||||||
|      */ |      */ | ||||||
|     void onItemSelected(T item); |     fun onItemSelected(item: T) | ||||||
| } | } | ||||||
| @@ -1,85 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.listener; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
|  |  | ||||||
| import java.util.Deque; |  | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.concurrent.ExecutorService; |  | ||||||
| import java.util.concurrent.Executors; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.MainActivity; |  | ||||||
| import ch.dissem.apps.abit.notification.NewMessageNotification; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Listens for decrypted Bitmessage messages. Does show a notification. |  | ||||||
|  * <p> |  | ||||||
|  * Should show a notification when the app isn't running, but update the message list when it is. |  | ||||||
|  * Also, |  | ||||||
|  * notifications should be combined. |  | ||||||
|  * </p> |  | ||||||
|  */ |  | ||||||
| public class MessageListener implements BitmessageContext.Listener { |  | ||||||
|     private final Deque<Plaintext> unacknowledged = new LinkedList<>(); |  | ||||||
|     private int numberOfUnacknowledgedMessages = 0; |  | ||||||
|     private final NewMessageNotification notification; |  | ||||||
|     private final ExecutorService pool = Executors.newSingleThreadExecutor(); |  | ||||||
|  |  | ||||||
|     public MessageListener(Context ctx) { |  | ||||||
|         this.notification = new NewMessageNotification(ctx); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void receive(final Plaintext plaintext) { |  | ||||||
|         pool.submit(new Runnable() { |  | ||||||
|             @Override |  | ||||||
|             public void run() { |  | ||||||
|                 unacknowledged.addFirst(plaintext); |  | ||||||
|                 numberOfUnacknowledgedMessages++; |  | ||||||
|                 if (unacknowledged.size() > 5) { |  | ||||||
|                     unacknowledged.removeLast(); |  | ||||||
|                 } |  | ||||||
|                 if (numberOfUnacknowledgedMessages == 1) { |  | ||||||
|                     notification.singleNotification(plaintext); |  | ||||||
|                 } else { |  | ||||||
|                     notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages); |  | ||||||
|                 } |  | ||||||
|                 notification.show(); |  | ||||||
|  |  | ||||||
|                 // If MainActivity is shown, update the sidebar badges |  | ||||||
|                 MainActivity main = MainActivity.getInstance(); |  | ||||||
|                 if (main != null) { |  | ||||||
|                     main.updateUnread(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void resetNotification() { |  | ||||||
|         pool.submit(new Runnable() { |  | ||||||
|             @Override |  | ||||||
|             public void run() { |  | ||||||
|                 notification.hide(); |  | ||||||
|                 unacknowledged.clear(); |  | ||||||
|                 numberOfUnacknowledgedMessages = 0; |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,68 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.listener | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import ch.dissem.apps.abit.MainActivity | ||||||
|  | import ch.dissem.apps.abit.notification.NewMessageNotification | ||||||
|  | import ch.dissem.bitmessage.BitmessageContext | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  | import java.util.* | ||||||
|  | import java.util.concurrent.Executors | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Listens for decrypted Bitmessage messages. Does show a notification. | ||||||
|  |  * | ||||||
|  |  * | ||||||
|  |  * Should show a notification when the app isn't running, but update the message list when it is. | ||||||
|  |  * Also, | ||||||
|  |  * notifications should be combined. | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | class MessageListener(ctx: Context) : BitmessageContext.Listener { | ||||||
|  |     private val unacknowledged = LinkedList<Plaintext>() | ||||||
|  |     private var numberOfUnacknowledgedMessages = 0 | ||||||
|  |     private val notification = NewMessageNotification(ctx) | ||||||
|  |     private val pool = Executors.newSingleThreadExecutor() | ||||||
|  |  | ||||||
|  |     override fun receive(plaintext: Plaintext) { | ||||||
|  |         pool.submit { | ||||||
|  |             unacknowledged.addFirst(plaintext) | ||||||
|  |             numberOfUnacknowledgedMessages++ | ||||||
|  |             if (unacknowledged.size > 5) { | ||||||
|  |                 unacknowledged.removeLast() | ||||||
|  |             } | ||||||
|  |             if (numberOfUnacknowledgedMessages == 1) { | ||||||
|  |                 notification.singleNotification(plaintext) | ||||||
|  |             } else { | ||||||
|  |                 notification.multiNotification(unacknowledged, numberOfUnacknowledgedMessages) | ||||||
|  |             } | ||||||
|  |             notification.show() | ||||||
|  |  | ||||||
|  |             // If MainActivity is shown, update the sidebar badges | ||||||
|  |             MainActivity.apply { updateUnread() } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun resetNotification() { | ||||||
|  |         pool.submit { | ||||||
|  |             notification.hide() | ||||||
|  |             unacknowledged.clear() | ||||||
|  |             numberOfUnacknowledgedMessages = 0 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,62 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.listener; |  | ||||||
|  |  | ||||||
| import android.content.BroadcastReceiver; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.net.ConnectivityManager; |  | ||||||
| import android.net.NetworkInfo; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.apps.abit.util.Preferences; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
|  |  | ||||||
| public class WifiReceiver extends BroadcastReceiver { |  | ||||||
|     @Override |  | ||||||
|     public void onReceive(Context ctx, Intent intent) { |  | ||||||
|         if ("android.net.conn.CONNECTIVITY_CHANGE".equals(intent.getAction())) { |  | ||||||
|             if (Preferences.isWifiOnly(ctx)) { |  | ||||||
|                 BitmessageContext bmc = Singleton.getBitmessageContext(ctx); |  | ||||||
|  |  | ||||||
|                 if (isConnectedToMeteredNetwork(ctx) && bmc.isRunning()) { |  | ||||||
|                     bmc.shutdown(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static boolean isConnectedToMeteredNetwork(Context ctx) { |  | ||||||
|         NetworkInfo netInfo = getNetworkInfo(ctx); |  | ||||||
|         if (netInfo == null || !netInfo.isConnectedOrConnecting()) { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|         switch (netInfo.getType()) { |  | ||||||
|             case ConnectivityManager.TYPE_ETHERNET: |  | ||||||
|             case ConnectivityManager.TYPE_WIFI: |  | ||||||
|                 return false; |  | ||||||
|             default: |  | ||||||
|                 return true; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static NetworkInfo getNetworkInfo(Context ctx) { |  | ||||||
|         ConnectivityManager conMan = (ConnectivityManager) ctx.getSystemService(Context |  | ||||||
|             .CONNECTIVITY_SERVICE); |  | ||||||
|         return conMan.getActiveNetworkInfo(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.listener | ||||||
|  |  | ||||||
|  | import android.content.BroadcastReceiver | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import ch.dissem.apps.abit.service.BitmessageService | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.apps.abit.util.Preferences | ||||||
|  | import org.jetbrains.anko.connectivityManager | ||||||
|  |  | ||||||
|  | class WifiReceiver : BroadcastReceiver() { | ||||||
|  |     override fun onReceive(ctx: Context, intent: Intent) { | ||||||
|  |         if ("android.net.conn.CONNECTIVITY_CHANGE" == intent.action) { | ||||||
|  |             val bmc = Singleton.getBitmessageContext(ctx) | ||||||
|  |             if (Preferences.isFullNodeActive(ctx) && !bmc.isRunning() && !(Preferences.isWifiOnly(ctx) && ctx.connectivityManager.isActiveNetworkMetered)) { | ||||||
|  |                 ctx.startService(Intent(ctx, BitmessageService::class.java)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,53 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.notification; |  | ||||||
|  |  | ||||||
| import android.app.Notification; |  | ||||||
| import android.app.NotificationManager; |  | ||||||
| import android.content.Context; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Some base class to create and handle notifications. |  | ||||||
|  */ |  | ||||||
| public abstract class AbstractNotification { |  | ||||||
|     protected final Context ctx; |  | ||||||
|     protected final NotificationManager manager; |  | ||||||
|     protected Notification notification; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     public AbstractNotification(Context ctx) { |  | ||||||
|         this.ctx = ctx.getApplicationContext(); |  | ||||||
|         this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @return an id unique to this notification class |  | ||||||
|      */ |  | ||||||
|     protected abstract int getNotificationId(); |  | ||||||
|  |  | ||||||
|     public Notification getNotification() { |  | ||||||
|         return notification; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void show() { |  | ||||||
|         manager.notify(getNotificationId(), notification); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void hide() { |  | ||||||
|         manager.cancel(getNotificationId()); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,49 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.notification | ||||||
|  |  | ||||||
|  | import android.app.Notification | ||||||
|  | import android.app.NotificationManager | ||||||
|  | import android.content.Context | ||||||
|  | import org.jetbrains.anko.notificationManager | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Some base class to create and handle notifications. | ||||||
|  |  */ | ||||||
|  | abstract class AbstractNotification(ctx: Context) { | ||||||
|  |     protected val ctx = ctx.applicationContext!! | ||||||
|  |     private val manager = ctx.notificationManager | ||||||
|  |     var notification: Notification? = null | ||||||
|  |         protected set | ||||||
|  |     protected var showing = false | ||||||
|  |         private set | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @return an id unique to this notification class | ||||||
|  |      */ | ||||||
|  |     protected abstract val notificationId: Int | ||||||
|  |  | ||||||
|  |     open fun show() { | ||||||
|  |         manager.notify(notificationId, notification) | ||||||
|  |         showing = true | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun hide() { | ||||||
|  |         showing = false | ||||||
|  |         manager.cancel(notificationId) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,61 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.notification; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.support.annotation.StringRes; |  | ||||||
| import android.support.v7.app.NotificationCompat; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Easily create notifications with error messages. Use carefully, users probably won't like them. |  | ||||||
|  * (But they are useful during development/testing) |  | ||||||
|  * |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public class ErrorNotification extends AbstractNotification { |  | ||||||
|     public static final int ERROR_NOTIFICATION_ID = 4; |  | ||||||
|  |  | ||||||
|     private final NotificationCompat.Builder builder; |  | ||||||
|  |  | ||||||
|     public ErrorNotification(Context ctx) { |  | ||||||
|         super(ctx); |  | ||||||
|         builder = new NotificationCompat.Builder(ctx); |  | ||||||
|         builder.setContentTitle(ctx.getString(R.string.app_name)) |  | ||||||
|                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public ErrorNotification setWarning(@StringRes int resId, Object... args) { |  | ||||||
|         builder.setSmallIcon(R.drawable.ic_notification_warning) |  | ||||||
|                 .setContentText(ctx.getString(resId, args)); |  | ||||||
|         notification = builder.build(); |  | ||||||
|         return this; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public ErrorNotification setError(@StringRes int resId, Object... args) { |  | ||||||
|         builder.setSmallIcon(R.drawable.ic_notification_error) |  | ||||||
|                 .setContentText(ctx.getString(resId, args)); |  | ||||||
|         notification = builder.build(); |  | ||||||
|         return this; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected int getNotificationId() { |  | ||||||
|         return ERROR_NOTIFICATION_ID; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,56 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.notification | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.support.annotation.StringRes | ||||||
|  | import android.support.v4.app.NotificationCompat | ||||||
|  |  | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Easily create notifications with error messages. Use carefully, users probably won't like them. | ||||||
|  |  * (But they are useful during development/testing) | ||||||
|  |  | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class ErrorNotification(ctx: Context) : AbstractNotification(ctx) { | ||||||
|  |  | ||||||
|  |     private val builder = NotificationCompat.Builder(ctx, "abit.error") | ||||||
|  |         .setContentTitle(ctx.getString(R.string.app_name)) | ||||||
|  |         .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||||
|  |  | ||||||
|  |     fun setWarning(@StringRes resId: Int, vararg args: Any): ErrorNotification { | ||||||
|  |         builder.setSmallIcon(R.drawable.ic_notification_warning) | ||||||
|  |             .setContentText(ctx.getString(resId, *args)) | ||||||
|  |         notification = builder.build() | ||||||
|  |         return this | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun setError(@StringRes resId: Int, vararg args: Any): ErrorNotification { | ||||||
|  |         builder.setSmallIcon(R.drawable.ic_notification_error) | ||||||
|  |             .setContentText(ctx.getString(resId, *args)) | ||||||
|  |         notification = builder.build() | ||||||
|  |         return this | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override val notificationId = ERROR_NOTIFICATION_ID | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val ERROR_NOTIFICATION_ID = 4 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,143 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.notification; |  | ||||||
|  |  | ||||||
| import android.annotation.SuppressLint; |  | ||||||
| import android.app.PendingIntent; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.support.v7.app.NotificationCompat; |  | ||||||
|  |  | ||||||
| import java.util.Timer; |  | ||||||
| import java.util.TimerTask; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.MainActivity; |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
| import ch.dissem.apps.abit.service.BitmessageIntentService; |  | ||||||
| import ch.dissem.apps.abit.service.BitmessageService; |  | ||||||
| import ch.dissem.bitmessage.utils.Property; |  | ||||||
|  |  | ||||||
| import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; |  | ||||||
| import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Shows the network status (as long as the client is connected as a full node) |  | ||||||
|  */ |  | ||||||
| public class NetworkNotification extends AbstractNotification { |  | ||||||
|     public static final int NETWORK_NOTIFICATION_ID = 2; |  | ||||||
|  |  | ||||||
|     private final NotificationCompat.Builder builder; |  | ||||||
|     private Timer timer; |  | ||||||
|  |  | ||||||
|     public NetworkNotification(Context ctx) { |  | ||||||
|         super(ctx); |  | ||||||
|         Intent showAppIntent = new Intent(ctx, MainActivity.class); |  | ||||||
|         PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0); |  | ||||||
|         builder = new NotificationCompat.Builder(ctx); |  | ||||||
|         builder.setSmallIcon(R.drawable.ic_notification_full_node) |  | ||||||
|             .setContentTitle(ctx.getString(R.string.bitmessage_full_node)) |  | ||||||
|             .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) |  | ||||||
|             .setShowWhen(false) |  | ||||||
|             .setContentIntent(pendingIntent); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @SuppressLint("StringFormatMatches") |  | ||||||
|     @SuppressWarnings("BooleanMethodIsAlwaysInverted") |  | ||||||
|     private boolean update() { |  | ||||||
|         boolean running = BitmessageService.isRunning(); |  | ||||||
|         builder.setOngoing(running); |  | ||||||
|         Property connections = BitmessageService.getStatus().getProperty("network", "connections"); |  | ||||||
|         if (!running) { |  | ||||||
|             builder.setContentText(ctx.getString(R.string.connection_info_disconnected)); |  | ||||||
|             updateNodeSwitch(); |  | ||||||
|         } else if (connections.getProperties().length == 0) { |  | ||||||
|             builder.setContentText(ctx.getString(R.string.connection_info_pending)); |  | ||||||
|         } else { |  | ||||||
|             StringBuilder info = new StringBuilder(); |  | ||||||
|             for (Property stream : connections.getProperties()) { |  | ||||||
|                 int streamNumber = Integer.parseInt(stream.getName().substring("stream ".length())); |  | ||||||
|                 Integer nodeCount = (Integer) stream.getProperty("nodes").getValue(); |  | ||||||
|                 if (nodeCount == 1) { |  | ||||||
|                     info.append(ctx.getString(R.string.connection_info_1, |  | ||||||
|                         streamNumber)); |  | ||||||
|                 } else { |  | ||||||
|                     info.append(ctx.getString(R.string.connection_info_n, |  | ||||||
|                         streamNumber, nodeCount)); |  | ||||||
|                 } |  | ||||||
|                 info.append('\n'); |  | ||||||
|             } |  | ||||||
|             builder.setContentText(info); |  | ||||||
|         } |  | ||||||
|         builder.mActions.clear(); |  | ||||||
|         Intent intent = new Intent(ctx, BitmessageIntentService.class); |  | ||||||
|         if (running) { |  | ||||||
|             intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true); |  | ||||||
|             builder.addAction(R.drawable.ic_notification_node_stop, |  | ||||||
|                 ctx.getString(R.string.full_node_stop), |  | ||||||
|                 PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)); |  | ||||||
|         } else { |  | ||||||
|             intent.putExtra(BitmessageIntentService.EXTRA_STARTUP_NODE, true); |  | ||||||
|             builder.addAction(R.drawable.ic_notification_node_start, |  | ||||||
|                 ctx.getString(R.string.full_node_restart), |  | ||||||
|                 PendingIntent.getService(ctx, 1, intent, FLAG_UPDATE_CURRENT)); |  | ||||||
|         } |  | ||||||
|         notification = builder.build(); |  | ||||||
|         return running; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void show() { |  | ||||||
|         super.show(); |  | ||||||
|  |  | ||||||
|         timer = new Timer(); |  | ||||||
|         timer.schedule(new TimerTask() { |  | ||||||
|             @Override |  | ||||||
|             public void run() { |  | ||||||
|                 if (!update()) { |  | ||||||
|                     cancel(); |  | ||||||
|                     ctx.stopService(new Intent(ctx, BitmessageService.class)); |  | ||||||
|                 } |  | ||||||
|                 NetworkNotification.super.show(); |  | ||||||
|             } |  | ||||||
|         }, 10_000, 10_000); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void showShutdown() { |  | ||||||
|         if (timer != null) { |  | ||||||
|             timer.cancel(); |  | ||||||
|         } |  | ||||||
|         update(); |  | ||||||
|         super.show(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected int getNotificationId() { |  | ||||||
|         return NETWORK_NOTIFICATION_ID; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void connecting() { |  | ||||||
|         builder.setOngoing(true); |  | ||||||
|         builder.setContentText(ctx.getString(R.string.connection_info_pending)); |  | ||||||
|         Intent intent = new Intent(ctx, BitmessageIntentService.class); |  | ||||||
|         intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true); |  | ||||||
|         builder.mActions.clear(); |  | ||||||
|         builder.addAction(R.drawable.ic_notification_node_stop, |  | ||||||
|             ctx.getString(R.string.full_node_stop), |  | ||||||
|             PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)); |  | ||||||
|         notification = builder.build(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,128 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.notification | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.app.PendingIntent | ||||||
|  | import android.app.PendingIntent.FLAG_UPDATE_CURRENT | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.support.v4.app.NotificationCompat | ||||||
|  | import ch.dissem.apps.abit.MainActivity | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.apps.abit.service.BitmessageIntentService | ||||||
|  | import ch.dissem.apps.abit.service.BitmessageService | ||||||
|  | import java.util.* | ||||||
|  | import kotlin.concurrent.fixedRateTimer | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Shows the network status (as long as the client is connected as a full node) | ||||||
|  |  */ | ||||||
|  | class NetworkNotification(ctx: Context) : AbstractNotification(ctx) { | ||||||
|  |  | ||||||
|  |     private val builder = NotificationCompat.Builder(ctx, "abit.network") | ||||||
|  |     private var timer: Timer? = null | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         val showAppIntent = Intent(ctx, MainActivity::class.java) | ||||||
|  |         val pendingIntent = PendingIntent.getActivity(ctx, 1, showAppIntent, 0) | ||||||
|  |         builder | ||||||
|  |                 .setSmallIcon(R.drawable.ic_notification_full_node) | ||||||
|  |                 .setContentTitle(ctx.getString(R.string.bitmessage_full_node)) | ||||||
|  |                 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||||
|  |                 .setShowWhen(false) | ||||||
|  |                 .setContentIntent(pendingIntent) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SuppressLint("StringFormatMatches") | ||||||
|  |     private fun update(): Boolean { | ||||||
|  |         val running = BitmessageService.isRunning | ||||||
|  |         builder.setOngoing(running) | ||||||
|  |         val connections = BitmessageService.status.getProperty("network", "connections") | ||||||
|  |         if (!running) { | ||||||
|  |             builder.setContentText(ctx.getString(R.string.connection_info_disconnected)) | ||||||
|  |         } else if (connections == null || connections.properties.isEmpty()) { | ||||||
|  |             builder.setContentText(ctx.getString(R.string.connection_info_pending)) | ||||||
|  |         } else { | ||||||
|  |             val info = StringBuilder() | ||||||
|  |             for (stream in connections.properties) { | ||||||
|  |                 val streamNumber = Integer.parseInt(stream.name.substring("stream ".length)) | ||||||
|  |                 val nodeCount = stream.getProperty("nodes")!!.value as Int? | ||||||
|  |                 if (nodeCount == 1) { | ||||||
|  |                     info.append(ctx.getString(R.string.connection_info_1, | ||||||
|  |                             streamNumber)) | ||||||
|  |                 } else { | ||||||
|  |                     info.append(ctx.getString(R.string.connection_info_n, | ||||||
|  |                             streamNumber, nodeCount)) | ||||||
|  |                 } | ||||||
|  |                 info.append('\n') | ||||||
|  |             } | ||||||
|  |             builder.setContentText(info) | ||||||
|  |         } | ||||||
|  |         builder.mActions.clear() | ||||||
|  |         val intent = Intent(ctx, BitmessageIntentService::class.java) | ||||||
|  |         if (running) { | ||||||
|  |             intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true) | ||||||
|  |             builder.addAction(R.drawable.ic_notification_node_stop, | ||||||
|  |                     ctx.getString(R.string.full_node_stop), | ||||||
|  |                     PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)) | ||||||
|  |         } else { | ||||||
|  |             intent.putExtra(BitmessageIntentService.EXTRA_STARTUP_NODE, true) | ||||||
|  |             builder.addAction(R.drawable.ic_notification_node_start, | ||||||
|  |                     ctx.getString(R.string.full_node_restart), | ||||||
|  |                     PendingIntent.getService(ctx, 1, intent, FLAG_UPDATE_CURRENT)) | ||||||
|  |         } | ||||||
|  |         notification = builder.build() | ||||||
|  |         return running | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun show() { | ||||||
|  |         super.show() | ||||||
|  |  | ||||||
|  |         timer = fixedRateTimer(initialDelay = 10000, period = 10000) { | ||||||
|  |             if (!update()) { | ||||||
|  |                 cancel() | ||||||
|  |                 ctx.stopService(Intent(ctx, BitmessageService::class.java)) | ||||||
|  |             } | ||||||
|  |             super@NetworkNotification.show() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun showShutdown() { | ||||||
|  |         timer?.cancel() | ||||||
|  |         update() | ||||||
|  |         super.show() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override val notificationId = NETWORK_NOTIFICATION_ID | ||||||
|  |  | ||||||
|  |     fun connecting() { | ||||||
|  |         builder.setOngoing(true) | ||||||
|  |         builder.setContentText(ctx.getString(R.string.connection_info_pending)) | ||||||
|  |         val intent = Intent(ctx, BitmessageIntentService::class.java) | ||||||
|  |         intent.putExtra(BitmessageIntentService.EXTRA_SHUTDOWN_NODE, true) | ||||||
|  |         builder.mActions.clear() | ||||||
|  |         builder.addAction(R.drawable.ic_notification_node_stop, | ||||||
|  |                 ctx.getString(R.string.full_node_stop), | ||||||
|  |                 PendingIntent.getService(ctx, 0, intent, FLAG_UPDATE_CURRENT)) | ||||||
|  |         notification = builder.build() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val NETWORK_NOTIFICATION_ID = 2 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,122 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.notification; |  | ||||||
|  |  | ||||||
| import android.app.PendingIntent; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.graphics.Typeface; |  | ||||||
| import android.support.v7.app.NotificationCompat; |  | ||||||
| import android.text.Spannable; |  | ||||||
| import android.text.SpannableString; |  | ||||||
| import android.text.Spanned; |  | ||||||
| import android.text.style.StyleSpan; |  | ||||||
|  |  | ||||||
| import java.util.Collection; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.Identicon; |  | ||||||
| import ch.dissem.apps.abit.MainActivity; |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
| import ch.dissem.apps.abit.service.BitmessageIntentService; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
|  |  | ||||||
| import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; |  | ||||||
| import static ch.dissem.apps.abit.MainActivity.EXTRA_REPLY_TO_MESSAGE; |  | ||||||
| import static ch.dissem.apps.abit.MainActivity.EXTRA_SHOW_MESSAGE; |  | ||||||
| import static ch.dissem.apps.abit.service.BitmessageIntentService.EXTRA_DELETE_MESSAGE; |  | ||||||
| import static ch.dissem.apps.abit.util.Drawables.toBitmap; |  | ||||||
|  |  | ||||||
| public class NewMessageNotification extends AbstractNotification { |  | ||||||
|     private static final int NEW_MESSAGE_NOTIFICATION_ID = 1; |  | ||||||
|     private static final StyleSpan SPAN_EMPHASIS = new StyleSpan(Typeface.BOLD); |  | ||||||
|  |  | ||||||
|     public NewMessageNotification(Context ctx) { |  | ||||||
|         super(ctx); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public NewMessageNotification singleNotification(Plaintext plaintext) { |  | ||||||
|         NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); |  | ||||||
|         Spannable bigText = new SpannableString(plaintext.getSubject() + "\n" + plaintext.getText |  | ||||||
|             ()); |  | ||||||
|         bigText.setSpan(SPAN_EMPHASIS, 0, plaintext.getSubject().length(), Spanned |  | ||||||
|             .SPAN_INCLUSIVE_EXCLUSIVE); |  | ||||||
|         builder.setSmallIcon(R.drawable.ic_notification_new_message) |  | ||||||
|             .setLargeIcon(toBitmap(new Identicon(plaintext.getFrom()), 192)) |  | ||||||
|             .setContentTitle(plaintext.getFrom().toString()) |  | ||||||
|             .setContentText(plaintext.getSubject()) |  | ||||||
|             .setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)) |  | ||||||
|             .setContentInfo("Info"); |  | ||||||
|  |  | ||||||
|         builder.setContentIntent( |  | ||||||
|             createActivityIntent(EXTRA_SHOW_MESSAGE, plaintext)); |  | ||||||
|         builder.addAction(R.drawable.ic_action_reply, ctx.getString(R.string.reply), |  | ||||||
|             createActivityIntent(EXTRA_REPLY_TO_MESSAGE, plaintext)); |  | ||||||
|         builder.addAction(R.drawable.ic_action_delete, ctx.getString(R.string.delete), |  | ||||||
|             createServiceIntent(ctx, EXTRA_DELETE_MESSAGE, plaintext)); |  | ||||||
|         notification = builder.build(); |  | ||||||
|         return this; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private PendingIntent createActivityIntent(String action, Plaintext message) { |  | ||||||
|         Intent intent = new Intent(ctx, MainActivity.class); |  | ||||||
|         intent.putExtra(action, message); |  | ||||||
|         return PendingIntent.getActivity(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private PendingIntent createServiceIntent(Context ctx, String action, Plaintext message) { |  | ||||||
|         Intent intent = new Intent(ctx, BitmessageIntentService.class); |  | ||||||
|         intent.putExtra(action, message); |  | ||||||
|         return PendingIntent.getService(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * @param unacknowledged will be accessed from different threads, so make sure wherever it's |  | ||||||
|      *                       accessed it will be in a <code>synchronized(unacknowledged) |  | ||||||
|      *                       {}</code> block |  | ||||||
|      */ |  | ||||||
|     public NewMessageNotification multiNotification(Collection<Plaintext> unacknowledged, int |  | ||||||
|         numberOfUnacknowledgedMessages) { |  | ||||||
|         NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); |  | ||||||
|         builder.setSmallIcon(R.drawable.ic_notification_new_message) |  | ||||||
|             .setContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages)) |  | ||||||
|             .setContentText(ctx.getString(R.string.app_name)); |  | ||||||
|  |  | ||||||
|         NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); |  | ||||||
|         //noinspection SynchronizationOnLocalVariableOrMethodParameter |  | ||||||
|         synchronized (unacknowledged) { |  | ||||||
|             for (Plaintext msg : unacknowledged) { |  | ||||||
|                 Spannable sb = new SpannableString(msg.getFrom() + " " + msg.getSubject()); |  | ||||||
|                 sb.setSpan(SPAN_EMPHASIS, 0, String.valueOf(msg.getFrom()).length(), Spannable |  | ||||||
|                     .SPAN_INCLUSIVE_EXCLUSIVE); |  | ||||||
|                 inboxStyle.addLine(sb); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         builder.setStyle(inboxStyle); |  | ||||||
|  |  | ||||||
|         Intent intent = new Intent(ctx, MainActivity.class); |  | ||||||
|         intent.setAction(MainActivity.ACTION_SHOW_INBOX); |  | ||||||
|         PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0); |  | ||||||
|         builder.setContentIntent(pendingIntent); |  | ||||||
|         notification = builder.build(); |  | ||||||
|         return this; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected int getNotificationId() { |  | ||||||
|         return NEW_MESSAGE_NOTIFICATION_ID; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,118 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.notification | ||||||
|  |  | ||||||
|  | import android.app.PendingIntent | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.graphics.Typeface | ||||||
|  | import android.support.v4.app.NotificationCompat | ||||||
|  | import android.support.v4.app.NotificationCompat.BigTextStyle | ||||||
|  | import android.support.v4.app.NotificationCompat.InboxStyle | ||||||
|  | import android.text.Spannable | ||||||
|  | import android.text.SpannableString | ||||||
|  | import android.text.Spanned | ||||||
|  | import android.text.style.StyleSpan | ||||||
|  |  | ||||||
|  | import ch.dissem.apps.abit.Identicon | ||||||
|  | import ch.dissem.apps.abit.MainActivity | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.apps.abit.service.BitmessageIntentService | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  |  | ||||||
|  | import android.app.PendingIntent.FLAG_UPDATE_CURRENT | ||||||
|  | import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_REPLY_TO_MESSAGE | ||||||
|  | import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_SHOW_MESSAGE | ||||||
|  | import ch.dissem.apps.abit.service.BitmessageIntentService.Companion.EXTRA_DELETE_MESSAGE | ||||||
|  | import ch.dissem.apps.abit.util.Drawables.toBitmap | ||||||
|  |  | ||||||
|  | class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) { | ||||||
|  |  | ||||||
|  |     fun singleNotification(plaintext: Plaintext): NewMessageNotification { | ||||||
|  |         val builder = NotificationCompat.Builder(ctx, CHANNEL_ID) | ||||||
|  |         val bigText = SpannableString(plaintext.subject + "\n" + plaintext.text) | ||||||
|  |         plaintext.subject?.let { subject -> | ||||||
|  |             bigText.setSpan(SPAN_EMPHASIS, 0, subject.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) | ||||||
|  |         } | ||||||
|  |         builder.setSmallIcon(R.drawable.ic_notification_new_message) | ||||||
|  |                 .setLargeIcon(toBitmap(Identicon(plaintext.from), 192)) | ||||||
|  |                 .setContentTitle(plaintext.from.toString()) | ||||||
|  |                 .setContentText(plaintext.subject) | ||||||
|  |                 .setStyle(BigTextStyle().bigText(bigText)) | ||||||
|  |                 .setContentInfo("Info") | ||||||
|  |  | ||||||
|  |         builder.setContentIntent( | ||||||
|  |                 createActivityIntent(EXTRA_SHOW_MESSAGE, plaintext)) | ||||||
|  |         builder.addAction(R.drawable.ic_action_reply, ctx.getString(R.string.reply), | ||||||
|  |                 createActivityIntent(EXTRA_REPLY_TO_MESSAGE, plaintext)) | ||||||
|  |         builder.addAction(R.drawable.ic_action_delete, ctx.getString(R.string.delete), | ||||||
|  |                 createServiceIntent(ctx, EXTRA_DELETE_MESSAGE, plaintext)) | ||||||
|  |         notification = builder.build() | ||||||
|  |         return this | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun createActivityIntent(action: String, message: Plaintext): PendingIntent { | ||||||
|  |         val intent = Intent(ctx, MainActivity::class.java) | ||||||
|  |         intent.putExtra(action, message) | ||||||
|  |         return PendingIntent.getActivity(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun createServiceIntent(ctx: Context, action: String, message: Plaintext): PendingIntent { | ||||||
|  |         val intent = Intent(ctx, BitmessageIntentService::class.java) | ||||||
|  |         intent.putExtra(action, message) | ||||||
|  |         return PendingIntent.getService(ctx, action.hashCode(), intent, FLAG_UPDATE_CURRENT) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @param unacknowledged will be accessed from different threads, so make sure wherever it's | ||||||
|  |      * *                       accessed it will be in a `synchronized(unacknowledged) | ||||||
|  |      * *                       {}` block | ||||||
|  |      */ | ||||||
|  |     fun multiNotification(unacknowledged: Collection<Plaintext>, numberOfUnacknowledgedMessages: Int): NewMessageNotification { | ||||||
|  |         val builder = NotificationCompat.Builder(ctx, CHANNEL_ID) | ||||||
|  |         builder.setSmallIcon(R.drawable.ic_notification_new_message) | ||||||
|  |                 .setContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages)) | ||||||
|  |                 .setContentText(ctx.getString(R.string.app_name)) | ||||||
|  |  | ||||||
|  |         val inboxStyle = InboxStyle() | ||||||
|  |  | ||||||
|  |         synchronized(unacknowledged) { | ||||||
|  |             for (msg in unacknowledged) { | ||||||
|  |                 val sb = SpannableString(msg.from.toString() + " " + msg.subject) | ||||||
|  |                 sb.setSpan(SPAN_EMPHASIS, 0, msg.from.toString().length, Spannable | ||||||
|  |                         .SPAN_INCLUSIVE_EXCLUSIVE) | ||||||
|  |                 inboxStyle.addLine(sb) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         builder.setStyle(inboxStyle) | ||||||
|  |  | ||||||
|  |         val intent = Intent(ctx, MainActivity::class.java) | ||||||
|  |         intent.action = MainActivity.ACTION_SHOW_INBOX | ||||||
|  |         val pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0) | ||||||
|  |         builder.setContentIntent(pendingIntent) | ||||||
|  |         notification = builder.build() | ||||||
|  |         return this | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override val notificationId = NEW_MESSAGE_NOTIFICATION_ID | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val NEW_MESSAGE_NOTIFICATION_ID = 1 | ||||||
|  |         private val SPAN_EMPHASIS = StyleSpan(Typeface.BOLD) | ||||||
|  |         private val CHANNEL_ID = "abit.message" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,63 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.notification; |  | ||||||
|  |  | ||||||
| import android.app.PendingIntent; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.support.v7.app.NotificationCompat; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.MainActivity; |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Ongoing notification while proof of work is in progress. |  | ||||||
|  */ |  | ||||||
| public class ProofOfWorkNotification extends AbstractNotification { |  | ||||||
|     public static final int ONGOING_NOTIFICATION_ID = 3; |  | ||||||
|  |  | ||||||
|     public ProofOfWorkNotification(Context ctx) { |  | ||||||
|         super(ctx); |  | ||||||
|         update(0); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected int getNotificationId() { |  | ||||||
|         return ONGOING_NOTIFICATION_ID; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public ProofOfWorkNotification update(int numberOfItems) { |  | ||||||
|         NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx); |  | ||||||
|  |  | ||||||
|         Intent showMessageIntent = new Intent(ctx, MainActivity.class); |  | ||||||
|         PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, |  | ||||||
|                 PendingIntent.FLAG_UPDATE_CURRENT); |  | ||||||
|  |  | ||||||
|         builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) |  | ||||||
|                 .setUsesChronometer(true) |  | ||||||
|                 .setOngoing(true) |  | ||||||
|                 .setSmallIcon(R.drawable.ic_notification_proof_of_work) |  | ||||||
|                 .setContentTitle(ctx.getString(R.string.proof_of_work_title)) |  | ||||||
|                 .setContentText(numberOfItems == 0 |  | ||||||
|                         ? ctx.getString(R.string.proof_of_work_text_0) |  | ||||||
|                         : ctx.getString(R.string.proof_of_work_text_n, numberOfItems)) |  | ||||||
|                 .setContentIntent(pendingIntent); |  | ||||||
|  |  | ||||||
|         notification = builder.build(); |  | ||||||
|         return this; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,104 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.notification | ||||||
|  |  | ||||||
|  | import android.app.PendingIntent | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.support.v4.app.NotificationCompat | ||||||
|  |  | ||||||
|  | import ch.dissem.apps.abit.MainActivity | ||||||
|  | import ch.dissem.apps.abit.R | ||||||
|  | import ch.dissem.apps.abit.service.ProofOfWorkService | ||||||
|  | import ch.dissem.apps.abit.util.PowStats | ||||||
|  | import java.util.* | ||||||
|  | import kotlin.concurrent.fixedRateTimer | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Ongoing notification while proof of work is in progress. | ||||||
|  |  */ | ||||||
|  | class ProofOfWorkNotification(ctx: Context) : AbstractNotification(ctx) { | ||||||
|  |  | ||||||
|  |     private val builder = NotificationCompat.Builder(ctx, "abit.pow") | ||||||
|  |         .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) | ||||||
|  |         .setUsesChronometer(true) | ||||||
|  |         .setOngoing(true) | ||||||
|  |         .setSmallIcon(R.drawable.ic_notification_proof_of_work) | ||||||
|  |         .setContentTitle(ctx.getString(R.string.proof_of_work_title)) | ||||||
|  |     private var startTime = 0L | ||||||
|  |     private var progress = 0 | ||||||
|  |     private var progressMax = 0 | ||||||
|  |  | ||||||
|  |     private var timer: Timer? = null | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         update(0) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override val notificationId = ONGOING_NOTIFICATION_ID | ||||||
|  |  | ||||||
|  |     fun update(numberOfItems: Int): ProofOfWorkNotification { | ||||||
|  |  | ||||||
|  |         val showMessageIntent = Intent(ctx, MainActivity::class.java) | ||||||
|  |         val pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, | ||||||
|  |             PendingIntent.FLAG_UPDATE_CURRENT) | ||||||
|  |  | ||||||
|  |         builder.setContentText(if (numberOfItems == 0) | ||||||
|  |             ctx.getString(R.string.proof_of_work_text_0) | ||||||
|  |         else | ||||||
|  |             ctx.getString(R.string.proof_of_work_text_n, numberOfItems)) | ||||||
|  |             .setContentIntent(pendingIntent) | ||||||
|  |  | ||||||
|  |         notification = builder.build() | ||||||
|  |         return this | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         const val ONGOING_NOTIFICATION_ID = 3 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun start(item: ProofOfWorkService.PowItem) { | ||||||
|  |         val expectedPowTimeInMilliseconds = PowStats.getExpectedPowTimeInMilliseconds(ctx, item.targetValue) | ||||||
|  |         val delta = (expectedPowTimeInMilliseconds / 3).toInt() | ||||||
|  |         startTime = System.currentTimeMillis() | ||||||
|  |         progress = 0 | ||||||
|  |         progressMax = delta | ||||||
|  |         builder.setProgress(progressMax, progress, false) | ||||||
|  |         notification = builder.build() | ||||||
|  |         show() | ||||||
|  |  | ||||||
|  |         timer = fixedRateTimer(initialDelay = 2000, period = 2000){ | ||||||
|  |             val elapsedTime = System.currentTimeMillis() - startTime | ||||||
|  |             progress = elapsedTime.toInt() | ||||||
|  |             progressMax = progress + delta | ||||||
|  |             builder.setProgress(progressMax, progress, false) | ||||||
|  |             notification = builder.build() | ||||||
|  |             show() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun finished() { | ||||||
|  |         timer?.cancel() | ||||||
|  |         progress = 0 | ||||||
|  |         progressMax = 0 | ||||||
|  |         if (showing) { | ||||||
|  |             builder.setProgress(0, 0, false) | ||||||
|  |             notification = builder.build() | ||||||
|  |             show() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,97 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.pow; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.support.annotation.NonNull; |  | ||||||
|  |  | ||||||
| import org.slf4j.Logger; |  | ||||||
| import org.slf4j.LoggerFactory; |  | ||||||
|  |  | ||||||
| import java.util.concurrent.ExecutorService; |  | ||||||
| import java.util.concurrent.Executors; |  | ||||||
| import java.util.concurrent.ThreadFactory; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.service.Singleton; |  | ||||||
| import ch.dissem.apps.abit.synchronization.SyncAdapter; |  | ||||||
| import ch.dissem.apps.abit.util.Preferences; |  | ||||||
| import ch.dissem.bitmessage.InternalContext; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.extensions.CryptoCustomMessage; |  | ||||||
| import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; |  | ||||||
| import ch.dissem.bitmessage.ports.ProofOfWorkEngine; |  | ||||||
|  |  | ||||||
| import static ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE; |  | ||||||
| import static ch.dissem.bitmessage.utils.Singleton.cryptography; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public class ServerPowEngine implements ProofOfWorkEngine, InternalContext |  | ||||||
|     .ContextHolder { |  | ||||||
|     private static final Logger LOG = LoggerFactory.getLogger(ServerPowEngine.class); |  | ||||||
|  |  | ||||||
|     private final Context ctx; |  | ||||||
|     private InternalContext context; |  | ||||||
|  |  | ||||||
|     private final ExecutorService pool; |  | ||||||
|  |  | ||||||
|     public ServerPowEngine(Context ctx) { |  | ||||||
|         this.ctx = ctx; |  | ||||||
|         pool = Executors.newCachedThreadPool(new ThreadFactory() { |  | ||||||
|             @Override |  | ||||||
|             public Thread newThread(@NonNull Runnable r) { |  | ||||||
|                 Thread thread = Executors.defaultThreadFactory().newThread(r); |  | ||||||
|                 thread.setPriority(Thread.MIN_PRIORITY); |  | ||||||
|                 return thread; |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void calculateNonce(final byte[] initialHash, final byte[] target, Callback callback) { |  | ||||||
|         pool.execute(new Runnable() { |  | ||||||
|             @Override |  | ||||||
|             public void run() { |  | ||||||
|                 BitmessageAddress identity = Singleton.getIdentity(ctx); |  | ||||||
|                 if (identity == null) throw new RuntimeException("No Identity for calculating POW"); |  | ||||||
|  |  | ||||||
|                 ProofOfWorkRequest request = new ProofOfWorkRequest(identity, initialHash, |  | ||||||
|                     CALCULATE, target); |  | ||||||
|                 SyncAdapter.startPowSync(ctx); |  | ||||||
|                 try { |  | ||||||
|                     CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<> |  | ||||||
|                         (request); |  | ||||||
|                     cryptoMsg.signAndEncrypt( |  | ||||||
|                         identity, |  | ||||||
|                         cryptography().createPublicKey(identity.getPublicDecryptionKey()) |  | ||||||
|                     ); |  | ||||||
|                     context.getNetworkHandler().send( |  | ||||||
|                         Preferences.getTrustedNode(ctx), Preferences.getTrustedNodePort(ctx), |  | ||||||
|                         cryptoMsg); |  | ||||||
|                 } catch (Exception e) { |  | ||||||
|                     LOG.error(e.getMessage(), e); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void setContext(InternalContext context) { |  | ||||||
|         this.context = context; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										83
									
								
								app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								app/src/main/java/ch/dissem/apps/abit/pow/ServerPowEngine.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.pow | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import ch.dissem.apps.abit.service.Singleton | ||||||
|  | import ch.dissem.apps.abit.synchronization.SyncAdapter | ||||||
|  | import ch.dissem.apps.abit.util.Preferences | ||||||
|  | import ch.dissem.bitmessage.InternalContext | ||||||
|  | import ch.dissem.bitmessage.extensions.CryptoCustomMessage | ||||||
|  | import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest | ||||||
|  | import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE | ||||||
|  | import ch.dissem.bitmessage.ports.ProofOfWorkEngine | ||||||
|  | import ch.dissem.bitmessage.utils.Singleton.cryptography | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import java.util.concurrent.ExecutorService | ||||||
|  | import java.util.concurrent.Executors | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class ServerPowEngine(private val ctx: Context) : ProofOfWorkEngine, InternalContext.ContextHolder { | ||||||
|  |     private lateinit var context: InternalContext | ||||||
|  |  | ||||||
|  |     private val pool: ExecutorService | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         pool = Executors.newCachedThreadPool { r -> | ||||||
|  |             val thread = Executors.defaultThreadFactory().newThread(r) | ||||||
|  |             thread.priority = Thread.MIN_PRIORITY | ||||||
|  |             thread | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) = | ||||||
|  |         pool.execute { | ||||||
|  |             val identity = Singleton.getIdentity(ctx) ?: throw RuntimeException("No Identity for calculating POW") | ||||||
|  |  | ||||||
|  |             val request = ProofOfWorkRequest(identity, initialHash, | ||||||
|  |                     CALCULATE, target) | ||||||
|  |             SyncAdapter.startPowSync(ctx) | ||||||
|  |             try { | ||||||
|  |                 val cryptoMsg = CryptoCustomMessage(request) | ||||||
|  |                 cryptoMsg.signAndEncrypt( | ||||||
|  |                         identity, | ||||||
|  |                         cryptography().createPublicKey(identity.publicDecryptionKey) | ||||||
|  |                 ) | ||||||
|  |                 val node = Preferences.getTrustedNode(ctx) | ||||||
|  |                 if (node == null) { | ||||||
|  |                     LOG.error("trusted node is not defined") | ||||||
|  |                 } else { | ||||||
|  |                     context.networkHandler.send( | ||||||
|  |                             node, | ||||||
|  |                             Preferences.getTrustedNodePort(ctx), | ||||||
|  |                             cryptoMsg) | ||||||
|  |                 } | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 LOG.error(e.message, e) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     override fun setContext(context: InternalContext) { | ||||||
|  |         this.context = context | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val LOG = LoggerFactory.getLogger(ServerPowEngine::class.java) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,261 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.repository; |  | ||||||
|  |  | ||||||
| import android.content.ContentValues; |  | ||||||
| import android.database.Cursor; |  | ||||||
| import android.database.sqlite.SQLiteDatabase; |  | ||||||
|  |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.Pubkey; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.V3Pubkey; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.V4Pubkey; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.PrivateKey; |  | ||||||
| import ch.dissem.bitmessage.factory.Factory; |  | ||||||
| import ch.dissem.bitmessage.ports.AddressRepository; |  | ||||||
| import ch.dissem.bitmessage.utils.Encode; |  | ||||||
|  |  | ||||||
| import org.slf4j.Logger; |  | ||||||
| import org.slf4j.LoggerFactory; |  | ||||||
|  |  | ||||||
| import java.io.ByteArrayInputStream; |  | ||||||
| import java.io.ByteArrayOutputStream; |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.util.Arrays; |  | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * {@link AddressRepository} implementation using the Android SQL API. |  | ||||||
|  */ |  | ||||||
| public class AndroidAddressRepository implements AddressRepository { |  | ||||||
|     private static final Logger LOG = LoggerFactory.getLogger(AndroidAddressRepository.class); |  | ||||||
|  |  | ||||||
|     private static final String TABLE_NAME = "Address"; |  | ||||||
|     private static final String COLUMN_ADDRESS = "address"; |  | ||||||
|     private static final String COLUMN_VERSION = "version"; |  | ||||||
|     private static final String COLUMN_ALIAS = "alias"; |  | ||||||
|     private static final String COLUMN_PUBLIC_KEY = "public_key"; |  | ||||||
|     private static final String COLUMN_PRIVATE_KEY = "private_key"; |  | ||||||
|     private static final String COLUMN_SUBSCRIBED = "subscribed"; |  | ||||||
|     private static final String COLUMN_CHAN = "chan"; |  | ||||||
|  |  | ||||||
|     private final SqlHelper sql; |  | ||||||
|  |  | ||||||
|     public AndroidAddressRepository(SqlHelper sql) { |  | ||||||
|         this.sql = sql; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public BitmessageAddress findContact(byte[] ripeOrTag) { |  | ||||||
|         for (BitmessageAddress address : find("public_key is null")) { |  | ||||||
|             if (address.getVersion() > 3) { |  | ||||||
|                 if (Arrays.equals(ripeOrTag, address.getTag())) return address; |  | ||||||
|             } else { |  | ||||||
|                 if (Arrays.equals(ripeOrTag, address.getRipe())) return address; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public BitmessageAddress findIdentity(byte[] ripeOrTag) { |  | ||||||
|         for (BitmessageAddress address : find("private_key is not null")) { |  | ||||||
|             if (address.getVersion() > 3) { |  | ||||||
|                 if (Arrays.equals(ripeOrTag, address.getTag())) return address; |  | ||||||
|             } else { |  | ||||||
|                 if (Arrays.equals(ripeOrTag, address.getRipe())) return address; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<BitmessageAddress> getIdentities() { |  | ||||||
|         return find("private_key IS NOT NULL"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<BitmessageAddress> getChans() { |  | ||||||
|         return find("chan = '1'"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<BitmessageAddress> getSubscriptions() { |  | ||||||
|         return find("subscribed = '1'"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<BitmessageAddress> getSubscriptions(long broadcastVersion) { |  | ||||||
|         if (broadcastVersion > 4) { |  | ||||||
|             return find("subscribed = '1' AND version > 3"); |  | ||||||
|         } else { |  | ||||||
|             return find("subscribed = '1' AND version <= 3"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<BitmessageAddress> getContacts() { |  | ||||||
|         return find("private_key IS NULL OR chan = '1'"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private List<BitmessageAddress> find(String where) { |  | ||||||
|         List<BitmessageAddress> result = new LinkedList<>(); |  | ||||||
|  |  | ||||||
|         // Define a projection that specifies which columns from the database |  | ||||||
|         // you will actually use after this query. |  | ||||||
|         String[] projection = { |  | ||||||
|             COLUMN_ADDRESS, |  | ||||||
|             COLUMN_ALIAS, |  | ||||||
|             COLUMN_PUBLIC_KEY, |  | ||||||
|             COLUMN_PRIVATE_KEY, |  | ||||||
|             COLUMN_SUBSCRIBED, |  | ||||||
|             COLUMN_CHAN |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         try (Cursor c = db.query( |  | ||||||
|             TABLE_NAME, projection, |  | ||||||
|             where, |  | ||||||
|             null, null, null, null |  | ||||||
|         )) { |  | ||||||
|             while (c.moveToNext()) { |  | ||||||
|                 BitmessageAddress address; |  | ||||||
|  |  | ||||||
|                 byte[] privateKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY)); |  | ||||||
|                 if (privateKeyBytes != null) { |  | ||||||
|                     PrivateKey privateKey = PrivateKey.read(new ByteArrayInputStream |  | ||||||
|                         (privateKeyBytes)); |  | ||||||
|                     address = new BitmessageAddress(privateKey); |  | ||||||
|                 } else { |  | ||||||
|                     address = new BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))); |  | ||||||
|                     byte[] publicKeyBytes = c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY)); |  | ||||||
|                     if (publicKeyBytes != null) { |  | ||||||
|                         Pubkey pubkey = Factory.readPubkey(address.getVersion(), address |  | ||||||
|                                 .getStream(), |  | ||||||
|                             new ByteArrayInputStream(publicKeyBytes), publicKeyBytes.length, |  | ||||||
|                             false); |  | ||||||
|                         if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) { |  | ||||||
|                             pubkey = new V4Pubkey((V3Pubkey) pubkey); |  | ||||||
|                         } |  | ||||||
|                         address.setPubkey(pubkey); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 address.setAlias(c.getString(c.getColumnIndex(COLUMN_ALIAS))); |  | ||||||
|                 address.setChan(c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1); |  | ||||||
|                 address.setSubscribed(c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1); |  | ||||||
|  |  | ||||||
|                 result.add(address); |  | ||||||
|             } |  | ||||||
|         } catch (IOException e) { |  | ||||||
|             LOG.error(e.getMessage(), e); |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void save(BitmessageAddress address) { |  | ||||||
|         if (exists(address)) { |  | ||||||
|             update(address); |  | ||||||
|         } else { |  | ||||||
|             insert(address); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private boolean exists(BitmessageAddress address) { |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         try (Cursor cursor = db.rawQuery( |  | ||||||
|             "SELECT COUNT(*) FROM Address WHERE address=?", |  | ||||||
|             new String[]{address.getAddress()} |  | ||||||
|         )) { |  | ||||||
|             cursor.moveToFirst(); |  | ||||||
|             return cursor.getInt(0) > 0; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void update(BitmessageAddress address) { |  | ||||||
|         try { |  | ||||||
|             SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|             // Create a new map of values, where column names are the keys |  | ||||||
|             ContentValues values = new ContentValues(); |  | ||||||
|             if (address.getAlias() != null) { |  | ||||||
|                 values.put(COLUMN_ALIAS, address.getAlias()); |  | ||||||
|             } |  | ||||||
|             if (address.getPubkey() != null) { |  | ||||||
|                 ByteArrayOutputStream out = new ByteArrayOutputStream(); |  | ||||||
|                 address.getPubkey().writeUnencrypted(out); |  | ||||||
|                 values.put(COLUMN_PUBLIC_KEY, out.toByteArray()); |  | ||||||
|             } |  | ||||||
|             if (address.getPrivateKey() != null) { |  | ||||||
|                 values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); |  | ||||||
|             } |  | ||||||
|             if (address.isChan()) { |  | ||||||
|                 values.put(COLUMN_CHAN, true); |  | ||||||
|             } |  | ||||||
|             values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); |  | ||||||
|  |  | ||||||
|             int update = db.update(TABLE_NAME, values, "address=?", |  | ||||||
|                 new String[]{address.getAddress()}); |  | ||||||
|             if (update < 0) { |  | ||||||
|                 LOG.error("Could not update address " + address); |  | ||||||
|             } |  | ||||||
|         } catch (IOException e) { |  | ||||||
|             LOG.error(e.getMessage(), e); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void insert(BitmessageAddress address) { |  | ||||||
|         try { |  | ||||||
|             SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|             // Create a new map of values, where column names are the keys |  | ||||||
|             ContentValues values = new ContentValues(); |  | ||||||
|             values.put(COLUMN_ADDRESS, address.getAddress()); |  | ||||||
|             values.put(COLUMN_VERSION, address.getVersion()); |  | ||||||
|             values.put(COLUMN_ALIAS, address.getAlias()); |  | ||||||
|             if (address.getPubkey() != null) { |  | ||||||
|                 ByteArrayOutputStream out = new ByteArrayOutputStream(); |  | ||||||
|                 address.getPubkey().writeUnencrypted(out); |  | ||||||
|                 values.put(COLUMN_PUBLIC_KEY, out.toByteArray()); |  | ||||||
|             } else { |  | ||||||
|                 values.put(COLUMN_PUBLIC_KEY, (byte[]) null); |  | ||||||
|             } |  | ||||||
|             values.put(COLUMN_PRIVATE_KEY, Encode.bytes(address.getPrivateKey())); |  | ||||||
|             values.put(COLUMN_CHAN, address.isChan()); |  | ||||||
|             values.put(COLUMN_SUBSCRIBED, address.isSubscribed()); |  | ||||||
|  |  | ||||||
|             long insert = db.insert(TABLE_NAME, null, values); |  | ||||||
|             if (insert < 0) { |  | ||||||
|                 LOG.error("Could not insert address " + address); |  | ||||||
|             } |  | ||||||
|         } catch (IOException e) { |  | ||||||
|             LOG.error(e.getMessage(), e); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void remove(BitmessageAddress address) { |  | ||||||
|         SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|         db.delete(TABLE_NAME, "address = ?", new String[]{address.getAddress()}); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public BitmessageAddress getAddress(String address) { |  | ||||||
|         List<BitmessageAddress> result = find("address = '" + address + "'"); |  | ||||||
|         if (result.size() > 0) return result.get(0); |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,227 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.repository | ||||||
|  |  | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.database.Cursor | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  | import ch.dissem.bitmessage.entity.payload.V3Pubkey | ||||||
|  | import ch.dissem.bitmessage.entity.payload.V4Pubkey | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.PrivateKey | ||||||
|  | import ch.dissem.bitmessage.factory.Factory | ||||||
|  | import ch.dissem.bitmessage.ports.AddressRepository | ||||||
|  | import ch.dissem.bitmessage.utils.Encode | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import java.io.ByteArrayInputStream | ||||||
|  | import java.io.ByteArrayOutputStream | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * [AddressRepository] implementation using the Android SQL API. | ||||||
|  |  */ | ||||||
|  | class AndroidAddressRepository(private val sql: SqlHelper) : AddressRepository { | ||||||
|  |  | ||||||
|  |     override fun findContact(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("public_key is null", ripeOrTag) | ||||||
|  |  | ||||||
|  |     override fun findIdentity(ripeOrTag: ByteArray): BitmessageAddress? = findByRipeOrTag("private_key is not null", ripeOrTag) | ||||||
|  |  | ||||||
|  |     private fun findByRipeOrTag(where: String, ripeOrTag: ByteArray): BitmessageAddress? { | ||||||
|  |         for (address in find(where)) { | ||||||
|  |             if (address.version > 3) { | ||||||
|  |                 if (Arrays.equals(ripeOrTag, address.tag)) return address | ||||||
|  |             } else { | ||||||
|  |                 if (Arrays.equals(ripeOrTag, address.ripe)) return address | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return null | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getIdentities() = find("private_key IS NOT NULL") | ||||||
|  |  | ||||||
|  |     override fun getChans() = find("chan = '1'") | ||||||
|  |  | ||||||
|  |     override fun getSubscriptions() = find("subscribed = '1'") | ||||||
|  |  | ||||||
|  |     override fun getSubscriptions(broadcastVersion: Long) = if (broadcastVersion > 4) { | ||||||
|  |         find("subscribed = '1' AND version > 3") | ||||||
|  |     } else { | ||||||
|  |         find("subscribed = '1' AND version <= 3") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getContacts() = find("private_key IS NULL OR chan = '1'") | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the contacts in the following order: | ||||||
|  |      * | ||||||
|  |      *  * Subscribed addresses come first | ||||||
|  |      *  * Addresses with Aliases (alphabetically) | ||||||
|  |      *  * Addresses (alphabetically) | ||||||
|  |      * | ||||||
|  |      * | ||||||
|  |      * @return the ordered list of ids (address strings) | ||||||
|  |      */ | ||||||
|  |     fun getContactIds(): List<String> = findIds( | ||||||
|  |         "private_key IS NULL OR chan = '1'", | ||||||
|  |         "$COLUMN_SUBSCRIBED DESC, $COLUMN_ALIAS IS NULL, $COLUMN_ALIAS, $COLUMN_ADDRESS" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     private fun findIds(where: String, orderBy: String): List<String> { | ||||||
|  |         val result = LinkedList<String>() | ||||||
|  |  | ||||||
|  |         // Define a projection that specifies which columns from the database | ||||||
|  |         // you will actually use after this query. | ||||||
|  |         val projection = arrayOf(COLUMN_ADDRESS) | ||||||
|  |  | ||||||
|  |         val db = sql.readableDatabase | ||||||
|  |         db.query( | ||||||
|  |             TABLE_NAME, projection, | ||||||
|  |             where, null, null, null, | ||||||
|  |             orderBy | ||||||
|  |         ).use { c -> | ||||||
|  |             while (c.moveToNext()) { | ||||||
|  |                 result.add(c.getString(c.getColumnIndex(COLUMN_ADDRESS))) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun find(where: String): List<BitmessageAddress> { | ||||||
|  |         val result = LinkedList<BitmessageAddress>() | ||||||
|  |  | ||||||
|  |         // Define a projection that specifies which columns from the database | ||||||
|  |         // you will actually use after this query. | ||||||
|  |         val projection = arrayOf(COLUMN_ADDRESS, COLUMN_ALIAS, COLUMN_PUBLIC_KEY, COLUMN_PRIVATE_KEY, COLUMN_SUBSCRIBED, COLUMN_CHAN) | ||||||
|  |  | ||||||
|  |         val db = sql.readableDatabase | ||||||
|  |         db.query( | ||||||
|  |             TABLE_NAME, projection, | ||||||
|  |             where, null, null, null, null | ||||||
|  |         ).use { c -> | ||||||
|  |             while (c.moveToNext()) { | ||||||
|  |                 result.add(getAddress(c)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun getAddress(c: Cursor): BitmessageAddress { | ||||||
|  |  | ||||||
|  |         fun getIdentity(c: Cursor) = c.getBlob(c.getColumnIndex(COLUMN_PRIVATE_KEY))?.let { | ||||||
|  |             BitmessageAddress(PrivateKey.read(ByteArrayInputStream(it))) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         fun getContact(c: Cursor) = BitmessageAddress(c.getString(c.getColumnIndex(COLUMN_ADDRESS))).also { address -> | ||||||
|  |             c.getBlob(c.getColumnIndex(COLUMN_PUBLIC_KEY))?.let { publicKeyBytes -> | ||||||
|  |                 Factory.readPubkey( | ||||||
|  |                     version = address.version, stream = address.stream, | ||||||
|  |                     input = ByteArrayInputStream(publicKeyBytes), length = publicKeyBytes.size, | ||||||
|  |                     encrypted = false | ||||||
|  |                 ).let { | ||||||
|  |                     address.pubkey = if (address.version == 4L && it is V3Pubkey) { | ||||||
|  |                         V4Pubkey(it) | ||||||
|  |                     } else { | ||||||
|  |                         it | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return (getIdentity(c) ?: getContact(c)).apply { | ||||||
|  |             alias = c.getString(c.getColumnIndex(COLUMN_ALIAS)) | ||||||
|  |             isChan = c.getInt(c.getColumnIndex(COLUMN_CHAN)) == 1 | ||||||
|  |             isSubscribed = c.getInt(c.getColumnIndex(COLUMN_SUBSCRIBED)) == 1 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun save(address: BitmessageAddress) = if (exists(address)) { | ||||||
|  |         update(address) | ||||||
|  |     } else { | ||||||
|  |         insert(address) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun exists(address: BitmessageAddress): Boolean { | ||||||
|  |         val db = sql.readableDatabase | ||||||
|  |         db.rawQuery( | ||||||
|  |             "SELECT COUNT(*) FROM Address WHERE address=?", | ||||||
|  |             arrayOf(address.address) | ||||||
|  |         ).use { cursor -> | ||||||
|  |             cursor.moveToFirst() | ||||||
|  |             return cursor.getInt(0) > 0 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun update(address: BitmessageAddress) { | ||||||
|  |         val db = sql.writableDatabase | ||||||
|  |         // Create a new map of values, where column names are the keys | ||||||
|  |         val values = getContentValues(address) | ||||||
|  |  | ||||||
|  |         val update = db.update(TABLE_NAME, values, "address=?", arrayOf(address.address)) | ||||||
|  |         if (update < 0) { | ||||||
|  |             LOG.error("Could not update address {}", address) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun insert(address: BitmessageAddress) { | ||||||
|  |         val db = sql.writableDatabase | ||||||
|  |         // Create a new map of values, where column names are the keys | ||||||
|  |         val values = getContentValues(address) | ||||||
|  |         values.put(COLUMN_ADDRESS, address.address) | ||||||
|  |         values.put(COLUMN_VERSION, address.version) | ||||||
|  |         values.put(COLUMN_CHAN, address.isChan) | ||||||
|  |  | ||||||
|  |         val insert = db.insert(TABLE_NAME, null, values) | ||||||
|  |         if (insert < 0) { | ||||||
|  |             LOG.error("Could not insert address {}", address) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun getContentValues(address: BitmessageAddress): ContentValues { | ||||||
|  |         val values = ContentValues() | ||||||
|  |         address.alias?.let { values.put(COLUMN_ALIAS, it) } | ||||||
|  |         address.pubkey?.let { pubkey -> | ||||||
|  |             val out = ByteArrayOutputStream() | ||||||
|  |             pubkey.writer().writeUnencrypted(out) | ||||||
|  |             values.put(COLUMN_PUBLIC_KEY, out.toByteArray()) | ||||||
|  |         } | ||||||
|  |         address.privateKey?.let { values.put(COLUMN_PRIVATE_KEY, Encode.bytes(it)) } | ||||||
|  |         if (address.isChan) { | ||||||
|  |             values.put(COLUMN_CHAN, true) | ||||||
|  |         } | ||||||
|  |         values.put(COLUMN_SUBSCRIBED, address.isSubscribed) | ||||||
|  |         return values | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun remove(address: BitmessageAddress) { | ||||||
|  |         val db = sql.writableDatabase | ||||||
|  |         db.delete(TABLE_NAME, "address = ?", arrayOf(address.address)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getAddress(address: String) = find("address = '$address'").firstOrNull() | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val LOG = LoggerFactory.getLogger(AndroidAddressRepository::class.java) | ||||||
|  |  | ||||||
|  |         private const val TABLE_NAME = "Address" | ||||||
|  |         private const val COLUMN_ADDRESS = "address" | ||||||
|  |         private const val COLUMN_VERSION = "version" | ||||||
|  |         private const val COLUMN_ALIAS = "alias" | ||||||
|  |         private const val COLUMN_PUBLIC_KEY = "public_key" | ||||||
|  |         private const val COLUMN_PRIVATE_KEY = "private_key" | ||||||
|  |         private const val COLUMN_SUBSCRIBED = "subscribed" | ||||||
|  |         private const val COLUMN_CHAN = "chan" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,232 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.repository; |  | ||||||
|  |  | ||||||
| import android.content.ContentValues; |  | ||||||
| import android.database.Cursor; |  | ||||||
| import android.database.sqlite.SQLiteConstraintException; |  | ||||||
| import android.database.sqlite.SQLiteDatabase; |  | ||||||
|  |  | ||||||
| import org.slf4j.Logger; |  | ||||||
| import org.slf4j.LoggerFactory; |  | ||||||
|  |  | ||||||
| import java.io.ByteArrayInputStream; |  | ||||||
| import java.util.Iterator; |  | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Map; |  | ||||||
| import java.util.concurrent.ConcurrentHashMap; |  | ||||||
|  |  | ||||||
| import ch.dissem.bitmessage.entity.ObjectMessage; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.ObjectType; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.InventoryVector; |  | ||||||
| import ch.dissem.bitmessage.factory.Factory; |  | ||||||
| import ch.dissem.bitmessage.ports.Inventory; |  | ||||||
| import ch.dissem.bitmessage.utils.Encode; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.repository.SqlHelper.join; |  | ||||||
| import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; |  | ||||||
| import static ch.dissem.bitmessage.utils.UnixTime.now; |  | ||||||
| import static java.lang.String.valueOf; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * {@link Inventory} implementation using the Android SQL API. |  | ||||||
|  */ |  | ||||||
| public class AndroidInventory implements Inventory { |  | ||||||
|     private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class); |  | ||||||
|  |  | ||||||
|     private static final String TABLE_NAME = "Inventory"; |  | ||||||
|     private static final String COLUMN_HASH = "hash"; |  | ||||||
|     private static final String COLUMN_STREAM = "stream"; |  | ||||||
|     private static final String COLUMN_EXPIRES = "expires"; |  | ||||||
|     private static final String COLUMN_DATA = "data"; |  | ||||||
|     private static final String COLUMN_TYPE = "type"; |  | ||||||
|     private static final String COLUMN_VERSION = "version"; |  | ||||||
|  |  | ||||||
|     private final SqlHelper sql; |  | ||||||
|  |  | ||||||
|     private final Map<Long, Map<InventoryVector, Long>> cache = new ConcurrentHashMap<>(); |  | ||||||
|  |  | ||||||
|     public AndroidInventory(SqlHelper sql) { |  | ||||||
|         this.sql = sql; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<InventoryVector> getInventory(long... streams) { |  | ||||||
|         List<InventoryVector> result = new LinkedList<>(); |  | ||||||
|         long now = now(); |  | ||||||
|         for (long stream : streams) { |  | ||||||
|             for (Map.Entry<InventoryVector, Long> e : getCache(stream).entrySet()) { |  | ||||||
|                 if (e.getValue() > now) { |  | ||||||
|                     result.add(e.getKey()); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private Map<InventoryVector, Long> getCache(long stream) { |  | ||||||
|         Map<InventoryVector, Long> result = cache.get(stream); |  | ||||||
|         if (result == null) { |  | ||||||
|             synchronized (cache) { |  | ||||||
|                 if (cache.get(stream) == null) { |  | ||||||
|                     result = new ConcurrentHashMap<>(); |  | ||||||
|                     cache.put(stream, result); |  | ||||||
|  |  | ||||||
|                     String[] projection = { |  | ||||||
|                         COLUMN_HASH, COLUMN_EXPIRES |  | ||||||
|                     }; |  | ||||||
|  |  | ||||||
|                     SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|                     try (Cursor c = db.query( |  | ||||||
|                         TABLE_NAME, projection, |  | ||||||
|                         "stream = " + stream, |  | ||||||
|                         null, null, null, null |  | ||||||
|                     )) { |  | ||||||
|                         while (c.moveToNext()) { |  | ||||||
|                             byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)); |  | ||||||
|                             long expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES)); |  | ||||||
|                             result.put(new InventoryVector(blob), expires); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     LOG.info("Stream #" + stream + " inventory size: " + result.size()); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<InventoryVector> getMissing(List<InventoryVector> offer, long... streams) { |  | ||||||
|         for (long stream : streams) { |  | ||||||
|             offer.removeAll(getCache(stream).keySet()); |  | ||||||
|         } |  | ||||||
|         LOG.info(offer.size() + " objects missing."); |  | ||||||
|         return offer; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public ObjectMessage getObject(InventoryVector vector) { |  | ||||||
|         // Define a projection that specifies which columns from the database |  | ||||||
|         // you will actually use after this query. |  | ||||||
|         String[] projection = { |  | ||||||
|             COLUMN_VERSION, |  | ||||||
|             COLUMN_DATA |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         try (Cursor c = db.query( |  | ||||||
|             TABLE_NAME, projection, |  | ||||||
|             "hash = X'" + vector + "'", |  | ||||||
|             null, null, null, null |  | ||||||
|         )) { |  | ||||||
|             if (!c.moveToFirst()) { |  | ||||||
|                 LOG.info("Object requested that we don't have. IV: " + vector); |  | ||||||
|                 return null; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); |  | ||||||
|             byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); |  | ||||||
|             return Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob.length); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) { |  | ||||||
|         // Define a projection that specifies which columns from the database |  | ||||||
|         // you will actually use after this query. |  | ||||||
|         String[] projection = { |  | ||||||
|             COLUMN_VERSION, |  | ||||||
|             COLUMN_DATA |  | ||||||
|         }; |  | ||||||
|         StringBuilder where = new StringBuilder("1=1"); |  | ||||||
|         if (stream > 0) { |  | ||||||
|             where.append(" AND stream = ").append(stream); |  | ||||||
|         } |  | ||||||
|         if (version > 0) { |  | ||||||
|             where.append(" AND version = ").append(version); |  | ||||||
|         } |  | ||||||
|         if (types.length > 0) { |  | ||||||
|             where.append(" AND type IN (").append(join(types)).append(")"); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         List<ObjectMessage> result = new LinkedList<>(); |  | ||||||
|         try (Cursor c = db.query( |  | ||||||
|             TABLE_NAME, projection, |  | ||||||
|             where.toString(), |  | ||||||
|             null, null, null, null |  | ||||||
|         )) { |  | ||||||
|             while (c.moveToNext()) { |  | ||||||
|                 int objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION)); |  | ||||||
|                 byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); |  | ||||||
|                 result.add(Factory.getObjectMessage(objectVersion, new ByteArrayInputStream(blob), |  | ||||||
|                     blob.length)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void storeObject(ObjectMessage object) { |  | ||||||
|         InventoryVector iv = object.getInventoryVector(); |  | ||||||
|  |  | ||||||
|         if (getCache(object.getStream()).containsKey(iv)) |  | ||||||
|             return; |  | ||||||
|  |  | ||||||
|         LOG.trace("Storing object " + iv); |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|             SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|             // Create a new map of values, where column names are the keys |  | ||||||
|             ContentValues values = new ContentValues(); |  | ||||||
|             values.put(COLUMN_HASH, object.getInventoryVector().getHash()); |  | ||||||
|             values.put(COLUMN_STREAM, object.getStream()); |  | ||||||
|             values.put(COLUMN_EXPIRES, object.getExpiresTime()); |  | ||||||
|             values.put(COLUMN_DATA, Encode.bytes(object)); |  | ||||||
|             values.put(COLUMN_TYPE, object.getType()); |  | ||||||
|             values.put(COLUMN_VERSION, object.getVersion()); |  | ||||||
|  |  | ||||||
|             db.insertOrThrow(TABLE_NAME, null, values); |  | ||||||
|  |  | ||||||
|             getCache(object.getStream()).put(iv, object.getExpiresTime()); |  | ||||||
|         } catch (SQLiteConstraintException e) { |  | ||||||
|             LOG.trace(e.getMessage(), e); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public boolean contains(ObjectMessage object) { |  | ||||||
|         return getCache(object.getStream()).keySet().contains(object.getInventoryVector()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void cleanup() { |  | ||||||
|         long fiveMinutesAgo = now() - 5 * MINUTE; |  | ||||||
|         SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|         db.delete(TABLE_NAME, "expires < ?", new String[]{valueOf(fiveMinutesAgo)}); |  | ||||||
|  |  | ||||||
|         for (Map<InventoryVector, Long> c : cache.values()) { |  | ||||||
|             Iterator<Map.Entry<InventoryVector, Long>> iterator = c.entrySet().iterator(); |  | ||||||
|             while (iterator.hasNext()) { |  | ||||||
|                 if (iterator.next().getValue() < fiveMinutesAgo) { |  | ||||||
|                     iterator.remove(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,182 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.repository | ||||||
|  |  | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.database.sqlite.SQLiteConstraintException | ||||||
|  | import ch.dissem.bitmessage.entity.ObjectMessage | ||||||
|  | import ch.dissem.bitmessage.entity.payload.ObjectType | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.InventoryVector | ||||||
|  | import ch.dissem.bitmessage.factory.Factory | ||||||
|  | import ch.dissem.bitmessage.ports.Inventory | ||||||
|  | import ch.dissem.bitmessage.utils.Encode | ||||||
|  | import ch.dissem.bitmessage.utils.UnixTime.MINUTE | ||||||
|  | import ch.dissem.bitmessage.utils.UnixTime.now | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import java.io.ByteArrayInputStream | ||||||
|  | import java.util.* | ||||||
|  | import java.util.concurrent.ConcurrentHashMap | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * [Inventory] implementation using the Android SQL API. | ||||||
|  |  */ | ||||||
|  | class AndroidInventory(private val sql: SqlHelper) : Inventory { | ||||||
|  |  | ||||||
|  |     private val cache = ConcurrentHashMap<Long, MutableMap<InventoryVector, Long>>() | ||||||
|  |  | ||||||
|  |     override fun getInventory(vararg streams: Long): List<InventoryVector> { | ||||||
|  |         val result = LinkedList<InventoryVector>() | ||||||
|  |         val now = now | ||||||
|  |         for (stream in streams) { | ||||||
|  |             for ((key, value) in getCache(stream)) { | ||||||
|  |                 if (value > now) { | ||||||
|  |                     result.add(key) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun getCache(stream: Long): MutableMap<InventoryVector, Long> { | ||||||
|  |         fun addToCache(stream: Long): MutableMap<InventoryVector, Long> { | ||||||
|  |             val result: MutableMap<InventoryVector, Long> = ConcurrentHashMap() | ||||||
|  |             cache.put(stream, result) | ||||||
|  |  | ||||||
|  |             val projection = arrayOf(COLUMN_HASH, COLUMN_EXPIRES) | ||||||
|  |             val db = sql.readableDatabase | ||||||
|  |             db.query( | ||||||
|  |                     TABLE_NAME, projection, | ||||||
|  |                     "stream = $stream", null, null, null, null | ||||||
|  |             ).use { c -> | ||||||
|  |                 while (c.moveToNext()) { | ||||||
|  |                     val blob = c.getBlob(c.getColumnIndex(COLUMN_HASH)) | ||||||
|  |                     val expires = c.getLong(c.getColumnIndex(COLUMN_EXPIRES)) | ||||||
|  |                     InventoryVector.fromHash(blob)?.let { result.put(it, expires) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             LOG.info("Stream #$stream inventory size: ${result.size}") | ||||||
|  |             return result | ||||||
|  |         } | ||||||
|  |         return cache[stream] ?: synchronized(cache) { | ||||||
|  |             return@synchronized cache[stream] ?: addToCache(stream) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     override fun getMissing(offer: List<InventoryVector>, vararg streams: Long) = offer - streams.flatMap { getCache(it).keys } | ||||||
|  |  | ||||||
|  |     override fun getObject(vector: InventoryVector): ObjectMessage? { | ||||||
|  |         // Define a projection that specifies which columns from the database | ||||||
|  |         // you will actually use after this query. | ||||||
|  |         val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA) | ||||||
|  |  | ||||||
|  |         val db = sql.readableDatabase | ||||||
|  |         db.query( | ||||||
|  |                 TABLE_NAME, projection, | ||||||
|  |                 "hash = X'$vector'", null, null, null, null | ||||||
|  |         ).use { c -> | ||||||
|  |             if (!c.moveToFirst()) { | ||||||
|  |                 LOG.info("Object requested that we don't have. IV: {}", vector) | ||||||
|  |                 return null | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             val version = c.getInt(c.getColumnIndex(COLUMN_VERSION)) | ||||||
|  |             val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)) | ||||||
|  |             return Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getObjects(stream: Long, version: Long, vararg types: ObjectType): List<ObjectMessage> { | ||||||
|  |         // Define a projection that specifies which columns from the database | ||||||
|  |         // you will actually use after this query. | ||||||
|  |         val projection = arrayOf(COLUMN_VERSION, COLUMN_DATA) | ||||||
|  |         val where = StringBuilder("1=1") | ||||||
|  |         if (stream > 0) { | ||||||
|  |             where.append(" AND stream = ").append(stream) | ||||||
|  |         } | ||||||
|  |         if (version > 0) { | ||||||
|  |             where.append(" AND version = ").append(version) | ||||||
|  |         } | ||||||
|  |         if (types.isNotEmpty()) { | ||||||
|  |             where.append(" AND type IN (").append(types.joinToString(separator = "', '", prefix = "'", postfix = "'", transform = { it.number.toString() })).append(")") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val db = sql.readableDatabase | ||||||
|  |         val result = LinkedList<ObjectMessage>() | ||||||
|  |         db.query( | ||||||
|  |                 TABLE_NAME, projection, | ||||||
|  |                 where.toString(), null, null, null, null | ||||||
|  |         ).use { c -> | ||||||
|  |             while (c.moveToNext()) { | ||||||
|  |                 val objectVersion = c.getInt(c.getColumnIndex(COLUMN_VERSION)) | ||||||
|  |                 val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)) | ||||||
|  |                 Factory.getObjectMessage(objectVersion, ByteArrayInputStream(blob), blob.size)?.let { result.add(it) } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun storeObject(objectMessage: ObjectMessage) { | ||||||
|  |         val iv = objectMessage.inventoryVector | ||||||
|  |  | ||||||
|  |         if (getCache(objectMessage.stream).containsKey(iv)) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         LOG.trace("Storing object {}", iv) | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             val db = sql.writableDatabase | ||||||
|  |             // Create a new map of values, where column names are the keys | ||||||
|  |             val values = ContentValues() | ||||||
|  |             values.put(COLUMN_HASH, objectMessage.inventoryVector.hash) | ||||||
|  |             values.put(COLUMN_STREAM, objectMessage.stream) | ||||||
|  |             values.put(COLUMN_EXPIRES, objectMessage.expiresTime) | ||||||
|  |             values.put(COLUMN_DATA, Encode.bytes(objectMessage)) | ||||||
|  |             values.put(COLUMN_TYPE, objectMessage.type) | ||||||
|  |             values.put(COLUMN_VERSION, objectMessage.version) | ||||||
|  |  | ||||||
|  |             db.insertOrThrow(TABLE_NAME, null, values) | ||||||
|  |  | ||||||
|  |             getCache(objectMessage.stream).put(iv, objectMessage.expiresTime) | ||||||
|  |         } catch (e: SQLiteConstraintException) { | ||||||
|  |             LOG.trace(e.message, e) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun contains(objectMessage: ObjectMessage) = getCache(objectMessage.stream).keys.contains(objectMessage.inventoryVector) | ||||||
|  |  | ||||||
|  |     override fun cleanup() { | ||||||
|  |         val fiveMinutesAgo = now - 5 * MINUTE | ||||||
|  |         val db = sql.writableDatabase | ||||||
|  |         db.delete(TABLE_NAME, "expires < ?", arrayOf(fiveMinutesAgo.toString())) | ||||||
|  |  | ||||||
|  |         cache.values.map { it.entries }.forEach { entries -> entries.removeAll { it.value < fiveMinutesAgo } } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java) | ||||||
|  |  | ||||||
|  |         private const val TABLE_NAME = "Inventory" | ||||||
|  |         private const val COLUMN_HASH = "hash" | ||||||
|  |         private const val COLUMN_STREAM = "stream" | ||||||
|  |         private const val COLUMN_EXPIRES = "expires" | ||||||
|  |         private const val COLUMN_DATA = "data" | ||||||
|  |         private const val COLUMN_TYPE = "type" | ||||||
|  |         private const val COLUMN_VERSION = "version" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,109 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.repository | ||||||
|  |  | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.content.Context | ||||||
|  | import android.database.Cursor | ||||||
|  | import android.database.DatabaseUtils | ||||||
|  | import ch.dissem.apps.abit.util.Labels | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.Label | ||||||
|  | import ch.dissem.bitmessage.ports.AbstractLabelRepository | ||||||
|  | import ch.dissem.bitmessage.ports.MessageRepository | ||||||
|  | import org.jetbrains.anko.db.transaction | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * [MessageRepository] implementation using the Android SQL API. | ||||||
|  |  */ | ||||||
|  | class AndroidLabelRepository(private val sql: SqlHelper, private val context: Context) : AbstractLabelRepository() { | ||||||
|  |  | ||||||
|  |     override fun find(where: String): List<Label> { | ||||||
|  |         val result = LinkedList<Label>() | ||||||
|  |  | ||||||
|  |         // Define a projection that specifies which columns from the database | ||||||
|  |         // you will actually use after this query. | ||||||
|  |         val projection = arrayOf(COLUMN_ID, COLUMN_LABEL, COLUMN_TYPE, COLUMN_COLOR) | ||||||
|  |  | ||||||
|  |         sql.readableDatabase.query( | ||||||
|  |             TABLE_NAME, projection, | ||||||
|  |             where, null, null, null, | ||||||
|  |             COLUMN_ORDER | ||||||
|  |         ).use { c -> | ||||||
|  |             while (c.moveToNext()) { | ||||||
|  |                 result.add(getLabel(c, context)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun save(label: Label) { | ||||||
|  |         val db = sql.writableDatabase | ||||||
|  |         if (label.id != null) { | ||||||
|  |             val values = ContentValues() | ||||||
|  |             values.put(COLUMN_LABEL, label.toString()) | ||||||
|  |             values.put(COLUMN_TYPE, label.type?.name) | ||||||
|  |             values.put(COLUMN_COLOR, label.color) | ||||||
|  |             values.put(COLUMN_ORDER, label.ord) | ||||||
|  |             db.update(TABLE_NAME, values, "id=?", arrayOf(label.id.toString())) | ||||||
|  |         } else { | ||||||
|  |             db.transaction { | ||||||
|  |                 val exists = DatabaseUtils.queryNumEntries(db, TABLE_NAME, "label=?", arrayOf(label.toString())) > 0 | ||||||
|  |  | ||||||
|  |                 if (exists) { | ||||||
|  |                     val values = ContentValues() | ||||||
|  |                     values.put(COLUMN_TYPE, label.type?.name) | ||||||
|  |                     values.put(COLUMN_COLOR, label.color) | ||||||
|  |                     values.put(COLUMN_ORDER, label.ord) | ||||||
|  |                     db.update(TABLE_NAME, values, "label=?", arrayOf(label.toString())) | ||||||
|  |                 } else { | ||||||
|  |                     val values = ContentValues() | ||||||
|  |                     values.put(COLUMN_LABEL, label.toString()) | ||||||
|  |                     values.put(COLUMN_TYPE, label.type?.name) | ||||||
|  |                     values.put(COLUMN_COLOR, label.color) | ||||||
|  |                     values.put(COLUMN_ORDER, label.ord) | ||||||
|  |                     db.insertOrThrow(TABLE_NAME, null, values) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     internal fun findLabels(msgId: Any) = find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)") | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val LABEL_ARCHIVE = Label("archive", null, 0) | ||||||
|  |  | ||||||
|  |         private const val TABLE_NAME = "Label" | ||||||
|  |         private const val COLUMN_ID = "id" | ||||||
|  |         private const val COLUMN_LABEL = "label" | ||||||
|  |         private const val COLUMN_TYPE = "type" | ||||||
|  |         private const val COLUMN_COLOR = "color" | ||||||
|  |         private const val COLUMN_ORDER = "ord" | ||||||
|  |  | ||||||
|  |         internal fun getLabel(c: Cursor, context: Context): Label { | ||||||
|  |             val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE)) | ||||||
|  |             val type = if (typeName == null) null else Label.Type.valueOf(typeName) | ||||||
|  |             val text: String? = Labels.getText(type, null, context) | ||||||
|  |             val label = Label( | ||||||
|  |                 text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)), | ||||||
|  |                 type, | ||||||
|  |                 c.getInt(c.getColumnIndex(COLUMN_COLOR))) | ||||||
|  |             label.id = c.getLong(c.getColumnIndex(COLUMN_ID)) | ||||||
|  |             return label | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,386 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.repository; |  | ||||||
|  |  | ||||||
| import android.content.ContentValues; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.database.Cursor; |  | ||||||
| import android.database.DatabaseUtils; |  | ||||||
| import android.database.sqlite.SQLiteConstraintException; |  | ||||||
| import android.database.sqlite.SQLiteDatabase; |  | ||||||
|  |  | ||||||
| import org.slf4j.Logger; |  | ||||||
| import org.slf4j.LoggerFactory; |  | ||||||
|  |  | ||||||
| import java.io.ByteArrayInputStream; |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.util.Collection; |  | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.UUID; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.util.Labels; |  | ||||||
| import ch.dissem.apps.abit.util.UuidUtils; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.InventoryVector; |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.Label; |  | ||||||
| import ch.dissem.bitmessage.ports.AbstractMessageRepository; |  | ||||||
| import ch.dissem.bitmessage.ports.MessageRepository; |  | ||||||
| import ch.dissem.bitmessage.utils.Encode; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.util.UuidUtils.asUuid; |  | ||||||
| import static ch.dissem.bitmessage.utils.Strings.hex; |  | ||||||
| import static java.lang.String.valueOf; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * {@link MessageRepository} implementation using the Android SQL API. |  | ||||||
|  */ |  | ||||||
| public class AndroidMessageRepository extends AbstractMessageRepository { |  | ||||||
|     private static final Logger LOG = LoggerFactory.getLogger(AndroidMessageRepository.class); |  | ||||||
|  |  | ||||||
|     public static final Label LABEL_ARCHIVE = new Label("archive", null, 0); |  | ||||||
|  |  | ||||||
|     private static final String TABLE_NAME = "Message"; |  | ||||||
|     private static final String COLUMN_ID = "id"; |  | ||||||
|     private static final String COLUMN_IV = "iv"; |  | ||||||
|     private static final String COLUMN_TYPE = "type"; |  | ||||||
|     private static final String COLUMN_SENDER = "sender"; |  | ||||||
|     private static final String COLUMN_RECIPIENT = "recipient"; |  | ||||||
|     private static final String COLUMN_DATA = "data"; |  | ||||||
|     private static final String COLUMN_ACK_DATA = "ack_data"; |  | ||||||
|     private static final String COLUMN_SENT = "sent"; |  | ||||||
|     private static final String COLUMN_RECEIVED = "received"; |  | ||||||
|     private static final String COLUMN_STATUS = "status"; |  | ||||||
|     private static final String COLUMN_TTL = "ttl"; |  | ||||||
|     private static final String COLUMN_RETRIES = "retries"; |  | ||||||
|     private static final String COLUMN_NEXT_TRY = "next_try"; |  | ||||||
|     private static final String COLUMN_INITIAL_HASH = "initial_hash"; |  | ||||||
|     private static final String COLUMN_CONVERSATION = "conversation"; |  | ||||||
|  |  | ||||||
|     private static final String PARENTS_TABLE_NAME = "Message_Parent"; |  | ||||||
|  |  | ||||||
|     private static final String JOIN_TABLE_NAME = "Message_Label"; |  | ||||||
|     private static final String JT_COLUMN_MESSAGE = "message_id"; |  | ||||||
|     private static final String JT_COLUMN_LABEL = "label_id"; |  | ||||||
|  |  | ||||||
|     private static final String LBL_TABLE_NAME = "Label"; |  | ||||||
|     private static final String LBL_COLUMN_ID = "id"; |  | ||||||
|     private static final String LBL_COLUMN_LABEL = "label"; |  | ||||||
|     private static final String LBL_COLUMN_TYPE = "type"; |  | ||||||
|     private static final String LBL_COLUMN_COLOR = "color"; |  | ||||||
|     private static final String LBL_COLUMN_ORDER = "ord"; |  | ||||||
|     private final SqlHelper sql; |  | ||||||
|     private final Context context; |  | ||||||
|  |  | ||||||
|     public AndroidMessageRepository(SqlHelper sql, Context ctx) { |  | ||||||
|         this.sql = sql; |  | ||||||
|         this.context = ctx; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<Plaintext> findMessages(Label label) { |  | ||||||
|         if (label == LABEL_ARCHIVE) { |  | ||||||
|             return super.findMessages((Label) null); |  | ||||||
|         } else { |  | ||||||
|             return super.findMessages(label); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public List<Label> findLabels(String where) { |  | ||||||
|         List<Label> result = new LinkedList<>(); |  | ||||||
|  |  | ||||||
|         // Define a projection that specifies which columns from the database |  | ||||||
|         // you will actually use after this query. |  | ||||||
|         String[] projection = { |  | ||||||
|             LBL_COLUMN_ID, |  | ||||||
|             LBL_COLUMN_LABEL, |  | ||||||
|             LBL_COLUMN_TYPE, |  | ||||||
|             LBL_COLUMN_COLOR |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         try (Cursor c = db.query( |  | ||||||
|             LBL_TABLE_NAME, projection, |  | ||||||
|             where, |  | ||||||
|             null, null, null, |  | ||||||
|             LBL_COLUMN_ORDER |  | ||||||
|         )) { |  | ||||||
|             while (c.moveToNext()) { |  | ||||||
|                 result.add(getLabel(c)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private Label getLabel(Cursor c) { |  | ||||||
|         String typeName = c.getString(c.getColumnIndex(LBL_COLUMN_TYPE)); |  | ||||||
|         Label.Type type = typeName == null ? null : Label.Type.valueOf(typeName); |  | ||||||
|         String text = Labels.getText(type, null, context); |  | ||||||
|         if (text == null) { |  | ||||||
|             text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); |  | ||||||
|         } |  | ||||||
|         Label label = new Label( |  | ||||||
|             text, |  | ||||||
|             type, |  | ||||||
|             c.getInt(c.getColumnIndex(LBL_COLUMN_COLOR))); |  | ||||||
|         label.setId(c.getLong(c.getColumnIndex(LBL_COLUMN_ID))); |  | ||||||
|         return label; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public int countUnread(Label label) { |  | ||||||
|         String[] args; |  | ||||||
|         String where; |  | ||||||
|         if (label == null) { |  | ||||||
|             return 0; |  | ||||||
|         } |  | ||||||
|         if (label == LABEL_ARCHIVE) { |  | ||||||
|             where = ""; |  | ||||||
|             args = new String[]{ |  | ||||||
|                 Label.Type.UNREAD.name() |  | ||||||
|             }; |  | ||||||
|         } else { |  | ||||||
|             where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=?) AND "; |  | ||||||
|             args = new String[]{ |  | ||||||
|                 label.getId().toString(), |  | ||||||
|                 Label.Type.UNREAD.name() |  | ||||||
|             }; |  | ||||||
|         } |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         return (int) DatabaseUtils.queryNumEntries(db, TABLE_NAME, |  | ||||||
|             where + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + |  | ||||||
|                 "SELECT id FROM Label WHERE type=?))", |  | ||||||
|             args |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<UUID> findConversations(Label label) { |  | ||||||
|         String[] projection = { |  | ||||||
|             COLUMN_CONVERSATION, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         String where; |  | ||||||
|         if (label == null) { |  | ||||||
|             where = "id NOT IN (SELECT message_id FROM Message_Label)"; |  | ||||||
|         } else { |  | ||||||
|             where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"; |  | ||||||
|         } |  | ||||||
|         List<UUID> result = new LinkedList<>(); |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         try (Cursor c = db.query( |  | ||||||
|             TABLE_NAME, projection, |  | ||||||
|             where, |  | ||||||
|             null, null, null, null |  | ||||||
|         )) { |  | ||||||
|             while (c.moveToNext()) { |  | ||||||
|                 byte[] uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)); |  | ||||||
|                 result.add(asUuid(uuidBytes)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     private void updateParents(SQLiteDatabase db, Plaintext message) { |  | ||||||
|         if (message.getInventoryVector() == null || message.getParents().isEmpty()) { |  | ||||||
|             // There are no parents to save yet (they are saved in the extended data, that's enough for now) |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         byte[] childIV = message.getInventoryVector().getHash(); |  | ||||||
|         db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(childIV).toString()}); |  | ||||||
|  |  | ||||||
|         // save new parents |  | ||||||
|         int order = 0; |  | ||||||
|         for (InventoryVector parentIV : message.getParents()) { |  | ||||||
|             Plaintext parent = getMessage(parentIV); |  | ||||||
|             mergeConversations(db, parent.getConversationId(), message.getConversationId()); |  | ||||||
|             order++; |  | ||||||
|             ContentValues values = new ContentValues(); |  | ||||||
|             values.put("parent", parentIV.getHash()); |  | ||||||
|             values.put("child", childIV); |  | ||||||
|             values.put("pos", order); |  | ||||||
|             values.put("conversation", UuidUtils.asBytes(message.getConversationId())); |  | ||||||
|             db.insertOrThrow(PARENTS_TABLE_NAME, null, values); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Replaces every occurrence of the source conversation ID with the target ID |  | ||||||
|      * |  | ||||||
|      * @param db     is used to keep everything within one transaction |  | ||||||
|      * @param source ID of the conversation to be merged |  | ||||||
|      * @param target ID of the merge target |  | ||||||
|      */ |  | ||||||
|     private void mergeConversations(SQLiteDatabase db, UUID source, UUID target) { |  | ||||||
|         ContentValues values = new ContentValues(); |  | ||||||
|         values.put("conversation", UuidUtils.asBytes(target)); |  | ||||||
|         String[] whereArgs = {hex(UuidUtils.asBytes(source)).toString()}; |  | ||||||
|         db.update(TABLE_NAME, values, "conversation=?", whereArgs); |  | ||||||
|         db.update(PARENTS_TABLE_NAME, values, "conversation=?", whereArgs); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected List<Plaintext> find(String where) { |  | ||||||
|         List<Plaintext> result = new LinkedList<>(); |  | ||||||
|  |  | ||||||
|         // Define a projection that specifies which columns from the database |  | ||||||
|         // you will actually use after this query. |  | ||||||
|         String[] projection = { |  | ||||||
|             COLUMN_ID, |  | ||||||
|             COLUMN_IV, |  | ||||||
|             COLUMN_TYPE, |  | ||||||
|             COLUMN_SENDER, |  | ||||||
|             COLUMN_RECIPIENT, |  | ||||||
|             COLUMN_DATA, |  | ||||||
|             COLUMN_ACK_DATA, |  | ||||||
|             COLUMN_SENT, |  | ||||||
|             COLUMN_RECEIVED, |  | ||||||
|             COLUMN_STATUS, |  | ||||||
|             COLUMN_TTL, |  | ||||||
|             COLUMN_RETRIES, |  | ||||||
|             COLUMN_NEXT_TRY, |  | ||||||
|             COLUMN_CONVERSATION |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         try (Cursor c = db.query( |  | ||||||
|             TABLE_NAME, projection, |  | ||||||
|             where, |  | ||||||
|             null, null, null, |  | ||||||
|             COLUMN_RECEIVED + " DESC, " + COLUMN_SENT + " DESC" |  | ||||||
|         )) { |  | ||||||
|             while (c.moveToNext()) { |  | ||||||
|                 byte[] iv = c.getBlob(c.getColumnIndex(COLUMN_IV)); |  | ||||||
|                 byte[] data = c.getBlob(c.getColumnIndex(COLUMN_DATA)); |  | ||||||
|                 Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex |  | ||||||
|                     (COLUMN_TYPE))); |  | ||||||
|                 Plaintext.Builder builder = Plaintext.readWithoutSignature(type, |  | ||||||
|                     new ByteArrayInputStream(data)); |  | ||||||
|                 long id = c.getLong(c.getColumnIndex(COLUMN_ID)); |  | ||||||
|                 builder.id(id); |  | ||||||
|                 builder.IV(new InventoryVector(iv)); |  | ||||||
|                 builder.from(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex |  | ||||||
|                     (COLUMN_SENDER)))); |  | ||||||
|                 builder.to(ctx.getAddressRepository().getAddress(c.getString(c.getColumnIndex |  | ||||||
|                     (COLUMN_RECIPIENT)))); |  | ||||||
|                 builder.ackData(c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA))); |  | ||||||
|                 builder.sent(c.getLong(c.getColumnIndex(COLUMN_SENT))); |  | ||||||
|                 builder.received(c.getLong(c.getColumnIndex(COLUMN_RECEIVED))); |  | ||||||
|                 builder.status(Plaintext.Status.valueOf(c.getString(c.getColumnIndex |  | ||||||
|                     (COLUMN_STATUS)))); |  | ||||||
|                 builder.ttl(c.getLong(c.getColumnIndex(COLUMN_TTL))); |  | ||||||
|                 builder.retries(c.getInt(c.getColumnIndex(COLUMN_RETRIES))); |  | ||||||
|                 int nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY); |  | ||||||
|                 if (!c.isNull(nextTryColumn)) { |  | ||||||
|                     builder.nextTry(c.getLong(nextTryColumn)); |  | ||||||
|                 } |  | ||||||
|                 builder.conversation(asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)))); |  | ||||||
|                 builder.labels(findLabels(id)); |  | ||||||
|                 result.add(builder.build()); |  | ||||||
|             } |  | ||||||
|         } catch (IOException e) { |  | ||||||
|             LOG.error(e.getMessage(), e); |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private Collection<Label> findLabels(long id) { |  | ||||||
|         return findLabels("id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ")"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void save(Plaintext message) { |  | ||||||
|         saveContactIfNecessary(message.getFrom()); |  | ||||||
|         saveContactIfNecessary(message.getTo()); |  | ||||||
|         SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|         try { |  | ||||||
|             db.beginTransaction(); |  | ||||||
|  |  | ||||||
|             // save message |  | ||||||
|             if (message.getId() == null) { |  | ||||||
|                 insert(db, message); |  | ||||||
|             } else { |  | ||||||
|                 update(db, message); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             updateParents(db, message); |  | ||||||
|  |  | ||||||
|             // remove existing labels |  | ||||||
|             db.delete(JOIN_TABLE_NAME, "message_id=?", new String[]{valueOf(message.getId())}); |  | ||||||
|  |  | ||||||
|             // save labels |  | ||||||
|             ContentValues values = new ContentValues(); |  | ||||||
|             for (Label label : message.getLabels()) { |  | ||||||
|                 values.put(JT_COLUMN_LABEL, (Long) label.getId()); |  | ||||||
|                 values.put(JT_COLUMN_MESSAGE, (Long) message.getId()); |  | ||||||
|                 db.insertOrThrow(JOIN_TABLE_NAME, null, values); |  | ||||||
|             } |  | ||||||
|             db.setTransactionSuccessful(); |  | ||||||
|         } catch (SQLiteConstraintException e) { |  | ||||||
|             LOG.trace(e.getMessage(), e); |  | ||||||
|         } finally { |  | ||||||
|             db.endTransaction(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void insert(SQLiteDatabase db, Plaintext message) { |  | ||||||
|         ContentValues values = new ContentValues(); |  | ||||||
|         values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message |  | ||||||
|             .getInventoryVector().getHash()); |  | ||||||
|         values.put(COLUMN_TYPE, message.getType().name()); |  | ||||||
|         values.put(COLUMN_SENDER, message.getFrom().getAddress()); |  | ||||||
|         values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress()); |  | ||||||
|         values.put(COLUMN_DATA, Encode.bytes(message)); |  | ||||||
|         values.put(COLUMN_ACK_DATA, message.getAckData()); |  | ||||||
|         values.put(COLUMN_SENT, message.getSent()); |  | ||||||
|         values.put(COLUMN_RECEIVED, message.getReceived()); |  | ||||||
|         values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name()); |  | ||||||
|         values.put(COLUMN_INITIAL_HASH, message.getInitialHash()); |  | ||||||
|         values.put(COLUMN_TTL, message.getTTL()); |  | ||||||
|         values.put(COLUMN_RETRIES, message.getRetries()); |  | ||||||
|         values.put(COLUMN_NEXT_TRY, message.getNextTry()); |  | ||||||
|         values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId())); |  | ||||||
|         long id = db.insertOrThrow(TABLE_NAME, null, values); |  | ||||||
|         message.setId(id); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void update(SQLiteDatabase db, Plaintext message) { |  | ||||||
|         ContentValues values = new ContentValues(); |  | ||||||
|         values.put(COLUMN_IV, message.getInventoryVector() == null ? null : message |  | ||||||
|             .getInventoryVector().getHash()); |  | ||||||
|         values.put(COLUMN_TYPE, message.getType().name()); |  | ||||||
|         values.put(COLUMN_SENDER, message.getFrom().getAddress()); |  | ||||||
|         values.put(COLUMN_RECIPIENT, message.getTo() == null ? null : message.getTo().getAddress()); |  | ||||||
|         values.put(COLUMN_DATA, Encode.bytes(message)); |  | ||||||
|         values.put(COLUMN_ACK_DATA, message.getAckData()); |  | ||||||
|         values.put(COLUMN_SENT, message.getSent()); |  | ||||||
|         values.put(COLUMN_RECEIVED, message.getReceived()); |  | ||||||
|         values.put(COLUMN_STATUS, message.getStatus() == null ? null : message.getStatus().name()); |  | ||||||
|         values.put(COLUMN_INITIAL_HASH, message.getInitialHash()); |  | ||||||
|         values.put(COLUMN_TTL, message.getTTL()); |  | ||||||
|         values.put(COLUMN_RETRIES, message.getRetries()); |  | ||||||
|         values.put(COLUMN_NEXT_TRY, message.getNextTry()); |  | ||||||
|         values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId())); |  | ||||||
|         db.update(TABLE_NAME, values, "id = " + message.getId(), null); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void remove(Plaintext message) { |  | ||||||
|         SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|         db.delete(TABLE_NAME, "id = " + message.getId(), null); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,266 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.repository | ||||||
|  |  | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.database.Cursor | ||||||
|  | import android.database.DatabaseUtils | ||||||
|  | import android.database.sqlite.SQLiteConstraintException | ||||||
|  | import android.database.sqlite.SQLiteDatabase | ||||||
|  | import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE | ||||||
|  | import ch.dissem.apps.abit.util.UuidUtils | ||||||
|  | import ch.dissem.apps.abit.util.UuidUtils.asUuid | ||||||
|  | import ch.dissem.bitmessage.entity.BitmessageAddress | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.InventoryVector | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.Label | ||||||
|  | import ch.dissem.bitmessage.ports.AbstractMessageRepository | ||||||
|  | import ch.dissem.bitmessage.ports.AlreadyStoredException | ||||||
|  | import ch.dissem.bitmessage.ports.MessageRepository | ||||||
|  | import ch.dissem.bitmessage.utils.Encode | ||||||
|  | import ch.dissem.bitmessage.utils.Strings.hex | ||||||
|  | import org.jetbrains.anko.db.transaction | ||||||
|  | import java.io.ByteArrayInputStream | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * [MessageRepository] implementation using the Android SQL API. | ||||||
|  |  */ | ||||||
|  | class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() { | ||||||
|  |  | ||||||
|  |     override fun findMessages(label: Label?, offset: Int, limit: Int) = if (label === LABEL_ARCHIVE) { | ||||||
|  |         super.findMessages(null as Label?, offset, limit) | ||||||
|  |     } else { | ||||||
|  |         super.findMessages(label, offset, limit) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun countUnread(label: Label?) = when { | ||||||
|  |         label === LABEL_ARCHIVE -> 0 | ||||||
|  |         label == null -> DatabaseUtils.queryNumEntries( | ||||||
|  |             sql.readableDatabase, | ||||||
|  |             TABLE_NAME, | ||||||
|  |             "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))", | ||||||
|  |             arrayOf(Label.Type.UNREAD.name) | ||||||
|  |         ).toInt() | ||||||
|  |         else -> DatabaseUtils.queryNumEntries( | ||||||
|  |             sql.readableDatabase, | ||||||
|  |             TABLE_NAME, | ||||||
|  |             "        id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " + | ||||||
|  |                 "AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))", | ||||||
|  |             arrayOf(label.id.toString(), Label.Type.UNREAD.name) | ||||||
|  |         ).toInt() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun findConversations(label: Label?): List<UUID> { | ||||||
|  |         val projection = arrayOf(COLUMN_CONVERSATION) | ||||||
|  |  | ||||||
|  |         val where = when { | ||||||
|  |             label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label)" | ||||||
|  |             label == null -> null | ||||||
|  |             else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id})" | ||||||
|  |         } | ||||||
|  |         val result = LinkedList<UUID>() | ||||||
|  |         sql.readableDatabase.query( | ||||||
|  |             true, | ||||||
|  |             TABLE_NAME, projection, where, | ||||||
|  |             null, null, null, null, null | ||||||
|  |         ).use { c -> | ||||||
|  |             while (c.moveToNext()) { | ||||||
|  |                 val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)) | ||||||
|  |                 result.add(asUuid(uuidBytes)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private fun updateParents(db: SQLiteDatabase, message: Plaintext) { | ||||||
|  |         val inventoryVector = message.inventoryVector | ||||||
|  |         if (inventoryVector == null || message.parents.isEmpty()) { | ||||||
|  |             // There are no parents to save yet (they are saved in the extended data, that's enough for now) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         val childIV = inventoryVector.hash | ||||||
|  |         db.delete(PARENTS_TABLE_NAME, "child=?", arrayOf(hex(childIV))) | ||||||
|  |  | ||||||
|  |         // save new parents | ||||||
|  |         var order = 0 | ||||||
|  |         val values = ContentValues() | ||||||
|  |         for (parentIV in message.parents) { | ||||||
|  |             getMessage(parentIV)?.let { parent -> | ||||||
|  |                 mergeConversations(db, parent.conversationId, message.conversationId) | ||||||
|  |                 order++ | ||||||
|  |                 values.put("parent", parentIV.hash) | ||||||
|  |                 values.put("child", childIV) | ||||||
|  |                 values.put("pos", order) | ||||||
|  |                 values.put("conversation", UuidUtils.asBytes(message.conversationId)) | ||||||
|  |                 db.insertOrThrow(PARENTS_TABLE_NAME, null, values) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Replaces every occurrence of the source conversation ID with the target ID | ||||||
|  |  | ||||||
|  |      * @param db     is used to keep everything within one transaction | ||||||
|  |      * * | ||||||
|  |      * @param source ID of the conversation to be merged | ||||||
|  |      * * | ||||||
|  |      * @param target ID of the merge target | ||||||
|  |      */ | ||||||
|  |     private fun mergeConversations(db: SQLiteDatabase, source: UUID, target: UUID) { | ||||||
|  |         val values = ContentValues() | ||||||
|  |         values.put("conversation", UuidUtils.asBytes(target)) | ||||||
|  |         val where = "conversation=X'${hex(UuidUtils.asBytes(source))}'" | ||||||
|  |         db.update(TABLE_NAME, values, where, null) | ||||||
|  |         db.update(PARENTS_TABLE_NAME, values, where, null) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun find(where: String, offset: Int, limit: Int): List<Plaintext> { | ||||||
|  |         val result = LinkedList<Plaintext>() | ||||||
|  |  | ||||||
|  |         // Define a projection that specifies which columns from the database | ||||||
|  |         // you will actually use after this query. | ||||||
|  |         val projection = arrayOf(COLUMN_ID, COLUMN_IV, COLUMN_TYPE, COLUMN_SENDER, COLUMN_RECIPIENT, COLUMN_DATA, COLUMN_ACK_DATA, COLUMN_SENT, COLUMN_RECEIVED, COLUMN_STATUS, COLUMN_TTL, COLUMN_RETRIES, COLUMN_NEXT_TRY, COLUMN_CONVERSATION) | ||||||
|  |  | ||||||
|  |         val db = sql.readableDatabase | ||||||
|  |         db.query( | ||||||
|  |             TABLE_NAME, projection, | ||||||
|  |             where, null, null, null, | ||||||
|  |             "$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC", | ||||||
|  |             if (limit == 0) null else "$offset, $limit" | ||||||
|  |         ).use { c -> | ||||||
|  |             while (c.moveToNext()) { | ||||||
|  |                 result.add(getMessage(c)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun getMessage(c: Cursor): Plaintext = Plaintext.readWithoutSignature( | ||||||
|  |         Plaintext.Type.valueOf(c.getString(c.getColumnIndex(COLUMN_TYPE))), | ||||||
|  |         ByteArrayInputStream(c.getBlob(c.getColumnIndex(COLUMN_DATA))) | ||||||
|  |     ).build { | ||||||
|  |         id = c.getLong(c.getColumnIndex(COLUMN_ID)) | ||||||
|  |         inventoryVector = InventoryVector.fromHash(c.getBlob(c.getColumnIndex(COLUMN_IV))) | ||||||
|  |         c.getString(c.getColumnIndex(COLUMN_SENDER))?.let { | ||||||
|  |             from = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it) | ||||||
|  |         } | ||||||
|  |         c.getString(c.getColumnIndex(COLUMN_RECIPIENT))?.let { | ||||||
|  |             to = ctx.addressRepository.getAddress(it) ?: BitmessageAddress(it) | ||||||
|  |         } | ||||||
|  |         ackData = c.getBlob(c.getColumnIndex(COLUMN_ACK_DATA)) | ||||||
|  |         sent = c.getLong(c.getColumnIndex(COLUMN_SENT)) | ||||||
|  |         received = c.getLong(c.getColumnIndex(COLUMN_RECEIVED)) | ||||||
|  |         status = Plaintext.Status.valueOf(c.getString(c.getColumnIndex(COLUMN_STATUS))) | ||||||
|  |         ttl = c.getLong(c.getColumnIndex(COLUMN_TTL)) | ||||||
|  |         retries = c.getInt(c.getColumnIndex(COLUMN_RETRIES)) | ||||||
|  |         val nextTryColumn = c.getColumnIndex(COLUMN_NEXT_TRY) | ||||||
|  |         if (!c.isNull(nextTryColumn)) { | ||||||
|  |             nextTry = c.getLong(nextTryColumn) | ||||||
|  |         } | ||||||
|  |         conversation = asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))) | ||||||
|  |         labels = findLabels(id!!) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun findLabels(msgId: Any) = (ctx.labelRepository as AndroidLabelRepository).findLabels(msgId) | ||||||
|  |  | ||||||
|  |     override fun save(message: Plaintext) { | ||||||
|  |         saveContactIfNecessary(message.from) | ||||||
|  |         saveContactIfNecessary(message.to) | ||||||
|  |         val db = sql.writableDatabase | ||||||
|  |         db.transaction { | ||||||
|  |             // save message | ||||||
|  |             if (message.id == null) { | ||||||
|  |                 insert(db, message) | ||||||
|  |             } else { | ||||||
|  |                 update(db, message) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             updateParents(db, message) | ||||||
|  |  | ||||||
|  |             // remove existing labels | ||||||
|  |             db.delete(JOIN_TABLE_NAME, "message_id=?", arrayOf(message.id.toString())) | ||||||
|  |  | ||||||
|  |             // save labels | ||||||
|  |             val values = ContentValues() | ||||||
|  |             for (label in message.labels) { | ||||||
|  |                 values.put(JT_COLUMN_LABEL, label.id as Long?) | ||||||
|  |                 values.put(JT_COLUMN_MESSAGE, message.id as Long?) | ||||||
|  |                 db.insertOrThrow(JOIN_TABLE_NAME, null, values) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun getValues(message: Plaintext): ContentValues { | ||||||
|  |         val values = ContentValues() | ||||||
|  |         values.put(COLUMN_IV, message.inventoryVector?.hash) | ||||||
|  |         values.put(COLUMN_TYPE, message.type.name) | ||||||
|  |         values.put(COLUMN_SENDER, message.from.address) | ||||||
|  |         values.put(COLUMN_RECIPIENT, message.to?.address) | ||||||
|  |         values.put(COLUMN_DATA, Encode.bytes(message)) | ||||||
|  |         values.put(COLUMN_ACK_DATA, message.ackData) | ||||||
|  |         values.put(COLUMN_SENT, message.sent) | ||||||
|  |         values.put(COLUMN_RECEIVED, message.received) | ||||||
|  |         values.put(COLUMN_STATUS, message.status.name) | ||||||
|  |         values.put(COLUMN_INITIAL_HASH, message.initialHash) | ||||||
|  |         values.put(COLUMN_TTL, message.ttl) | ||||||
|  |         values.put(COLUMN_RETRIES, message.retries) | ||||||
|  |         values.put(COLUMN_NEXT_TRY, message.nextTry) | ||||||
|  |         values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.conversationId)) | ||||||
|  |         return values | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun insert(db: SQLiteDatabase, message: Plaintext) { | ||||||
|  |         val id = db.insertOrThrow(TABLE_NAME, null, getValues(message)) | ||||||
|  |         message.id = id | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun update(db: SQLiteDatabase, message: Plaintext) { | ||||||
|  |         db.update(TABLE_NAME, getValues(message), "id=?", arrayOf(message.id.toString())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun remove(message: Plaintext) { | ||||||
|  |         val db = sql.writableDatabase | ||||||
|  |         db.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private const val TABLE_NAME = "Message" | ||||||
|  |         private const val COLUMN_ID = "id" | ||||||
|  |         private const val COLUMN_IV = "iv" | ||||||
|  |         private const val COLUMN_TYPE = "type" | ||||||
|  |         private const val COLUMN_SENDER = "sender" | ||||||
|  |         private const val COLUMN_RECIPIENT = "recipient" | ||||||
|  |         private const val COLUMN_DATA = "data" | ||||||
|  |         private const val COLUMN_ACK_DATA = "ack_data" | ||||||
|  |         private const val COLUMN_SENT = "sent" | ||||||
|  |         private const val COLUMN_RECEIVED = "received" | ||||||
|  |         private const val COLUMN_STATUS = "status" | ||||||
|  |         private const val COLUMN_TTL = "ttl" | ||||||
|  |         private const val COLUMN_RETRIES = "retries" | ||||||
|  |         private const val COLUMN_NEXT_TRY = "next_try" | ||||||
|  |         private const val COLUMN_INITIAL_HASH = "initial_hash" | ||||||
|  |         private const val COLUMN_CONVERSATION = "conversation" | ||||||
|  |  | ||||||
|  |         private const val PARENTS_TABLE_NAME = "Message_Parent" | ||||||
|  |  | ||||||
|  |         private const val JOIN_TABLE_NAME = "Message_Label" | ||||||
|  |         private const val JT_COLUMN_MESSAGE = "message_id" | ||||||
|  |         private const val JT_COLUMN_LABEL = "label_id" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,194 +0,0 @@ | |||||||
| package ch.dissem.apps.abit.repository; |  | ||||||
|  |  | ||||||
| import android.content.ContentValues; |  | ||||||
| import android.database.Cursor; |  | ||||||
| import android.database.sqlite.SQLiteConstraintException; |  | ||||||
| import android.database.sqlite.SQLiteDatabase; |  | ||||||
| import android.database.sqlite.SQLiteDoneException; |  | ||||||
| import android.database.sqlite.SQLiteStatement; |  | ||||||
|  |  | ||||||
| import org.slf4j.Logger; |  | ||||||
| import org.slf4j.LoggerFactory; |  | ||||||
|  |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Map; |  | ||||||
| import java.util.Set; |  | ||||||
|  |  | ||||||
| import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; |  | ||||||
| import ch.dissem.bitmessage.exception.ApplicationException; |  | ||||||
| import ch.dissem.bitmessage.ports.NodeRegistry; |  | ||||||
| import ch.dissem.bitmessage.utils.Collections; |  | ||||||
| import ch.dissem.bitmessage.utils.SqlStrings; |  | ||||||
|  |  | ||||||
| import static ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes; |  | ||||||
| import static ch.dissem.bitmessage.utils.Strings.hex; |  | ||||||
| import static ch.dissem.bitmessage.utils.UnixTime.DAY; |  | ||||||
| import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; |  | ||||||
| import static ch.dissem.bitmessage.utils.UnixTime.now; |  | ||||||
| import static java.lang.String.valueOf; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public class AndroidNodeRegistry implements NodeRegistry { |  | ||||||
|     private static final Logger LOG = LoggerFactory.getLogger(AndroidInventory.class); |  | ||||||
|  |  | ||||||
|     private static final String TABLE_NAME = "Node"; |  | ||||||
|     private static final String COLUMN_STREAM = "stream"; |  | ||||||
|     private static final String COLUMN_ADDRESS = "address"; |  | ||||||
|     private static final String COLUMN_PORT = "port"; |  | ||||||
|     private static final String COLUMN_SERVICES = "services"; |  | ||||||
|     private static final String COLUMN_TIME = "time"; |  | ||||||
|  |  | ||||||
|     private final ThreadLocal<SQLiteStatement> loadExistingStatement = new ThreadLocal<>(); |  | ||||||
|  |  | ||||||
|     private final SqlHelper sql; |  | ||||||
|     private Map<Long, Set<NetworkAddress>> stableNodes; |  | ||||||
|  |  | ||||||
|     public AndroidNodeRegistry(SqlHelper sql) { |  | ||||||
|         this.sql = sql; |  | ||||||
|         cleanUp(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void cleanUp() { |  | ||||||
|         SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|         db.delete(TABLE_NAME, "time < ?", new String[]{valueOf(now(-28 * DAY))}); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void clear() { |  | ||||||
|         SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|         db.delete(TABLE_NAME, null, null); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private Long loadExistingTime(NetworkAddress node) { |  | ||||||
|         SQLiteStatement statement = loadExistingStatement.get(); |  | ||||||
|         if (statement == null) { |  | ||||||
|             statement = sql.getWritableDatabase().compileStatement( |  | ||||||
|                 "SELECT " + COLUMN_TIME + |  | ||||||
|                     " FROM " + TABLE_NAME + |  | ||||||
|                     " WHERE stream=? AND address=? AND port=?" |  | ||||||
|             ); |  | ||||||
|             loadExistingStatement.set(statement); |  | ||||||
|         } |  | ||||||
|         statement.bindLong(1, node.getStream()); |  | ||||||
|         statement.bindBlob(2, node.getIPv6()); |  | ||||||
|         statement.bindLong(3, node.getPort()); |  | ||||||
|         try { |  | ||||||
|             return statement.simpleQueryForLong(); |  | ||||||
|         } catch (SQLiteDoneException e) { |  | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { |  | ||||||
|         String[] projection = { |  | ||||||
|             COLUMN_STREAM, |  | ||||||
|             COLUMN_ADDRESS, |  | ||||||
|             COLUMN_PORT, |  | ||||||
|             COLUMN_SERVICES, |  | ||||||
|             COLUMN_TIME |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         List<NetworkAddress> result = new LinkedList<>(); |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         try (Cursor c = db.query( |  | ||||||
|             TABLE_NAME, projection, |  | ||||||
|             "stream IN (?)", |  | ||||||
|             new String[]{SqlStrings.join(streams).toString()}, |  | ||||||
|             null, null, |  | ||||||
|             "time DESC", |  | ||||||
|             valueOf(limit) |  | ||||||
|         )) { |  | ||||||
|             while (c.moveToNext()) { |  | ||||||
|                 result.add( |  | ||||||
|                     new NetworkAddress.Builder() |  | ||||||
|                         .stream(c.getLong(c.getColumnIndex(COLUMN_STREAM))) |  | ||||||
|                         .ipv6(c.getBlob(c.getColumnIndex(COLUMN_ADDRESS))) |  | ||||||
|                         .port(c.getInt(c.getColumnIndex(COLUMN_PORT))) |  | ||||||
|                         .services(c.getLong(c.getColumnIndex(COLUMN_SERVICES))) |  | ||||||
|                         .time(c.getLong(c.getColumnIndex(COLUMN_TIME))) |  | ||||||
|                         .build() |  | ||||||
|                 ); |  | ||||||
|             } |  | ||||||
|         } catch (Exception e) { |  | ||||||
|             LOG.error(e.getMessage(), e); |  | ||||||
|             throw new ApplicationException(e); |  | ||||||
|         } |  | ||||||
|         if (result.isEmpty()) { |  | ||||||
|             synchronized (this) { |  | ||||||
|                 if (stableNodes == null) { |  | ||||||
|                     stableNodes = loadStableNodes(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             for (long stream : streams) { |  | ||||||
|                 Set<NetworkAddress> nodes = stableNodes.get(stream); |  | ||||||
|                 if (nodes != null && !nodes.isEmpty()) { |  | ||||||
|                     result.add(Collections.selectRandom(nodes)); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void offerAddresses(List<NetworkAddress> nodes) { |  | ||||||
|         SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|         db.beginTransaction(); |  | ||||||
|         try { |  | ||||||
|             cleanUp(); |  | ||||||
|             for (NetworkAddress node : nodes) { |  | ||||||
|                 if (node.getTime() < now(+5 * MINUTE) && node.getTime() > now(-28 * DAY)) { |  | ||||||
|                     synchronized (this) { |  | ||||||
|                         Long existing = loadExistingTime(node); |  | ||||||
|                         if (existing == null) { |  | ||||||
|                             insert(node); |  | ||||||
|                         } else if (node.getTime() > existing) { |  | ||||||
|                             update(node); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             db.setTransactionSuccessful(); |  | ||||||
|         } finally { |  | ||||||
|             db.endTransaction(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void insert(NetworkAddress node) { |  | ||||||
|         try { |  | ||||||
|             SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|             // Create a new map of values, where column names are the keys |  | ||||||
|             ContentValues values = new ContentValues(); |  | ||||||
|             values.put(COLUMN_STREAM, node.getStream()); |  | ||||||
|             values.put(COLUMN_ADDRESS, node.getIPv6()); |  | ||||||
|             values.put(COLUMN_PORT, node.getPort()); |  | ||||||
|             values.put(COLUMN_SERVICES, node.getServices()); |  | ||||||
|             values.put(COLUMN_TIME, node.getTime()); |  | ||||||
|  |  | ||||||
|             db.insertOrThrow(TABLE_NAME, null, values); |  | ||||||
|         } catch (SQLiteConstraintException e) { |  | ||||||
|             LOG.trace(e.getMessage(), e); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void update(NetworkAddress node) { |  | ||||||
|         try { |  | ||||||
|             SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|             // Create a new map of values, where column names are the keys |  | ||||||
|             ContentValues values = new ContentValues(); |  | ||||||
|             values.put(COLUMN_SERVICES, node.getServices()); |  | ||||||
|             values.put(COLUMN_TIME, node.getTime()); |  | ||||||
|  |  | ||||||
|             db.update(TABLE_NAME, values, |  | ||||||
|                 "stream=" + node.getStream() + " AND address=X'" + hex(node.getIPv6()) + "' AND " + |  | ||||||
|                     "port=" + node.getPort(), |  | ||||||
|                 null); |  | ||||||
|         } catch (SQLiteConstraintException e) { |  | ||||||
|             LOG.trace(e.getMessage(), e); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,202 @@ | |||||||
|  | package ch.dissem.apps.abit.repository | ||||||
|  |  | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.database.sqlite.SQLiteConstraintException | ||||||
|  | import android.database.sqlite.SQLiteDoneException | ||||||
|  | import android.database.sqlite.SQLiteStatement | ||||||
|  | import ch.dissem.bitmessage.entity.valueobject.NetworkAddress | ||||||
|  | import ch.dissem.bitmessage.exception.ApplicationException | ||||||
|  | import ch.dissem.bitmessage.ports.NodeRegistry | ||||||
|  | import ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes | ||||||
|  | import ch.dissem.bitmessage.utils.Collections | ||||||
|  | import ch.dissem.bitmessage.utils.SqlStrings | ||||||
|  | import ch.dissem.bitmessage.utils.Strings.hex | ||||||
|  | import ch.dissem.bitmessage.utils.UnixTime | ||||||
|  | import ch.dissem.bitmessage.utils.UnixTime.DAY | ||||||
|  | import ch.dissem.bitmessage.utils.UnixTime.MINUTE | ||||||
|  | import ch.dissem.bitmessage.utils.UnixTime.now | ||||||
|  | import ch.dissem.bitmessage.utils.max | ||||||
|  | import org.jetbrains.anko.db.transaction | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  | import java.util.* | ||||||
|  | import kotlin.concurrent.getOrSet | ||||||
|  |  | ||||||
|  | const val MAX_ENTRY_AGE = 7 * DAY | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class AndroidNodeRegistry(private val sql: SqlHelper) : NodeRegistry { | ||||||
|  |  | ||||||
|  |     private val loadExistingStatement = ThreadLocal<SQLiteStatement>() | ||||||
|  |     private var stableNodes: Map<Long, Set<NetworkAddress>> = emptyMap() | ||||||
|  |         get() { | ||||||
|  |             if (field.isEmpty()) | ||||||
|  |                 field = loadStableNodes() | ||||||
|  |             return field | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         cleanUp() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun cleanUp() { | ||||||
|  |         sql.writableDatabase.delete(TABLE_NAME, "time < ?", arrayOf((now - MAX_ENTRY_AGE).toString())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun clear() { | ||||||
|  |         sql.writableDatabase.delete(TABLE_NAME, null, null) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun loadExistingTime(node: NetworkAddress): Long? { | ||||||
|  |         val statement: SQLiteStatement = loadExistingStatement.getOrSet { | ||||||
|  |             sql.writableDatabase.compileStatement( | ||||||
|  |                 "SELECT $COLUMN_TIME FROM $TABLE_NAME WHERE stream=? AND address=? AND port=?" | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         statement.bindLong(1, node.stream) | ||||||
|  |         statement.bindBlob(2, node.IPv6) | ||||||
|  |         statement.bindLong(3, node.port.toLong()) | ||||||
|  |         return try { | ||||||
|  |             statement.simpleQueryForLong() | ||||||
|  |         } catch (e: SQLiteDoneException) { | ||||||
|  |             null | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getKnownAddresses(limit: Int, vararg streams: Long): List<NetworkAddress> { | ||||||
|  |         val result = LinkedList<NetworkAddress>() | ||||||
|  |  | ||||||
|  |         sql.readableDatabase.query( | ||||||
|  |             TABLE_NAME, | ||||||
|  |             arrayOf(COLUMN_STREAM, COLUMN_ADDRESS, COLUMN_PORT, COLUMN_SERVICES, COLUMN_TIME), | ||||||
|  |             "stream IN (?)", | ||||||
|  |             arrayOf(SqlStrings.join(*streams)), null, null, | ||||||
|  |             "time DESC", | ||||||
|  |             limit.toString() | ||||||
|  |         ).use { c -> | ||||||
|  |             while (c.moveToNext()) { | ||||||
|  |                 result.add(NetworkAddress( | ||||||
|  |                     time = c.getLong(c.getColumnIndex(COLUMN_TIME)), | ||||||
|  |                     stream = c.getLong(c.getColumnIndex(COLUMN_STREAM)), | ||||||
|  |                     services = c.getLong(c.getColumnIndex(COLUMN_SERVICES)), | ||||||
|  |                     IPv6 = c.getBlob(c.getColumnIndex(COLUMN_ADDRESS)), | ||||||
|  |                     port = c.getInt(c.getColumnIndex(COLUMN_PORT)) | ||||||
|  |                 )) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (result.isEmpty()) { | ||||||
|  |             streams | ||||||
|  |                 .asSequence() | ||||||
|  |                 .mapNotNull { stableNodes[it] } | ||||||
|  |                 .filterNot { it.isEmpty() } | ||||||
|  |                 .mapTo(result) { Collections.selectRandom(it) } | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun offerAddresses(nodes: List<NetworkAddress>) { | ||||||
|  |         sql.writableDatabase.transaction { | ||||||
|  |             cleanUp() | ||||||
|  |             nodes | ||||||
|  |                 .filter { | ||||||
|  |                     // Don't accept nodes from the future, it might be a trap | ||||||
|  |                     it.time < now + 5 * MINUTE && it.time > now - MAX_ENTRY_AGE | ||||||
|  |                 } | ||||||
|  |                 .forEach { node -> | ||||||
|  |                     synchronized(this) { | ||||||
|  |                         val existing = loadExistingTime(node) | ||||||
|  |                         if (existing == null) { | ||||||
|  |                             insert(node) | ||||||
|  |                         } else if (node.time > existing) { | ||||||
|  |                             update(node) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun insert(node: NetworkAddress) { | ||||||
|  |         try { | ||||||
|  |             // Create a new map of values, where column names are the keys | ||||||
|  |             val values = ContentValues() | ||||||
|  |             values.put(COLUMN_STREAM, node.stream) | ||||||
|  |             values.put(COLUMN_ADDRESS, node.IPv6) | ||||||
|  |             values.put(COLUMN_PORT, node.port) | ||||||
|  |             values.put(COLUMN_SERVICES, node.services) | ||||||
|  |             values.put(COLUMN_TIME, | ||||||
|  |                 if (node.time > UnixTime.now) { | ||||||
|  |                     // This might be an attack, let's not use those nodes with priority | ||||||
|  |                     UnixTime.now - 7 * UnixTime.DAY | ||||||
|  |                 } else { | ||||||
|  |                     node.time | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             sql.writableDatabase.insertOrThrow(TABLE_NAME, null, values) | ||||||
|  |         } catch (e: SQLiteConstraintException) { | ||||||
|  |             LOG.trace(e.message, e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun update(node: NetworkAddress) { | ||||||
|  |         try { | ||||||
|  |             val time = if (node.time > UnixTime.now) { | ||||||
|  |                 // This might be an attack, let's not use those nodes with priority | ||||||
|  |                 UnixTime.now - 7 * UnixTime.DAY | ||||||
|  |             } else { | ||||||
|  |                 node.time | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Create a new map of values, where column names are the keys | ||||||
|  |             val values = ContentValues() | ||||||
|  |             values.put(COLUMN_SERVICES, node.services) | ||||||
|  |             values.put(COLUMN_TIME, max(node.time, time)) | ||||||
|  |  | ||||||
|  |             sql.writableDatabase.update( | ||||||
|  |                 TABLE_NAME, | ||||||
|  |                 values, | ||||||
|  |                 "stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}", | ||||||
|  |                 null | ||||||
|  |             ) | ||||||
|  |         } catch (e: SQLiteConstraintException) { | ||||||
|  |             LOG.trace(e.message, e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun remove(node: NetworkAddress) { | ||||||
|  |         try { | ||||||
|  |             sql.writableDatabase.delete( | ||||||
|  |                 TABLE_NAME, | ||||||
|  |                 "stream=${node.stream} AND address=X'${hex(node.IPv6)}' AND port=${node.port}", | ||||||
|  |                 null | ||||||
|  |             ) | ||||||
|  |         } catch (e: SQLiteConstraintException) { | ||||||
|  |             LOG.trace(e.message, e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun cleanup() { | ||||||
|  |         try { | ||||||
|  |             sql.writableDatabase.delete( | ||||||
|  |                 TABLE_NAME, | ||||||
|  |                 "time<${UnixTime.now - 8 * DAY}", | ||||||
|  |                 null | ||||||
|  |             ) | ||||||
|  |         } catch (e: SQLiteConstraintException) { | ||||||
|  |             LOG.trace(e.message, e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val LOG = LoggerFactory.getLogger(AndroidInventory::class.java) | ||||||
|  |  | ||||||
|  |         private const val TABLE_NAME = "Node" | ||||||
|  |         private const val COLUMN_STREAM = "stream" | ||||||
|  |         private const val COLUMN_ADDRESS = "address" | ||||||
|  |         private const val COLUMN_PORT = "port" | ||||||
|  |         private const val COLUMN_SERVICES = "services" | ||||||
|  |         private const val COLUMN_TIME = "time" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,172 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.repository; |  | ||||||
|  |  | ||||||
| import android.content.ContentValues; |  | ||||||
| import android.database.Cursor; |  | ||||||
| import android.database.sqlite.SQLiteConstraintException; |  | ||||||
| import android.database.sqlite.SQLiteDatabase; |  | ||||||
|  |  | ||||||
| import org.slf4j.Logger; |  | ||||||
| import org.slf4j.LoggerFactory; |  | ||||||
|  |  | ||||||
| import java.io.ByteArrayInputStream; |  | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import ch.dissem.bitmessage.InternalContext; |  | ||||||
| import ch.dissem.bitmessage.entity.ObjectMessage; |  | ||||||
| import ch.dissem.bitmessage.factory.Factory; |  | ||||||
| import ch.dissem.bitmessage.ports.ProofOfWorkRepository; |  | ||||||
| import ch.dissem.bitmessage.utils.Encode; |  | ||||||
|  |  | ||||||
| import static ch.dissem.bitmessage.utils.Singleton.cryptography; |  | ||||||
| import static ch.dissem.bitmessage.utils.Strings.hex; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
| public class AndroidProofOfWorkRepository implements ProofOfWorkRepository, InternalContext |  | ||||||
|     .ContextHolder { |  | ||||||
|     private static final Logger LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository.class); |  | ||||||
|  |  | ||||||
|     private static final String TABLE_NAME = "POW"; |  | ||||||
|     private static final String COLUMN_INITIAL_HASH = "initial_hash"; |  | ||||||
|     private static final String COLUMN_DATA = "data"; |  | ||||||
|     private static final String COLUMN_VERSION = "version"; |  | ||||||
|     private static final String COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte"; |  | ||||||
|     private static final String COLUMN_EXTRA_BYTES = "extra_bytes"; |  | ||||||
|     private static final String COLUMN_EXPIRATION_TIME = "expiration_time"; |  | ||||||
|     private static final String COLUMN_MESSAGE_ID = "message_id"; |  | ||||||
|  |  | ||||||
|     private final SqlHelper sql; |  | ||||||
|     private InternalContext bmc; |  | ||||||
|  |  | ||||||
|     public AndroidProofOfWorkRepository(SqlHelper sql) { |  | ||||||
|         this.sql = sql; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void setContext(InternalContext internalContext) { |  | ||||||
|         this.bmc = internalContext; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public Item getItem(byte[] initialHash) { |  | ||||||
|         // Define a projection that specifies which columns from the database |  | ||||||
|         // you will actually use after this query. |  | ||||||
|         String[] projection = { |  | ||||||
|             COLUMN_DATA, |  | ||||||
|             COLUMN_VERSION, |  | ||||||
|             COLUMN_NONCE_TRIALS_PER_BYTE, |  | ||||||
|             COLUMN_EXTRA_BYTES, |  | ||||||
|             COLUMN_EXPIRATION_TIME, |  | ||||||
|             COLUMN_MESSAGE_ID |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         try (Cursor c = db.query( |  | ||||||
|             TABLE_NAME, projection, |  | ||||||
|             "initial_hash=X'" + hex(initialHash) + "'", |  | ||||||
|             null, null, null, null |  | ||||||
|         )) { |  | ||||||
|             if (c.moveToFirst()) { |  | ||||||
|                 int version = c.getInt(c.getColumnIndex(COLUMN_VERSION)); |  | ||||||
|                 byte[] blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)); |  | ||||||
|                 if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) { |  | ||||||
|                     return new Item( |  | ||||||
|                         Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob |  | ||||||
|                             .length), |  | ||||||
|                         c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), |  | ||||||
|                         c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)) |  | ||||||
|                     ); |  | ||||||
|                 } else { |  | ||||||
|                     return new Item( |  | ||||||
|                         Factory.getObjectMessage(version, new ByteArrayInputStream(blob), blob |  | ||||||
|                             .length), |  | ||||||
|                         c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), |  | ||||||
|                         c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)), |  | ||||||
|                         c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)), |  | ||||||
|                         bmc.getMessageRepository().getMessage( |  | ||||||
|                             c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID))) |  | ||||||
|                     ); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         throw new RuntimeException("Object requested that we don't have. Initial hash: " + |  | ||||||
|             hex(initialHash)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public List<byte[]> getItems() { |  | ||||||
|         // Define a projection that specifies which columns from the database |  | ||||||
|         // you will actually use after this query. |  | ||||||
|         String[] projection = { |  | ||||||
|             COLUMN_INITIAL_HASH |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         SQLiteDatabase db = sql.getReadableDatabase(); |  | ||||||
|         List<byte[]> result = new LinkedList<>(); |  | ||||||
|         try (Cursor c = db.query( |  | ||||||
|             TABLE_NAME, projection, |  | ||||||
|             null, null, null, null, null |  | ||||||
|         )) { |  | ||||||
|             while (c.moveToNext()) { |  | ||||||
|                 byte[] initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH)); |  | ||||||
|                 result.add(initialHash); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return result; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void putObject(Item item) { |  | ||||||
|         try { |  | ||||||
|             SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|             // Create a new map of values, where column names are the keys |  | ||||||
|             ContentValues values = new ContentValues(); |  | ||||||
|             values.put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.object)); |  | ||||||
|             values.put(COLUMN_DATA, Encode.bytes(item.object)); |  | ||||||
|             values.put(COLUMN_VERSION, item.object.getVersion()); |  | ||||||
|             values.put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte); |  | ||||||
|             values.put(COLUMN_EXTRA_BYTES, item.extraBytes); |  | ||||||
|             if (item.message != null) { |  | ||||||
|                 values.put(COLUMN_EXPIRATION_TIME, item.expirationTime); |  | ||||||
|                 values.put(COLUMN_MESSAGE_ID, (Long) item.message.getId()); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             db.insertOrThrow(TABLE_NAME, null, values); |  | ||||||
|         } catch (SQLiteConstraintException e) { |  | ||||||
|             LOG.trace(e.getMessage(), e); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { |  | ||||||
|         putObject(new Item(object, nonceTrialsPerByte, extraBytes)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void removeObject(byte[] initialHash) { |  | ||||||
|         SQLiteDatabase db = sql.getWritableDatabase(); |  | ||||||
|         db.delete( |  | ||||||
|             TABLE_NAME, |  | ||||||
|             "initial_hash=X'" + hex(initialHash) + "'", |  | ||||||
|             null |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,140 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.repository | ||||||
|  |  | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.database.sqlite.SQLiteConstraintException | ||||||
|  |  | ||||||
|  | import org.slf4j.LoggerFactory | ||||||
|  |  | ||||||
|  | import java.io.ByteArrayInputStream | ||||||
|  | import java.util.LinkedList | ||||||
|  |  | ||||||
|  | import ch.dissem.bitmessage.InternalContext | ||||||
|  | import ch.dissem.bitmessage.entity.ObjectMessage | ||||||
|  | import ch.dissem.bitmessage.factory.Factory | ||||||
|  | import ch.dissem.bitmessage.ports.ProofOfWorkRepository | ||||||
|  | import ch.dissem.bitmessage.utils.Encode | ||||||
|  |  | ||||||
|  | import ch.dissem.bitmessage.utils.Singleton.cryptography | ||||||
|  | import ch.dissem.bitmessage.utils.Strings.hex | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  | class AndroidProofOfWorkRepository(private val sql: SqlHelper) : ProofOfWorkRepository, InternalContext.ContextHolder { | ||||||
|  |     private lateinit var bmc: InternalContext | ||||||
|  |  | ||||||
|  |     override fun setContext(context: InternalContext) { | ||||||
|  |         this.bmc = context | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getItem(initialHash: ByteArray): ProofOfWorkRepository.Item { | ||||||
|  |         // Define a projection that specifies which columns from the database | ||||||
|  |         // you will actually use after this query. | ||||||
|  |         val projection = arrayOf(COLUMN_DATA, COLUMN_VERSION, COLUMN_NONCE_TRIALS_PER_BYTE, COLUMN_EXTRA_BYTES, COLUMN_EXPIRATION_TIME, COLUMN_MESSAGE_ID) | ||||||
|  |  | ||||||
|  |         val db = sql.readableDatabase | ||||||
|  |         db.query( | ||||||
|  |                 TABLE_NAME, projection, | ||||||
|  |                 "initial_hash=X'${hex(initialHash)}'", | ||||||
|  |                 null, null, null, null | ||||||
|  |         ).use { c -> | ||||||
|  |             if (c.moveToFirst()) { | ||||||
|  |                 val version = c.getInt(c.getColumnIndex(COLUMN_VERSION)) | ||||||
|  |                 val blob = c.getBlob(c.getColumnIndex(COLUMN_DATA)) | ||||||
|  |                 return if (c.isNull(c.getColumnIndex(COLUMN_MESSAGE_ID))) { | ||||||
|  |                     ProofOfWorkRepository.Item( | ||||||
|  |                             Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"), | ||||||
|  |                             c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), | ||||||
|  |                             c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)) | ||||||
|  |                     ) | ||||||
|  |                 } else { | ||||||
|  |                     ProofOfWorkRepository.Item( | ||||||
|  |                             Factory.getObjectMessage(version, ByteArrayInputStream(blob), blob.size) ?: throw RuntimeException("Invalid object in repository"), | ||||||
|  |                             c.getLong(c.getColumnIndex(COLUMN_NONCE_TRIALS_PER_BYTE)), | ||||||
|  |                             c.getLong(c.getColumnIndex(COLUMN_EXTRA_BYTES)), | ||||||
|  |                             c.getLong(c.getColumnIndex(COLUMN_EXPIRATION_TIME)), | ||||||
|  |                             bmc.messageRepository.getMessage(c.getLong(c.getColumnIndex(COLUMN_MESSAGE_ID))) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         throw RuntimeException("Object requested that we don't have. Initial hash: ${hex(initialHash)}") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getItems(): List<ByteArray> { | ||||||
|  |         // Define a projection that specifies which columns from the database | ||||||
|  |         // you will actually use after this query. | ||||||
|  |         val projection = arrayOf(COLUMN_INITIAL_HASH) | ||||||
|  |  | ||||||
|  |         val db = sql.readableDatabase | ||||||
|  |         val result = LinkedList<ByteArray>() | ||||||
|  |         db.query( | ||||||
|  |                 TABLE_NAME, projection, null, null, null, null, null | ||||||
|  |         ).use { c -> | ||||||
|  |             while (c.moveToNext()) { | ||||||
|  |                 val initialHash = c.getBlob(c.getColumnIndex(COLUMN_INITIAL_HASH)) | ||||||
|  |                 result.add(initialHash) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return result | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun putObject(item: ProofOfWorkRepository.Item) { | ||||||
|  |         try { | ||||||
|  |             val db = sql.writableDatabase | ||||||
|  |             // Create a new map of values, where column names are the keys | ||||||
|  |             val values = ContentValues() | ||||||
|  |             values.put(COLUMN_INITIAL_HASH, cryptography().getInitialHash(item.objectMessage)) | ||||||
|  |             values.put(COLUMN_DATA, Encode.bytes(item.objectMessage)) | ||||||
|  |             values.put(COLUMN_VERSION, item.objectMessage.version) | ||||||
|  |             values.put(COLUMN_NONCE_TRIALS_PER_BYTE, item.nonceTrialsPerByte) | ||||||
|  |             values.put(COLUMN_EXTRA_BYTES, item.extraBytes) | ||||||
|  |             item.message?.let { message -> | ||||||
|  |                 values.put(COLUMN_EXPIRATION_TIME, item.expirationTime) | ||||||
|  |                 values.put(COLUMN_MESSAGE_ID, message.id as Long?) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             db.insertOrThrow(TABLE_NAME, null, values) | ||||||
|  |         } catch (e: SQLiteConstraintException) { | ||||||
|  |             LOG.trace(e.message, e) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun putObject(objectMessage: ObjectMessage, nonceTrialsPerByte: Long, extraBytes: Long) = | ||||||
|  |         putObject(ProofOfWorkRepository.Item(objectMessage, nonceTrialsPerByte, extraBytes)) | ||||||
|  |  | ||||||
|  |     override fun removeObject(initialHash: ByteArray) { | ||||||
|  |         val db = sql.writableDatabase | ||||||
|  |         db.delete(TABLE_NAME, "initial_hash=X'${hex(initialHash)}'", null) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val LOG = LoggerFactory.getLogger(AndroidProofOfWorkRepository::class.java) | ||||||
|  |  | ||||||
|  |         private val TABLE_NAME = "POW" | ||||||
|  |         private val COLUMN_INITIAL_HASH = "initial_hash" | ||||||
|  |         private val COLUMN_DATA = "data" | ||||||
|  |         private val COLUMN_VERSION = "version" | ||||||
|  |         private val COLUMN_NONCE_TRIALS_PER_BYTE = "nonce_trials_per_byte" | ||||||
|  |         private val COLUMN_EXTRA_BYTES = "extra_bytes" | ||||||
|  |         private val COLUMN_EXPIRATION_TIME = "expiration_time" | ||||||
|  |         private val COLUMN_MESSAGE_ID = "message_id" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,95 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2015 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.repository; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.database.sqlite.SQLiteDatabase; |  | ||||||
| import android.database.sqlite.SQLiteOpenHelper; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.util.Assets; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Handles database migration and provides access. |  | ||||||
|  */ |  | ||||||
| public class SqlHelper extends SQLiteOpenHelper { |  | ||||||
|     // If you change the database schema, you must increment the database version. |  | ||||||
|     private static final int DATABASE_VERSION = 7; |  | ||||||
|     private static final String DATABASE_NAME = "jabit.db"; |  | ||||||
|  |  | ||||||
|     private final Context ctx; |  | ||||||
|  |  | ||||||
|     public SqlHelper(Context ctx) { |  | ||||||
|         super(ctx, DATABASE_NAME, null, DATABASE_VERSION); |  | ||||||
|         this.ctx = ctx; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreate(SQLiteDatabase db) { |  | ||||||
|         onUpgrade(db, 0, DATABASE_VERSION); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { |  | ||||||
|         switch (oldVersion) { |  | ||||||
|             case 0: |  | ||||||
|                 executeMigration(db, "V1.0__Create_table_inventory"); |  | ||||||
|                 executeMigration(db, "V1.1__Create_table_address"); |  | ||||||
|                 executeMigration(db, "V1.2__Create_table_message"); |  | ||||||
|             case 1: |  | ||||||
|                 // executeMigration(db, "V2.0__Update_table_message"); |  | ||||||
|                 executeMigration(db, "V2.1__Create_table_POW"); |  | ||||||
|             case 2: |  | ||||||
|                 executeMigration(db, "V3.0__Update_table_address"); |  | ||||||
|             case 3: |  | ||||||
|                 executeMigration(db, "V3.1__Update_table_POW"); |  | ||||||
|                 executeMigration(db, "V3.2__Update_table_message"); |  | ||||||
|             case 4: |  | ||||||
|                 executeMigration(db, "V3.3__Create_table_node"); |  | ||||||
|             case 5: |  | ||||||
|                 executeMigration(db, "V3.4__Add_label_outbox"); |  | ||||||
|             case 6: |  | ||||||
|                 executeMigration(db, "V4.0__Create_table_message_parent"); |  | ||||||
|             default: |  | ||||||
|                 // Nothing to do. Let's assume we won't upgrade from a version that's newer than |  | ||||||
|                 // DATABASE_VERSION. |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void executeMigration(SQLiteDatabase db, String name) { |  | ||||||
|         for (String statement : Assets.readSqlStatements(ctx, "db/migration/" + name + ".sql")) { |  | ||||||
|             db.execSQL(statement); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     static StringBuilder join(long... numbers) { |  | ||||||
|         StringBuilder streamList = new StringBuilder(); |  | ||||||
|         for (int i = 0; i < numbers.length; i++) { |  | ||||||
|             if (i > 0) streamList.append(", "); |  | ||||||
|             streamList.append(numbers[i]); |  | ||||||
|         } |  | ||||||
|         return streamList; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     static StringBuilder join(Enum<?>... types) { |  | ||||||
|         StringBuilder streamList = new StringBuilder(); |  | ||||||
|         for (int i = 0; i < types.length; i++) { |  | ||||||
|             if (i > 0) streamList.append(", "); |  | ||||||
|             streamList.append('\'').append(types[i].name()).append('\''); |  | ||||||
|         } |  | ||||||
|         return streamList; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,95 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2015 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.repository | ||||||
|  |  | ||||||
|  | import android.content.ContentValues | ||||||
|  | import android.content.Context | ||||||
|  | import android.database.sqlite.SQLiteDatabase | ||||||
|  | import android.database.sqlite.SQLiteOpenHelper | ||||||
|  | import ch.dissem.apps.abit.util.Assets | ||||||
|  | import ch.dissem.apps.abit.util.UuidUtils | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Handles database migration and provides access. | ||||||
|  |  */ | ||||||
|  | class SqlHelper(private val ctx: Context) : SQLiteOpenHelper(ctx, DATABASE_NAME, null, DATABASE_VERSION) { | ||||||
|  |  | ||||||
|  |     override fun onCreate(db: SQLiteDatabase) = onUpgrade(db, 0, DATABASE_VERSION) | ||||||
|  |  | ||||||
|  |     override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = mapOf( | ||||||
|  |             0 to { | ||||||
|  |                 executeMigration(db, "V1.0__Create_table_inventory") | ||||||
|  |                 executeMigration(db, "V1.1__Create_table_address") | ||||||
|  |                 executeMigration(db, "V1.2__Create_table_message") | ||||||
|  |             }, | ||||||
|  |             1 to { | ||||||
|  |                 // executeMigration(db, "V2.0__Update_table_message"); | ||||||
|  |                 executeMigration(db, "V2.1__Create_table_POW") | ||||||
|  |             }, | ||||||
|  |             2 to { | ||||||
|  |                 executeMigration(db, "V3.0__Update_table_address") | ||||||
|  |             }, | ||||||
|  |             3 to { | ||||||
|  |                 executeMigration(db, "V3.1__Update_table_POW") | ||||||
|  |                 executeMigration(db, "V3.2__Update_table_message") | ||||||
|  |             }, | ||||||
|  |             4 to { | ||||||
|  |                 executeMigration(db, "V3.3__Create_table_node") | ||||||
|  |             }, | ||||||
|  |             5 to { | ||||||
|  |                 executeMigration(db, "V3.4__Add_label_outbox") | ||||||
|  |             }, | ||||||
|  |             6 to { | ||||||
|  |                 executeMigration(db, "V4.0__Create_table_message_parent") | ||||||
|  |             }, | ||||||
|  |             7 to { | ||||||
|  |                 setMissingConversationIds(db) | ||||||
|  |             } | ||||||
|  |     ).filterKeys { it in oldVersion until newVersion }.forEach { (_, v) -> v.invoke() } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Set UUIDs for all messages that have no conversation ID | ||||||
|  |      */ | ||||||
|  |     private fun setMissingConversationIds(db: SQLiteDatabase) = db.query( | ||||||
|  |             "Message", arrayOf("id"), | ||||||
|  |             "conversation IS NULL", null, null, null, null | ||||||
|  |     ).use { c -> | ||||||
|  |         while (c.moveToNext()) { | ||||||
|  |             val id = c.getLong(0) | ||||||
|  |             setMissingConversationId(id, db) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun setMissingConversationId(id: Long, db: SQLiteDatabase) { | ||||||
|  |         val values = ContentValues(1) | ||||||
|  |         values.put("conversation", UuidUtils.asBytes(UUID.randomUUID())) | ||||||
|  |         db.update("Message", values, "id=?", arrayOf(id.toString())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun executeMigration(db: SQLiteDatabase, name: String) { | ||||||
|  |         for (statement in Assets.readSqlStatements(ctx, "db/migration/$name.sql")) { | ||||||
|  |             db.execSQL(statement) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         // If you change the database schema, you must increment the database version. | ||||||
|  |         private val DATABASE_VERSION = 7 | ||||||
|  |         val DATABASE_NAME = "jabit.db" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,73 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.service; |  | ||||||
|  |  | ||||||
| import android.app.IntentService; |  | ||||||
| import android.content.Intent; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; |  | ||||||
| import ch.dissem.apps.abit.util.Preferences; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.entity.Plaintext; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.MainActivity.updateNodeSwitch; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @author Christian Basler |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| public class BitmessageIntentService extends IntentService { |  | ||||||
|     public static final String EXTRA_DELETE_MESSAGE = "ch.dissem.abit.DeleteMessage"; |  | ||||||
|     public static final String EXTRA_STARTUP_NODE = "ch.dissem.abit.StartFullNode"; |  | ||||||
|     public static final String EXTRA_SHUTDOWN_NODE = "ch.dissem.abit.StopFullNode"; |  | ||||||
|  |  | ||||||
|     private BitmessageContext bmc; |  | ||||||
|  |  | ||||||
|     public BitmessageIntentService() { |  | ||||||
|         super("BitmessageIntentService"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreate() { |  | ||||||
|         super.onCreate(); |  | ||||||
|         bmc = Singleton.getBitmessageContext(this); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     protected void onHandleIntent(Intent intent) { |  | ||||||
|         if (intent.hasExtra(EXTRA_DELETE_MESSAGE)) { |  | ||||||
|             Plaintext item = (Plaintext) intent.getSerializableExtra(EXTRA_DELETE_MESSAGE); |  | ||||||
|             bmc.labeler().delete(item); |  | ||||||
|             bmc.messages().save(item); |  | ||||||
|             Singleton.getMessageListener(this).resetNotification(); |  | ||||||
|         } |  | ||||||
|         if (intent.hasExtra(EXTRA_STARTUP_NODE)) { |  | ||||||
|             if (Preferences.isConnectionAllowed(this)) { |  | ||||||
|                 startService(new Intent(this, BitmessageService.class)); |  | ||||||
|                 updateNodeSwitch(); |  | ||||||
|             } else { |  | ||||||
|                 Intent dialogIntent = new Intent(this, FullNodeDialogActivity.class); |  | ||||||
|                 dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |  | ||||||
|                 startActivity(dialogIntent); |  | ||||||
|                 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         if (intent.hasExtra(EXTRA_SHUTDOWN_NODE)) { |  | ||||||
|             stopService(new Intent(this, BitmessageService.class)); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,60 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.service | ||||||
|  |  | ||||||
|  | import android.app.IntentService | ||||||
|  | import android.content.Intent | ||||||
|  | import ch.dissem.apps.abit.util.NetworkUtils | ||||||
|  | import ch.dissem.bitmessage.BitmessageContext | ||||||
|  | import ch.dissem.bitmessage.entity.Plaintext | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @author Christian Basler | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | class BitmessageIntentService : IntentService("BitmessageIntentService") { | ||||||
|  |  | ||||||
|  |     private lateinit var bmc: BitmessageContext | ||||||
|  |  | ||||||
|  |     override fun onCreate() { | ||||||
|  |         super.onCreate() | ||||||
|  |         bmc = Singleton.getBitmessageContext(this) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onHandleIntent(intent: Intent?) { | ||||||
|  |         intent?.let { | ||||||
|  |             if (it.hasExtra(EXTRA_DELETE_MESSAGE)) { | ||||||
|  |                 val item = it.getSerializableExtra(EXTRA_DELETE_MESSAGE) as Plaintext | ||||||
|  |                 bmc.labeler.delete(item) | ||||||
|  |                 bmc.messages.save(item) | ||||||
|  |                 Singleton.getMessageListener(this).resetNotification() | ||||||
|  |             } | ||||||
|  |             if (it.hasExtra(EXTRA_STARTUP_NODE)) { | ||||||
|  |                 NetworkUtils.enableNode(this) | ||||||
|  |             } | ||||||
|  |             if (it.hasExtra(EXTRA_SHUTDOWN_NODE)) { | ||||||
|  |                 NetworkUtils.disableNode(this) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         const val EXTRA_DELETE_MESSAGE = "ch.dissem.abit.DeleteMessage" | ||||||
|  |         const val EXTRA_STARTUP_NODE = "ch.dissem.abit.StartFullNode" | ||||||
|  |         const val EXTRA_SHUTDOWN_NODE = "ch.dissem.abit.StopFullNode" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,106 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.service; |  | ||||||
|  |  | ||||||
| import android.app.Service; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Handler; |  | ||||||
| import android.os.IBinder; |  | ||||||
| import android.support.annotation.Nullable; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.notification.NetworkNotification; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.utils.Property; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.notification.NetworkNotification.NETWORK_NOTIFICATION_ID; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Define a Service that returns an IBinder for the |  | ||||||
|  * sync adapter class, allowing the sync adapter framework to call |  | ||||||
|  * onPerformSync(). |  | ||||||
|  */ |  | ||||||
| public class BitmessageService extends Service { |  | ||||||
|     private static BitmessageContext bmc = null; |  | ||||||
|     private static volatile boolean running = false; |  | ||||||
|  |  | ||||||
|     private NetworkNotification notification = null; |  | ||||||
|  |  | ||||||
|     private final Handler cleanupHandler = new Handler(); |  | ||||||
|     private final Runnable cleanupTask = new Runnable() { |  | ||||||
|         @Override |  | ||||||
|         public void run() { |  | ||||||
|             bmc.cleanup(); |  | ||||||
|             if (isRunning()) { |  | ||||||
|                 cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     public static boolean isRunning() { |  | ||||||
|         return running && bmc.isRunning(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreate() { |  | ||||||
|         if (bmc == null) { |  | ||||||
|             bmc = Singleton.getBitmessageContext(this); |  | ||||||
|         } |  | ||||||
|         notification = new NetworkNotification(this); |  | ||||||
|         running = false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public int onStartCommand(Intent intent, int flags, int startId) { |  | ||||||
|         if (!isRunning()) { |  | ||||||
|             running = true; |  | ||||||
|             notification.connecting(); |  | ||||||
|             startForeground(NETWORK_NOTIFICATION_ID, notification.getNotification()); |  | ||||||
|             if (!bmc.isRunning()) { |  | ||||||
|                 bmc.startup(); |  | ||||||
|             } |  | ||||||
|             notification.show(); |  | ||||||
|             cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L); |  | ||||||
|         } |  | ||||||
|         return Service.START_STICKY; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onDestroy() { |  | ||||||
|         if (bmc.isRunning()) { |  | ||||||
|             bmc.shutdown(); |  | ||||||
|         } |  | ||||||
|         running = false; |  | ||||||
|         notification.showShutdown(); |  | ||||||
|         cleanupHandler.removeCallbacks(cleanupTask); |  | ||||||
|         bmc.cleanup(); |  | ||||||
|         stopSelf(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public IBinder onBind(Intent intent) { |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static Property getStatus() { |  | ||||||
|         if (bmc != null) { |  | ||||||
|             return bmc.status(); |  | ||||||
|         } else { |  | ||||||
|             return new Property("bitmessage context", null); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,102 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.service | ||||||
|  |  | ||||||
|  | import android.app.Service | ||||||
|  | import android.content.BroadcastReceiver | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.content.IntentFilter | ||||||
|  | import android.net.ConnectivityManager | ||||||
|  | import android.os.Handler | ||||||
|  | import ch.dissem.apps.abit.notification.NetworkNotification | ||||||
|  | import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID | ||||||
|  | import ch.dissem.bitmessage.BitmessageContext | ||||||
|  | import ch.dissem.bitmessage.utils.Property | ||||||
|  | import org.jetbrains.anko.connectivityManager | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Define a Service that returns an IBinder for the | ||||||
|  |  * sync adapter class, allowing the sync adapter framework to call | ||||||
|  |  * onPerformSync(). | ||||||
|  |  */ | ||||||
|  | class BitmessageService : Service() { | ||||||
|  |  | ||||||
|  |     private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) } | ||||||
|  |     private lateinit var notification: NetworkNotification | ||||||
|  |  | ||||||
|  |     private val connectivityReceiver: BroadcastReceiver = object: BroadcastReceiver() { | ||||||
|  |         override fun onReceive(context: Context?, intent: Intent?) { | ||||||
|  |             if (bmc.isRunning() && connectivityManager.isActiveNetworkMetered){ | ||||||
|  |                 bmc.shutdown() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private val cleanupHandler = Handler() | ||||||
|  |     private val cleanupTask: Runnable = object : Runnable { | ||||||
|  |         override fun run() { | ||||||
|  |             bmc.cleanup() | ||||||
|  |             if (isRunning) { | ||||||
|  |                 cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L) // once a day | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onCreate() { | ||||||
|  |         registerReceiver(connectivityReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)) | ||||||
|  |         notification = NetworkNotification(this) | ||||||
|  |         running = false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||||||
|  |         if (!isRunning) { | ||||||
|  |             running = true | ||||||
|  |             notification.connecting() | ||||||
|  |             startForeground(NETWORK_NOTIFICATION_ID, notification.notification) | ||||||
|  |             if (!bmc.isRunning()) { | ||||||
|  |                 bmc.startup() | ||||||
|  |             } | ||||||
|  |             notification.show() | ||||||
|  |             cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L) | ||||||
|  |         } | ||||||
|  |         return Service.START_STICKY | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onDestroy() { | ||||||
|  |         if (bmc.isRunning()) { | ||||||
|  |             bmc.shutdown() | ||||||
|  |         } | ||||||
|  |         running = false | ||||||
|  |         notification.showShutdown() | ||||||
|  |         cleanupHandler.removeCallbacks(cleanupTask) | ||||||
|  |         bmc.cleanup() | ||||||
|  |         stopSelf() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onBind(intent: Intent) = null | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         @Volatile private var running = false | ||||||
|  |  | ||||||
|  |         val isRunning: Boolean | ||||||
|  |             get() = running && Singleton.bitmessageContext?.isRunning() == true | ||||||
|  |  | ||||||
|  |         val status: Property | ||||||
|  |             get() = Singleton.bitmessageContext?.status() ?: Property("bitmessage context") | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,119 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.service; |  | ||||||
|  |  | ||||||
| import android.app.Service; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.Binder; |  | ||||||
| import android.os.IBinder; |  | ||||||
| import android.support.annotation.Nullable; |  | ||||||
|  |  | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.Queue; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.notification.ProofOfWorkNotification; |  | ||||||
| import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; |  | ||||||
| import ch.dissem.bitmessage.ports.ProofOfWorkEngine; |  | ||||||
|  |  | ||||||
| import static ch.dissem.apps.abit.notification.ProofOfWorkNotification.ONGOING_NOTIFICATION_ID; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * The Proof of Work Service makes sure POW is done in a foreground process, so it shouldn't be |  | ||||||
|  * killed by the system before the nonce is found. |  | ||||||
|  */ |  | ||||||
| public class ProofOfWorkService extends Service { |  | ||||||
|     // Object to use as a thread-safe lock |  | ||||||
|     private static final ProofOfWorkEngine engine = new MultiThreadedPOWEngine(); |  | ||||||
|     private static final Queue<PowItem> queue = new LinkedList<>(); |  | ||||||
|     private static boolean calculating; |  | ||||||
|     private ProofOfWorkNotification notification; |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void onCreate() { |  | ||||||
|         notification = new ProofOfWorkNotification(this); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Nullable |  | ||||||
|     @Override |  | ||||||
|     public IBinder onBind(Intent intent) { |  | ||||||
|         return new PowBinder(this); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static class PowBinder extends Binder { |  | ||||||
|         private final ProofOfWorkService service; |  | ||||||
|         private final ProofOfWorkNotification notification; |  | ||||||
|  |  | ||||||
|         private PowBinder(ProofOfWorkService service) { |  | ||||||
|             this.service = service; |  | ||||||
|             this.notification = service.notification; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         void process(PowItem item) { |  | ||||||
|             synchronized (queue) { |  | ||||||
|                 service.startService(new Intent(service, ProofOfWorkService.class)); |  | ||||||
|                 service.startForeground(ONGOING_NOTIFICATION_ID, |  | ||||||
|                     notification.getNotification()); |  | ||||||
|                 if (!calculating) { |  | ||||||
|                     calculating = true; |  | ||||||
|                     service.calculateNonce(item); |  | ||||||
|                 } else { |  | ||||||
|                     queue.add(item); |  | ||||||
|                     notification.update(queue.size()).show(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     static class PowItem { |  | ||||||
|         private final byte[] initialHash; |  | ||||||
|         private final byte[] targetValue; |  | ||||||
|         private final ProofOfWorkEngine.Callback callback; |  | ||||||
|  |  | ||||||
|         PowItem(byte[] initialHash, byte[] targetValue, ProofOfWorkEngine.Callback callback) { |  | ||||||
|             this.initialHash = initialHash; |  | ||||||
|             this.targetValue = targetValue; |  | ||||||
|             this.callback = callback; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void calculateNonce(final PowItem item) { |  | ||||||
|         engine.calculateNonce(item.initialHash, item.targetValue, new ProofOfWorkEngine.Callback() { |  | ||||||
|             @Override |  | ||||||
|             public void onNonceCalculated(byte[] initialHash, byte[] nonce) { |  | ||||||
|                 try { |  | ||||||
|                     item.callback.onNonceCalculated(initialHash, nonce); |  | ||||||
|                 } finally { |  | ||||||
|                     PowItem next; |  | ||||||
|                     synchronized (queue) { |  | ||||||
|                         next = queue.poll(); |  | ||||||
|                         if (next == null) { |  | ||||||
|                             calculating = false; |  | ||||||
|                             stopForeground(true); |  | ||||||
|                             stopSelf(); |  | ||||||
|                         } else { |  | ||||||
|                             notification.update(queue.size()).show(); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     if (next != null) { |  | ||||||
|                         calculateNonce(next); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,115 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.service | ||||||
|  |  | ||||||
|  | import android.app.Service | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Binder | ||||||
|  | import android.os.IBinder | ||||||
|  | import ch.dissem.apps.abit.notification.ProofOfWorkNotification | ||||||
|  | import ch.dissem.apps.abit.notification.ProofOfWorkNotification.Companion.ONGOING_NOTIFICATION_ID | ||||||
|  | import ch.dissem.apps.abit.util.PowStats | ||||||
|  | import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine | ||||||
|  | import ch.dissem.bitmessage.ports.ProofOfWorkEngine | ||||||
|  | import java.util.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * The Proof of Work Service makes sure POW is done in a foreground process, so it shouldn't be | ||||||
|  |  * killed by the system before the nonce is found. | ||||||
|  |  */ | ||||||
|  | class ProofOfWorkService : Service() { | ||||||
|  |     private lateinit var notification: ProofOfWorkNotification | ||||||
|  |  | ||||||
|  |     override fun onCreate() { | ||||||
|  |         notification = ProofOfWorkNotification(this) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onBind(intent: Intent) = PowBinder(this) | ||||||
|  |  | ||||||
|  |     class PowBinder internal constructor(private val service: ProofOfWorkService) : Binder() { | ||||||
|  |         private val notification = service.notification | ||||||
|  |  | ||||||
|  |         fun process(item: PowItem) = synchronized(queue) { | ||||||
|  |             service.startService(Intent(service, ProofOfWorkService::class.java)) | ||||||
|  |             service.startForeground(ONGOING_NOTIFICATION_ID, | ||||||
|  |                 notification.notification) | ||||||
|  |             if (!calculating) { | ||||||
|  |                 calculating = true | ||||||
|  |                 service.calculateNonce(item) | ||||||
|  |             } else { | ||||||
|  |                 queue.add(item) | ||||||
|  |                 notification.update(queue.size).show() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     data class PowItem(val initialHash: ByteArray, val targetValue: ByteArray, val callback: ProofOfWorkEngine.Callback) { | ||||||
|  |         override fun equals(other: Any?): Boolean { | ||||||
|  |             if (this === other) return true | ||||||
|  |             if (javaClass != other?.javaClass) return false | ||||||
|  |  | ||||||
|  |             other as PowItem | ||||||
|  |  | ||||||
|  |             if (!Arrays.equals(initialHash, other.initialHash)) return false | ||||||
|  |             if (!Arrays.equals(targetValue, other.targetValue)) return false | ||||||
|  |  | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override fun hashCode(): Int { | ||||||
|  |             var result = Arrays.hashCode(initialHash) | ||||||
|  |             result = 31 * result + Arrays.hashCode(targetValue) | ||||||
|  |             return result | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun calculateNonce(item: PowItem) { | ||||||
|  |         notification.start(item) | ||||||
|  |         val startTime = System.currentTimeMillis() | ||||||
|  |         engine.calculateNonce(item.initialHash, item.targetValue, object : ProofOfWorkEngine.Callback { | ||||||
|  |             override fun onNonceCalculated(initialHash: ByteArray, nonce: ByteArray) { | ||||||
|  |                 notification.finished() | ||||||
|  |                 val time = System.currentTimeMillis() - startTime | ||||||
|  |                 PowStats.addPow(this@ProofOfWorkService, time, item.targetValue) | ||||||
|  |                 try { | ||||||
|  |                     item.callback.onNonceCalculated(initialHash, nonce) | ||||||
|  |                 } finally { | ||||||
|  |                     var next: PowItem? = null | ||||||
|  |                     synchronized(queue) { | ||||||
|  |                         next = queue.poll() | ||||||
|  |                         if (next == null) { | ||||||
|  |                             calculating = false | ||||||
|  |                             stopForeground(true) | ||||||
|  |                             stopSelf() | ||||||
|  |                         } else { | ||||||
|  |                             notification.update(queue.size).show() | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     next?.let { calculateNonce(it) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         // Object to use as a thread-safe lock | ||||||
|  |         private val engine = MultiThreadedPOWEngine() | ||||||
|  |         private val queue = LinkedList<PowItem>() | ||||||
|  |         private var calculating: Boolean = false | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,78 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.service; |  | ||||||
|  |  | ||||||
| import android.content.ComponentName; |  | ||||||
| import android.content.Context; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.content.ServiceConnection; |  | ||||||
| import android.os.IBinder; |  | ||||||
|  |  | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.Queue; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.service.ProofOfWorkService.PowBinder; |  | ||||||
| import ch.dissem.apps.abit.service.ProofOfWorkService.PowItem; |  | ||||||
| import ch.dissem.bitmessage.ports.ProofOfWorkEngine; |  | ||||||
|  |  | ||||||
| import static android.content.Context.BIND_AUTO_CREATE; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Proof of Work engine that uses the Proof of Work service. |  | ||||||
|  */ |  | ||||||
| public class ServicePowEngine implements ProofOfWorkEngine { |  | ||||||
|     private final Context ctx; |  | ||||||
|  |  | ||||||
|     private static final Object lock = new Object(); |  | ||||||
|     private final Queue<PowItem> queue = new LinkedList<>(); |  | ||||||
|     private PowBinder service; |  | ||||||
|  |  | ||||||
|     public ServicePowEngine(Context ctx) { |  | ||||||
|         this.ctx = ctx; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private final ServiceConnection connection = new ServiceConnection() { |  | ||||||
|         @Override |  | ||||||
|         public void onServiceConnected(ComponentName name, IBinder service) { |  | ||||||
|             synchronized (lock) { |  | ||||||
|                 ServicePowEngine.this.service = (PowBinder) service; |  | ||||||
|                 while (!queue.isEmpty()) { |  | ||||||
|                     ServicePowEngine.this.service.process(queue.poll()); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         public void onServiceDisconnected(ComponentName name) { |  | ||||||
|             service = null; |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void calculateNonce(byte[] initialHash, byte[] targetValue, Callback callback) { |  | ||||||
|         PowItem item = new PowItem(initialHash, targetValue, callback); |  | ||||||
|         synchronized (lock) { |  | ||||||
|             if (service != null) { |  | ||||||
|                 service.process(item); |  | ||||||
|             } else { |  | ||||||
|                 queue.add(item); |  | ||||||
|                 ctx.bindService(new Intent(ctx, ProofOfWorkService.class), connection, |  | ||||||
|                         BIND_AUTO_CREATE); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -0,0 +1,66 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 Christian Basler | ||||||
|  |  * | ||||||
|  |  * 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 ch.dissem.apps.abit.service | ||||||
|  |  | ||||||
|  | import android.content.ComponentName | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import android.content.ServiceConnection | ||||||
|  | import android.os.IBinder | ||||||
|  |  | ||||||
|  | import java.util.LinkedList | ||||||
|  |  | ||||||
|  | import ch.dissem.apps.abit.service.ProofOfWorkService.PowBinder | ||||||
|  | import ch.dissem.apps.abit.service.ProofOfWorkService.PowItem | ||||||
|  | import ch.dissem.bitmessage.ports.ProofOfWorkEngine | ||||||
|  |  | ||||||
|  | import android.content.Context.BIND_AUTO_CREATE | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Proof of Work engine that uses the Proof of Work service. | ||||||
|  |  */ | ||||||
|  | class ServicePowEngine(private val ctx: Context) : ProofOfWorkEngine { | ||||||
|  |     private val queue = LinkedList<PowItem>() | ||||||
|  |     private var service: PowBinder? = null | ||||||
|  |  | ||||||
|  |     private val connection = object : ServiceConnection { | ||||||
|  |         override fun onServiceConnected(name: ComponentName, service: IBinder) = synchronized(lock) { | ||||||
|  |             this@ServicePowEngine.service = service as PowBinder | ||||||
|  |             while (!queue.isEmpty()) { | ||||||
|  |                 service.process(queue.poll()) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override fun onServiceDisconnected(name: ComponentName) { | ||||||
|  |             service = null | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) { | ||||||
|  |         val item = PowItem(initialHash, target, callback) | ||||||
|  |         synchronized(lock) { | ||||||
|  |             service?.process(item) ?: { | ||||||
|  |                 queue.add(item) | ||||||
|  |                 ctx.bindService(Intent(ctx, ProofOfWorkService::class.java), connection, BIND_AUTO_CREATE) | ||||||
|  |             }.invoke() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private val lock = Any() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,177 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright 2016 Christian Basler |  | ||||||
|  * |  | ||||||
|  * 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 ch.dissem.apps.abit.service; |  | ||||||
|  |  | ||||||
| import android.content.Context; |  | ||||||
| import android.os.AsyncTask; |  | ||||||
| import android.widget.Toast; |  | ||||||
|  |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import ch.dissem.apps.abit.MainActivity; |  | ||||||
| import ch.dissem.apps.abit.R; |  | ||||||
| import ch.dissem.apps.abit.adapter.AndroidCryptography; |  | ||||||
| import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine; |  | ||||||
| import ch.dissem.apps.abit.listener.MessageListener; |  | ||||||
| import ch.dissem.apps.abit.pow.ServerPowEngine; |  | ||||||
| import ch.dissem.apps.abit.repository.AndroidAddressRepository; |  | ||||||
| import ch.dissem.apps.abit.repository.AndroidInventory; |  | ||||||
| import ch.dissem.apps.abit.repository.AndroidMessageRepository; |  | ||||||
| import ch.dissem.apps.abit.repository.AndroidNodeRegistry; |  | ||||||
| import ch.dissem.apps.abit.repository.AndroidProofOfWorkRepository; |  | ||||||
| import ch.dissem.apps.abit.repository.SqlHelper; |  | ||||||
| import ch.dissem.apps.abit.util.Constants; |  | ||||||
| import ch.dissem.bitmessage.BitmessageContext; |  | ||||||
| import ch.dissem.bitmessage.entity.BitmessageAddress; |  | ||||||
| import ch.dissem.bitmessage.entity.payload.Pubkey; |  | ||||||
| import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; |  | ||||||
| import ch.dissem.bitmessage.ports.AddressRepository; |  | ||||||
| import ch.dissem.bitmessage.ports.MessageRepository; |  | ||||||
| import ch.dissem.bitmessage.ports.ProofOfWorkRepository; |  | ||||||
| import ch.dissem.bitmessage.utils.ConversationService; |  | ||||||
| import ch.dissem.bitmessage.utils.TTL; |  | ||||||
|  |  | ||||||
| import static ch.dissem.bitmessage.utils.UnixTime.DAY; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Provides singleton objects across the application. |  | ||||||
|  */ |  | ||||||
| public class Singleton { |  | ||||||
|     private static BitmessageContext bitmessageContext; |  | ||||||
|     private static ConversationService conversationService; |  | ||||||
|     private static MessageListener messageListener; |  | ||||||
|     private static BitmessageAddress identity; |  | ||||||
|     private static AndroidProofOfWorkRepository powRepo; |  | ||||||
|     private static boolean creatingIdentity; |  | ||||||
|  |  | ||||||
|     public static BitmessageContext getBitmessageContext(Context context) { |  | ||||||
|         if (bitmessageContext == null) { |  | ||||||
|             synchronized (Singleton.class) { |  | ||||||
|                 if (bitmessageContext == null) { |  | ||||||
|                     final Context ctx = context.getApplicationContext(); |  | ||||||
|                     SqlHelper sqlHelper = new SqlHelper(ctx); |  | ||||||
|                     powRepo = new AndroidProofOfWorkRepository(sqlHelper); |  | ||||||
|                     TTL.pubkey(2 * DAY); |  | ||||||
|                     bitmessageContext = new BitmessageContext.Builder() |  | ||||||
|                         .proofOfWorkEngine(new SwitchingProofOfWorkEngine( |  | ||||||
|                             ctx, Constants.PREFERENCE_SERVER_POW, |  | ||||||
|                             new ServerPowEngine(ctx), |  | ||||||
|                             new ServicePowEngine(ctx) |  | ||||||
|                         )) |  | ||||||
|                         .cryptography(new AndroidCryptography()) |  | ||||||
|                         .nodeRegistry(new AndroidNodeRegistry(sqlHelper)) |  | ||||||
|                         .inventory(new AndroidInventory(sqlHelper)) |  | ||||||
|                         .addressRepo(new AndroidAddressRepository(sqlHelper)) |  | ||||||
|                         .messageRepo(new AndroidMessageRepository(sqlHelper, ctx)) |  | ||||||
|                         .powRepo(powRepo) |  | ||||||
|                         .networkHandler(new NioNetworkHandler()) |  | ||||||
|                         .listener(getMessageListener(ctx)) |  | ||||||
|                         .doNotSendPubkeyOnIdentityCreation() |  | ||||||
|                         .build(); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return bitmessageContext; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static MessageListener getMessageListener(Context ctx) { |  | ||||||
|         if (messageListener == null) { |  | ||||||
|             synchronized (Singleton.class) { |  | ||||||
|                 if (messageListener == null) { |  | ||||||
|                     messageListener = new MessageListener(ctx); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return messageListener; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static MessageRepository getMessageRepository(Context ctx) { |  | ||||||
|         return getBitmessageContext(ctx).messages(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static AddressRepository getAddressRepository(Context ctx) { |  | ||||||
|         return getBitmessageContext(ctx).addresses(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static ProofOfWorkRepository getProofOfWorkRepository(Context ctx) { |  | ||||||
|         if (powRepo == null) getBitmessageContext(ctx); |  | ||||||
|         return powRepo; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static BitmessageAddress getIdentity(final Context ctx) { |  | ||||||
|         if (identity == null) { |  | ||||||
|             final BitmessageContext bmc = getBitmessageContext(ctx); |  | ||||||
|             synchronized (Singleton.class) { |  | ||||||
|                 if (identity == null) { |  | ||||||
|                     List<BitmessageAddress> identities = bmc.addresses() |  | ||||||
|                         .getIdentities(); |  | ||||||
|                     if (identities.size() > 0) { |  | ||||||
|                         identity = identities.get(0); |  | ||||||
|                     } else { |  | ||||||
|                         if (!creatingIdentity) { |  | ||||||
|                             creatingIdentity = true; |  | ||||||
|                             new AsyncTask<Void, Void, BitmessageAddress>() { |  | ||||||
|                                 @Override |  | ||||||
|                                 protected BitmessageAddress doInBackground(Void... args) { |  | ||||||
|                                     BitmessageAddress identity = bmc.createIdentity(false, |  | ||||||
|                                         Pubkey.Feature.DOES_ACK); |  | ||||||
|                                     identity.setAlias( |  | ||||||
|                                         ctx.getString(R.string.alias_default_identity) |  | ||||||
|                                     ); |  | ||||||
|                                     bmc.addresses().save(identity); |  | ||||||
|                                     return identity; |  | ||||||
|                                 } |  | ||||||
|  |  | ||||||
|                                 @Override |  | ||||||
|                                 protected void onPostExecute(BitmessageAddress identity) { |  | ||||||
|                                     Singleton.identity = identity; |  | ||||||
|                                     Toast.makeText(ctx, |  | ||||||
|                                         R.string.toast_identity_created, |  | ||||||
|                                         Toast.LENGTH_SHORT).show(); |  | ||||||
|                                     MainActivity mainActivity = MainActivity.getInstance(); |  | ||||||
|                                     if (mainActivity != null) { |  | ||||||
|                                         mainActivity.addIdentityEntry(identity); |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             }.execute(); |  | ||||||
|                         } |  | ||||||
|                         return null; |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return identity; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static void setIdentity(BitmessageAddress identity) { |  | ||||||
|         if (identity.getPrivateKey() == null) |  | ||||||
|             throw new IllegalArgumentException("Identity expected, but no private key available"); |  | ||||||
|         Singleton.identity = identity; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static ConversationService getConversationService(Context ctx) { |  | ||||||
|         if (conversationService == null) { |  | ||||||
|             final BitmessageContext bmc = getBitmessageContext(ctx); |  | ||||||
|             synchronized (Singleton.class) { |  | ||||||
|                 if (conversationService == null) { |  | ||||||
|                     conversationService = new ConversationService(bmc.messages()); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return conversationService; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user