removed closed source components
This commit is contained in:
parent
983913967c
commit
5d02ff277d
|
@ -0,0 +1 @@
|
|||
google/
|
|
@ -1,46 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
package="org.mariotaku.twidere"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
|
||||
<uses-permission android:name="com.android.vending.BILLING"/>
|
||||
|
||||
<application
|
||||
android:fullBackupContent="true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version"/>
|
||||
<meta-data
|
||||
android:name="com.google.android.maps.v2.API_KEY"
|
||||
android:value="AIzaSyCVdCIMFFxdNqHnCPrJ9yKUzoTfs8jhYGc"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.sync.DropboxAuthStarterActivity"
|
||||
android:theme="@style/Theme.Twidere.NoDisplay"/>
|
||||
<activity
|
||||
android:name=".activity.sync.GoogleDriveAuthActivity"
|
||||
android:theme="@style/Theme.Twidere.NoDisplay"/>
|
||||
<activity
|
||||
android:name="com.dropbox.core.android.AuthActivity"
|
||||
android:configChanges="orientation|keyboard"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<data android:scheme="db-lflrwypk2e5pjm6"/>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.GooglePlayInAppPurchaseActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Twidere.NoDisplay"/>
|
||||
|
||||
</application>
|
||||
</manifest>
|
Binary file not shown.
Before Width: | Height: | Size: 140 B |
|
@ -1,77 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Google Maps</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" user-scalable="no"/>
|
||||
<style type="text/css">
|
||||
html, body, #map_canvas {
|
||||
background-image: url('images/loading_tile.png');
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript"
|
||||
src="http://maps.googleapis.com/maps/api/js?sensor=true"></script>
|
||||
<script type="text/javascript">
|
||||
var map;
|
||||
function initialize() {
|
||||
var latitude = 0;
|
||||
var longitude = 0;
|
||||
if (window.android) {
|
||||
latitude = window.android.getLatitude();
|
||||
longitude = window.android.getLongitude();
|
||||
}
|
||||
|
||||
setupMap(latitude, longitude, 12);
|
||||
setCenter(latitude, longitude);
|
||||
setMark(latitude, longitude);
|
||||
}
|
||||
|
||||
function setupMap(latitude, longitude, default_zoom) {
|
||||
var options = {
|
||||
zoom: default_zoom,
|
||||
center: getLatLng(latitude, longitude),
|
||||
disableDefaultUI: true,
|
||||
mapTypeId: google.maps.MapTypeId.ROADMAP
|
||||
};
|
||||
map = new google.maps.Map(document.getElementById('map_canvas'), options);
|
||||
}
|
||||
|
||||
|
||||
function getLatLng(latitude, longitude) {
|
||||
return new google.maps.LatLng(latitude, longitude);
|
||||
}
|
||||
|
||||
function setCenter(latitude, longitude) {
|
||||
map.panTo(getLatLng(latitude, longitude));
|
||||
}
|
||||
|
||||
function center() {
|
||||
var latitude = 0;
|
||||
var longitude = 0;
|
||||
if (window.android) {
|
||||
latitude = window.android.getLatitude();
|
||||
longitude = window.android.getLongitude();
|
||||
}
|
||||
setCenter(latitude, longitude);
|
||||
}
|
||||
|
||||
function setMark(latitude, longitude) {
|
||||
var latlng = getLatLng(latitude, longitude);
|
||||
var marker = new google.maps.Marker({
|
||||
position: latlng,
|
||||
map: map
|
||||
});
|
||||
}
|
||||
|
||||
google.maps.event.addDomListener(window, 'load', initialize);
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map_canvas"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,145 +0,0 @@
|
|||
package org.mariotaku.twidere.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import com.anjlab.android.iab.v3.BillingProcessor
|
||||
import com.anjlab.android.iab.v3.Constants.*
|
||||
import com.anjlab.android.iab.v3.SkuDetails
|
||||
import com.anjlab.android.iab.v3.TransactionDetails
|
||||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.alwaysUi
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.mariotaku.twidere.Constants
|
||||
import org.mariotaku.twidere.activity.premium.AbsExtraFeaturePurchaseActivity
|
||||
import org.mariotaku.twidere.fragment.ProgressDialogFragment
|
||||
import org.mariotaku.twidere.model.premium.PurchaseResult
|
||||
import org.mariotaku.twidere.util.premium.GooglePlayExtraFeaturesService
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2016/12/25.
|
||||
*/
|
||||
|
||||
class GooglePlayInAppPurchaseActivity : AbsExtraFeaturePurchaseActivity(),
|
||||
BillingProcessor.IBillingHandler {
|
||||
|
||||
private lateinit var billingProcessor: BillingProcessor
|
||||
|
||||
private val productId: String get() = GooglePlayExtraFeaturesService.getProductId(requestingFeature)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
billingProcessor = BillingProcessor(this, Constants.GOOGLE_PLAY_LICENCING_PUBKEY, this)
|
||||
if (!isFinishing && !BillingProcessor.isIabServiceAvailable(this)) {
|
||||
handleError(BILLING_RESPONSE_RESULT_USER_CANCELED)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
billingProcessor.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (!billingProcessor.handleActivityResult(requestCode, resultCode, data)) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Payment methods
|
||||
override fun onBillingError(code: Int, error: Throwable?) {
|
||||
handleError(code)
|
||||
}
|
||||
|
||||
override fun onBillingInitialized() {
|
||||
// See https://github.com/anjlab/android-inapp-billing-v3/issues/156
|
||||
if (intent.action == ACTION_RESTORE_PURCHASE) {
|
||||
getProductDetailsAndFinish()
|
||||
} else {
|
||||
billingProcessor.purchase(this, productId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProductPurchased(productId: String?, details: TransactionDetails?) {
|
||||
getProductDetailsAndFinish()
|
||||
}
|
||||
|
||||
override fun onPurchaseHistoryRestored() {
|
||||
getProductDetailsAndFinish()
|
||||
}
|
||||
|
||||
private fun handleError(billingResponse: Int) {
|
||||
when (billingResponse) {
|
||||
BILLING_ERROR_OTHER_ERROR, BILLING_ERROR_INVALID_DEVELOPER_PAYLOAD -> {
|
||||
getProductDetailsAndFinish()
|
||||
}
|
||||
else -> {
|
||||
finishWithError(getResultCode(billingResponse))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePurchased(sku: SkuDetails, transaction: TransactionDetails) {
|
||||
val result = PurchaseResult()
|
||||
result.feature = requestingFeature
|
||||
result.price = sku.priceValue
|
||||
result.currency = sku.currency
|
||||
finishWithResult(result)
|
||||
}
|
||||
|
||||
|
||||
private fun getProductDetailsAndFinish() {
|
||||
executeAfterFragmentResumed {
|
||||
val weakThis = WeakReference(it as GooglePlayInAppPurchaseActivity)
|
||||
ProgressDialogFragment.show(it.supportFragmentManager, TAG_PURCHASE_PROCESS)
|
||||
task {
|
||||
val activity = weakThis.get() ?: throw PurchaseException(BILLING_RESPONSE_RESULT_USER_CANCELED)
|
||||
val productId = activity.productId
|
||||
val bp = activity.billingProcessor
|
||||
bp.loadOwnedPurchasesFromGoogle()
|
||||
val skuDetails = bp.getPurchaseListingDetails(productId)
|
||||
?: throw PurchaseException(BILLING_RESPONSE_RESULT_ERROR)
|
||||
val transactionDetails = bp.getPurchaseTransactionDetails(productId)
|
||||
?: throw PurchaseException(BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED)
|
||||
return@task Pair(skuDetails, transactionDetails)
|
||||
}.successUi { result ->
|
||||
weakThis.get()?.handlePurchased(result.first, result.second)
|
||||
}.failUi { error ->
|
||||
if (error is PurchaseException) {
|
||||
weakThis.get()?.handleError(error.code)
|
||||
} else {
|
||||
weakThis.get()?.handleError(BILLING_RESPONSE_RESULT_ERROR)
|
||||
}
|
||||
}.alwaysUi {
|
||||
weakThis.get()?.executeAfterFragmentResumed { fragment ->
|
||||
val fm = fragment.supportFragmentManager
|
||||
val df = fm?.findFragmentByTag(TAG_PURCHASE_PROCESS) as? DialogFragment
|
||||
df?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun getResultCode(billingResponse: Int): Int {
|
||||
val resultCode = when (billingResponse) {
|
||||
BILLING_RESPONSE_RESULT_OK -> Activity.RESULT_OK
|
||||
BILLING_RESPONSE_RESULT_USER_CANCELED -> Activity.RESULT_CANCELED
|
||||
BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE -> RESULT_SERVICE_UNAVAILABLE
|
||||
BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED -> RESULT_NOT_PURCHASED
|
||||
BILLING_RESPONSE_RESULT_ERROR -> RESULT_INTERNAL_ERROR
|
||||
else -> billingResponse
|
||||
}
|
||||
return resultCode
|
||||
}
|
||||
|
||||
class PurchaseException(val code: Int) : Exception()
|
||||
|
||||
companion object {
|
||||
private const val TAG_PURCHASE_PROCESS = "get_purchase_process"
|
||||
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package org.mariotaku.twidere.activity.sync
|
||||
|
||||
import android.os.Bundle
|
||||
import com.dropbox.core.android.Auth
|
||||
import org.mariotaku.kpreferences.set
|
||||
import org.mariotaku.twidere.Constants.DROPBOX_APP_KEY
|
||||
import org.mariotaku.twidere.activity.BaseActivity
|
||||
import org.mariotaku.twidere.constant.dataSyncProviderInfoKey
|
||||
import org.mariotaku.twidere.model.sync.DropboxSyncProviderInfo
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2016/12/7.
|
||||
*/
|
||||
class DropboxAuthStarterActivity : BaseActivity() {
|
||||
|
||||
private var shouldGetAuthResult: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Auth.startOAuth2Authentication(this, DROPBOX_APP_KEY)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (shouldGetAuthResult) {
|
||||
val oauthToken = Auth.getOAuth2Token()
|
||||
if (oauthToken != null) {
|
||||
preferences[dataSyncProviderInfoKey] = DropboxSyncProviderInfo(oauthToken)
|
||||
}
|
||||
finish()
|
||||
shouldGetAuthResult = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
shouldGetAuthResult = true
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
package org.mariotaku.twidere.activity.sync
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import com.google.android.gms.auth.api.Auth
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.api.GoogleApiClient
|
||||
import com.google.android.gms.common.api.Scope
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest
|
||||
import com.google.api.client.http.javanet.NetHttpTransport
|
||||
import com.google.api.client.json.jackson2.JacksonFactory
|
||||
import com.google.api.services.drive.DriveScopes
|
||||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.mariotaku.kpreferences.set
|
||||
import org.mariotaku.twidere.activity.BaseActivity
|
||||
import org.mariotaku.twidere.constant.dataSyncProviderInfoKey
|
||||
import org.mariotaku.twidere.model.sync.GoogleDriveSyncProviderInfo
|
||||
|
||||
|
||||
class GoogleDriveAuthActivity : BaseActivity(), GoogleApiClient.ConnectionCallbacks,
|
||||
GoogleApiClient.OnConnectionFailedListener {
|
||||
|
||||
|
||||
private lateinit var googleApiClient: GoogleApiClient
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||||
.requestScopes(Scope(DriveScopes.DRIVE_APPDATA))
|
||||
.requestServerAuthCode(GoogleDriveSyncProviderInfo.WEB_CLIENT_ID, true)
|
||||
.build()
|
||||
|
||||
googleApiClient = GoogleApiClient.Builder(this)
|
||||
.addConnectionCallbacks(this)
|
||||
.addOnConnectionFailedListener(this)
|
||||
.addApi(Auth.GOOGLE_SIGN_IN_API, gso)
|
||||
.build();
|
||||
|
||||
googleApiClient.connect();
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
googleApiClient.disconnect()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_RESOLVE_ERROR -> {
|
||||
if (!googleApiClient.isConnected && !googleApiClient.isConnecting) {
|
||||
googleApiClient.connect()
|
||||
}
|
||||
}
|
||||
REQUEST_GOOGLE_SIGN_IN -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val result = Auth.GoogleSignInApi.getSignInResultFromIntent(data)
|
||||
val authCode = result.signInAccount?.serverAuthCode ?: return
|
||||
val httpTransport = NetHttpTransport()
|
||||
val jsonFactory = JacksonFactory.getDefaultInstance()
|
||||
val tokenRequest = GoogleAuthorizationCodeTokenRequest(httpTransport, jsonFactory,
|
||||
"https://www.googleapis.com/oauth2/v4/token", GoogleDriveSyncProviderInfo.WEB_CLIENT_ID,
|
||||
GoogleDriveSyncProviderInfo.WEB_CLIENT_SECRET, authCode, "")
|
||||
task {
|
||||
tokenRequest.execute()
|
||||
}.successUi { response ->
|
||||
preferences[dataSyncProviderInfoKey] = GoogleDriveSyncProviderInfo(response.refreshToken)
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}.fail { ex ->
|
||||
ex.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnected(connectionHint: Bundle?) {
|
||||
Auth.GoogleSignInApi.signOut(googleApiClient).setResultCallback {
|
||||
// Start sign in
|
||||
val signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient)
|
||||
startActivityForResult(signInIntent, REQUEST_GOOGLE_SIGN_IN)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnectionSuspended(cause: Int) {
|
||||
}
|
||||
|
||||
override fun onConnectionFailed(connectionResult: ConnectionResult) {
|
||||
if (connectionResult.hasResolution()) {
|
||||
connectionResult.startResolutionForResult(this, REQUEST_RESOLVE_ERROR)
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val REQUEST_RESOLVE_ERROR: Int = 101
|
||||
private const val REQUEST_GOOGLE_SIGN_IN: Int = 102
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
* Twidere - Twitter client for Android
|
||||
*
|
||||
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.mariotaku.twidere.fragment
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import com.google.android.gms.maps.CameraUpdateFactory
|
||||
import com.google.android.gms.maps.GoogleMap
|
||||
import com.google.android.gms.maps.SupportMapFragment
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.android.gms.maps.model.MarkerOptions
|
||||
import org.mariotaku.twidere.Constants
|
||||
import org.mariotaku.twidere.R
|
||||
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_LATITUDE
|
||||
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_LONGITUDE
|
||||
import org.mariotaku.twidere.fragment.iface.IBaseFragment
|
||||
import org.mariotaku.twidere.fragment.iface.IMapFragment
|
||||
|
||||
class GoogleMapFragment : SupportMapFragment(), Constants, IMapFragment, IBaseFragment<GoogleMapFragment> {
|
||||
|
||||
private val actionHelper = IBaseFragment.ActionHelper(this)
|
||||
|
||||
private var activeMap: GoogleMap? = null
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
val args = arguments
|
||||
if (args == null || !args.containsKey(EXTRA_LATITUDE) || !args.containsKey(EXTRA_LONGITUDE))
|
||||
return
|
||||
val lat = args.getDouble(EXTRA_LATITUDE, 0.0)
|
||||
val lng = args.getDouble(EXTRA_LONGITUDE, 0.0)
|
||||
getMapAsync { googleMap ->
|
||||
val marker = MarkerOptions()
|
||||
marker.position(LatLng(lat, lng))
|
||||
googleMap.addMarker(marker)
|
||||
center(false)
|
||||
activeMap = googleMap
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
actionHelper.dispatchOnPause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
actionHelper.dispatchOnResumeFragments()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_google_maps_viewer, menu)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
requestFitSystemWindows()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.center -> {
|
||||
center()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
override fun center() {
|
||||
center(true)
|
||||
}
|
||||
|
||||
fun center(animate: Boolean) {
|
||||
val googleMap = activeMap ?: return
|
||||
val args = arguments ?: return
|
||||
if (!args.containsKey(EXTRA_LATITUDE) || !args.containsKey(EXTRA_LONGITUDE))
|
||||
return
|
||||
val lat = args.getDouble(EXTRA_LATITUDE, 0.0)
|
||||
val lng = args.getDouble(EXTRA_LONGITUDE, 0.0)
|
||||
val c = CameraUpdateFactory.newLatLngZoom(LatLng(lat, lng), 12f)
|
||||
if (animate) {
|
||||
googleMap.animateCamera(c)
|
||||
} else {
|
||||
googleMap.moveCamera(c)
|
||||
}
|
||||
}
|
||||
|
||||
override fun executeAfterFragmentResumed(useHandler: Boolean, action: (GoogleMapFragment) -> Unit) {
|
||||
actionHelper.executeAfterFragmentResumed(useHandler, action)
|
||||
}
|
||||
|
||||
override fun fitSystemWindows(insets: Rect) {
|
||||
val view = view
|
||||
view?.setPadding(insets.left, insets.top, insets.right, insets.bottom)
|
||||
}
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
/*
|
||||
* Twidere - Twitter client for Android
|
||||
*
|
||||
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.mariotaku.twidere.fragment
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import org.mariotaku.twidere.Constants.EXTRA_LATITUDE
|
||||
import org.mariotaku.twidere.Constants.EXTRA_LONGITUDE
|
||||
import org.mariotaku.twidere.R
|
||||
import org.mariotaku.twidere.fragment.iface.IMapFragment
|
||||
import org.mariotaku.twidere.util.webkit.DefaultWebViewClient
|
||||
|
||||
class WebMapFragment : BaseWebViewFragment(), IMapFragment {
|
||||
|
||||
private var latitude: Double = 0.toDouble()
|
||||
private var longitude: Double = 0.toDouble()
|
||||
|
||||
override fun center() {
|
||||
webView?.loadUrl("javascript:center();")
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
when (item!!.itemId) {
|
||||
R.id.center -> {
|
||||
center()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
|
||||
inflater!!.inflate(R.menu.menu_google_maps_viewer, menu)
|
||||
}
|
||||
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
getLocation()
|
||||
setupWebView()
|
||||
}
|
||||
|
||||
/**
|
||||
* The Location Manager manages location providers. This code searches for
|
||||
* the best provider of data (GPS, WiFi/cell phone tower lookup, some other
|
||||
* mechanism) and finds the last known location.
|
||||
*/
|
||||
private fun getLocation() {
|
||||
val bundle = arguments
|
||||
if (bundle != null) {
|
||||
latitude = bundle.getDouble(EXTRA_LATITUDE, 0.0)
|
||||
longitude = bundle.getDouble(EXTRA_LONGITUDE, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the WebView object and loads the URL of the page *
|
||||
*/
|
||||
private fun setupWebView() {
|
||||
|
||||
val webView = webView!!
|
||||
webView.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
|
||||
webView.setWebViewClient(MapWebViewClient(activity))
|
||||
webView.loadUrl(MAPVIEW_URI)
|
||||
|
||||
val settings = webView.settings
|
||||
settings.builtInZoomControls = false
|
||||
|
||||
/** Allows JavaScript calls to access application resources */
|
||||
webView.addJavascriptInterface(MapJavaScriptInterface(this), "android")
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the interface for getting access to Latitude and Longitude data
|
||||
* from device
|
||||
*/
|
||||
internal class MapJavaScriptInterface(val fragment: WebMapFragment) {
|
||||
|
||||
@JavascriptInterface
|
||||
fun getLatitude(): Double {
|
||||
return fragment.latitude
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun getLongitude(): Double {
|
||||
return fragment.longitude
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal inner class MapWebViewClient(activity: Activity) : DefaultWebViewClient(activity) {
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
val uri = Uri.parse(url)
|
||||
if (uri.scheme == Uri.parse(MAPVIEW_URI).scheme) return false
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(intent)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val MAPVIEW_URI = "file:///android_asset/mapview.html"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package org.mariotaku.twidere.model.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import org.mariotaku.twidere.util.sync.SyncTaskRunner
|
||||
import org.mariotaku.twidere.util.sync.dropbox.DropboxSyncTaskRunner
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2017/1/2.
|
||||
*/
|
||||
|
||||
class DropboxSyncProviderInfo(val authToken: String) : SyncProviderInfo(DropboxSyncProviderInfo.TYPE) {
|
||||
override fun writeToPreferences(editor: SharedPreferences.Editor) {
|
||||
editor.putString(KEY_DROPBOX_AUTH_TOKEN, authToken)
|
||||
}
|
||||
|
||||
override fun newSyncTaskRunner(context: Context): SyncTaskRunner {
|
||||
return DropboxSyncTaskRunner(context, authToken)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE = "dropbox"
|
||||
|
||||
private const val KEY_DROPBOX_AUTH_TOKEN = "dropbox_auth_token"
|
||||
fun newInstance(preferences: SharedPreferences): DropboxSyncProviderInfo? {
|
||||
val authToken = preferences.getString(KEY_DROPBOX_AUTH_TOKEN, null) ?: return null
|
||||
return DropboxSyncProviderInfo(authToken)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package org.mariotaku.twidere.model.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import org.mariotaku.twidere.util.sync.SyncTaskRunner
|
||||
import org.mariotaku.twidere.util.sync.google.GoogleDriveSyncTaskRunner
|
||||
|
||||
class GoogleDriveSyncProviderInfo(val refreshToken: String) : SyncProviderInfo(GoogleDriveSyncProviderInfo.TYPE) {
|
||||
override fun writeToPreferences(editor: SharedPreferences.Editor) {
|
||||
editor.putString(KEY_GOOGLE_DRIVE_REFRESH_TOKEN, refreshToken)
|
||||
}
|
||||
|
||||
override fun newSyncTaskRunner(context: Context): SyncTaskRunner {
|
||||
return GoogleDriveSyncTaskRunner(context, refreshToken)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE = "google_drive"
|
||||
private const val KEY_GOOGLE_DRIVE_REFRESH_TOKEN = "google_drive_refresh_token"
|
||||
|
||||
const val WEB_CLIENT_ID = "223623398518-0sc2i5fsqliidcdoogn53iqltpktfnff.apps.googleusercontent.com"
|
||||
const val WEB_CLIENT_SECRET = "BsZ0a06UgJf5hJOTI3fcxI2u"
|
||||
|
||||
fun newInstance(preferences: SharedPreferences): GoogleDriveSyncProviderInfo? {
|
||||
val accessToken = preferences.getString(KEY_GOOGLE_DRIVE_REFRESH_TOKEN, null) ?: return null
|
||||
return GoogleDriveSyncProviderInfo(accessToken)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
/*
|
||||
* Twidere - Twitter client for Android
|
||||
*
|
||||
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.mariotaku.twidere.util
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.accounts.OnAccountsUpdateListener
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import com.crashlytics.android.answers.*
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import org.mariotaku.ktextension.addOnAccountsUpdatedListenerSafe
|
||||
import org.mariotaku.ktextension.configure
|
||||
import org.mariotaku.twidere.BuildConfig
|
||||
import org.mariotaku.twidere.Constants
|
||||
import org.mariotaku.twidere.TwidereConstants.ACCOUNT_TYPE
|
||||
import org.mariotaku.twidere.model.analyzer.*
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 15/7/8.
|
||||
*/
|
||||
class FabricAnalyzer : Analyzer(), Constants {
|
||||
|
||||
override fun log(priority: Int, tag: String, msg: String) {
|
||||
Crashlytics.log(priority, tag, msg)
|
||||
}
|
||||
|
||||
override fun logException(throwable: Throwable) {
|
||||
Crashlytics.logException(throwable)
|
||||
}
|
||||
|
||||
override fun log(event: Event) {
|
||||
val answers = Answers.getInstance()
|
||||
when (event) {
|
||||
is SignIn -> {
|
||||
answers.logLogin(configure(LoginEvent()) {
|
||||
putMethod(event.accountType)
|
||||
putSuccess(event.success)
|
||||
if (event.errorReason != null) {
|
||||
putCustomAttribute("Error reason", event.errorReason)
|
||||
}
|
||||
if (event.accountHost != null) {
|
||||
putCustomAttribute("Account host", event.accountHost)
|
||||
}
|
||||
if (event.credentialsType != null) {
|
||||
putCustomAttribute("Credentials type", event.credentialsType)
|
||||
}
|
||||
putCustomAttribute("Official key", event.officialKey.toString())
|
||||
})
|
||||
}
|
||||
is Search -> {
|
||||
answers.logSearch(configure(SearchEvent()) {
|
||||
putQuery(event.query)
|
||||
putAttributes(event)
|
||||
})
|
||||
}
|
||||
is Share -> {
|
||||
answers.logShare(configure(ShareEvent()) {
|
||||
putContentType(event.type)
|
||||
putContentId(event.id)
|
||||
putAttributes(event)
|
||||
})
|
||||
}
|
||||
is PurchaseConfirm -> {
|
||||
answers.logStartCheckout(configure(StartCheckoutEvent()) {
|
||||
event.forEachValues { name, value ->
|
||||
putCustomAttribute(name, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
is PurchaseIntroduction -> {
|
||||
answers.logAddToCart(configure(AddToCartEvent()) {
|
||||
event.forEachValues { name, value ->
|
||||
putCustomAttribute(name, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
is PurchaseFinished -> {
|
||||
answers.logPurchase(configure(PurchaseEvent()) {
|
||||
putItemName(event.productName)
|
||||
if (!event.price.isNaN() && event.currency != null) {
|
||||
putCurrency(Currency.getInstance(event.currency) ?: Currency.getInstance(Locale.getDefault()))
|
||||
putItemPrice(BigDecimal(event.price))
|
||||
}
|
||||
event.forEachValues { name, value ->
|
||||
putCustomAttribute(name, value)
|
||||
}
|
||||
putAttributes(event)
|
||||
})
|
||||
}
|
||||
else -> {
|
||||
answers.logCustom(configure(CustomEvent(event.name)) {
|
||||
putAttributes(event)
|
||||
event.forEachValues { name, value ->
|
||||
putCustomAttribute(name, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun init(application: Application) {
|
||||
Fabric.with(application, Crashlytics())
|
||||
Crashlytics.setBool("debug", BuildConfig.DEBUG)
|
||||
Crashlytics.setString("build.brand", Build.BRAND)
|
||||
Crashlytics.setString("build.device", Build.DEVICE)
|
||||
Crashlytics.setString("build.display", Build.DISPLAY)
|
||||
Crashlytics.setString("build.hardware", Build.HARDWARE)
|
||||
Crashlytics.setString("build.manufacturer", Build.MANUFACTURER)
|
||||
Crashlytics.setString("build.model", Build.MODEL)
|
||||
Crashlytics.setString("build.product", Build.PRODUCT)
|
||||
val am = AccountManager.get(application)
|
||||
try {
|
||||
am.addOnAccountsUpdatedListenerSafe(OnAccountsUpdateListener { accounts ->
|
||||
Crashlytics.setString("twidere.accounts", accounts.filter { it.type == ACCOUNT_TYPE }
|
||||
.joinToString(transform = Account::name))
|
||||
}, updateImmediately = true)
|
||||
} catch (e: SecurityException) {
|
||||
// Permission managers (like some Xposed plugins) may block Twidere from getting accounts
|
||||
}
|
||||
}
|
||||
|
||||
private fun AnswersEvent<*>.putAttributes(event: Analyzer.Event) {
|
||||
if (event.accountType != null) {
|
||||
putCustomAttribute("Account type", event.accountType)
|
||||
}
|
||||
if (event.accountHost != null) {
|
||||
putCustomAttribute("Account host", event.accountHost)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Twidere - Twitter client for Android
|
||||
*
|
||||
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.mariotaku.twidere.util
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v4.app.Fragment
|
||||
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
|
||||
import org.mariotaku.twidere.fragment.GoogleMapFragment
|
||||
import org.mariotaku.twidere.fragment.WebMapFragment
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 15/4/27.
|
||||
*/
|
||||
class GoogleMapFragmentFactory : MapFragmentFactory() {
|
||||
|
||||
override fun createMapFragment(context: Context): Fragment {
|
||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) {
|
||||
return GoogleMapFragment()
|
||||
}
|
||||
return WebMapFragment()
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package org.mariotaku.twidere.util.premium
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.anjlab.android.iab.v3.BillingProcessor
|
||||
import nl.komponents.kovenant.task
|
||||
import org.mariotaku.twidere.Constants.GOOGLE_PLAY_LICENCING_PUBKEY
|
||||
import org.mariotaku.twidere.activity.GooglePlayInAppPurchaseActivity
|
||||
import org.mariotaku.twidere.activity.premium.AbsExtraFeaturePurchaseActivity
|
||||
import org.mariotaku.twidere.view.controller.premium.GoogleFiltersImportViewController
|
||||
import org.mariotaku.twidere.view.controller.premium.GoogleFiltersSubscriptionsViewController
|
||||
import org.mariotaku.twidere.view.controller.premium.SyncStatusViewController
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2016/12/25.
|
||||
*/
|
||||
|
||||
class GooglePlayExtraFeaturesService : ExtraFeaturesService() {
|
||||
|
||||
private lateinit var bp: BillingProcessor
|
||||
|
||||
override fun getDashboardControllers() = listOf(
|
||||
SyncStatusViewController::class.java,
|
||||
GoogleFiltersImportViewController::class.java,
|
||||
GoogleFiltersSubscriptionsViewController::class.java
|
||||
)
|
||||
|
||||
override fun init(context: Context) {
|
||||
super.init(context)
|
||||
bp = BillingProcessor(context, GOOGLE_PLAY_LICENCING_PUBKEY, null)
|
||||
}
|
||||
|
||||
override fun appStarted() {
|
||||
task {
|
||||
bp.loadOwnedPurchasesFromGoogle()
|
||||
}
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
bp.release()
|
||||
}
|
||||
|
||||
override fun isSupported(): Boolean = BillingProcessor.isIabServiceAvailable(context)
|
||||
|
||||
override fun isEnabled(feature: String): Boolean {
|
||||
if (bp.hasValidTransaction(PRODUCT_ID_EXTRA_FEATURES_PACK)) return true
|
||||
val productId = getProductId(feature)
|
||||
return bp.hasValidTransaction(productId)
|
||||
}
|
||||
|
||||
override fun destroyPurchase(): Boolean {
|
||||
bp.consumePurchase(PRODUCT_ID_EXTRA_FEATURES_PACK)
|
||||
bp.consumePurchase(PRODUCT_ID_DATA_SYNC)
|
||||
bp.consumePurchase(PRODUCT_ID_FILTERS_IMPORT)
|
||||
bp.consumePurchase(PRODUCT_ID_FILTERS_SUBSCRIPTION)
|
||||
bp.consumePurchase(PRODUCT_ID_SCHEDULE_STATUS)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun createPurchaseIntent(context: Context, feature: String): Intent? {
|
||||
return AbsExtraFeaturePurchaseActivity.purchaseIntent(context,
|
||||
GooglePlayInAppPurchaseActivity::class.java, feature)
|
||||
}
|
||||
|
||||
override fun createRestorePurchaseIntent(context: Context, feature: String): Intent? {
|
||||
return AbsExtraFeaturePurchaseActivity.restorePurchaseIntent(context,
|
||||
GooglePlayInAppPurchaseActivity::class.java, feature)
|
||||
}
|
||||
|
||||
private fun BillingProcessor.hasValidTransaction(productId: String): Boolean {
|
||||
val details = getPurchaseTransactionDetails(productId) ?: return false
|
||||
return isValidTransactionDetails(details)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PRODUCT_ID_EXTRA_FEATURES_PACK = "twidere.extra.features"
|
||||
private const val PRODUCT_ID_DATA_SYNC = "twidere.extra.feature.data_sync"
|
||||
private const val PRODUCT_ID_FILTERS_IMPORT = "twidere.extra.feature.filter_import"
|
||||
private const val PRODUCT_ID_FILTERS_SUBSCRIPTION = "twidere.extra.feature.filter_subscription"
|
||||
private const val PRODUCT_ID_SCHEDULE_STATUS = "twidere.extra.feature.schedule_status"
|
||||
|
||||
@JvmStatic
|
||||
fun getProductId(feature: String): String {
|
||||
return when (feature) {
|
||||
FEATURE_FEATURES_PACK -> PRODUCT_ID_EXTRA_FEATURES_PACK
|
||||
FEATURE_SYNC_DATA -> PRODUCT_ID_DATA_SYNC
|
||||
FEATURE_FILTERS_IMPORT -> PRODUCT_ID_FILTERS_IMPORT
|
||||
FEATURE_FILTERS_SUBSCRIPTION -> PRODUCT_ID_FILTERS_SUBSCRIPTION
|
||||
FEATURE_SCHEDULE_STATUS -> PRODUCT_ID_SCHEDULE_STATUS
|
||||
else -> throw UnsupportedOperationException(feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import org.mariotaku.twidere.R
|
||||
import org.mariotaku.twidere.activity.sync.DropboxAuthStarterActivity
|
||||
import org.mariotaku.twidere.activity.sync.GoogleDriveAuthActivity
|
||||
import org.mariotaku.twidere.model.sync.DropboxSyncProviderInfo
|
||||
import org.mariotaku.twidere.model.sync.GoogleDriveSyncProviderInfo
|
||||
import org.mariotaku.twidere.model.sync.SyncProviderEntry
|
||||
import org.mariotaku.twidere.model.sync.SyncProviderInfo
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2017/1/2.
|
||||
*/
|
||||
class NonFreeSyncProviderInfoFactory : SyncProviderInfoFactory() {
|
||||
override fun getInfoForType(type: String, preferences: SharedPreferences): SyncProviderInfo? {
|
||||
return when (type) {
|
||||
DropboxSyncProviderInfo.TYPE -> DropboxSyncProviderInfo.newInstance(preferences)
|
||||
GoogleDriveSyncProviderInfo.TYPE -> GoogleDriveSyncProviderInfo.newInstance(preferences)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSupportedProviders(context: Context): List<SyncProviderEntry> {
|
||||
val list = ArrayList<SyncProviderEntry>()
|
||||
list.add(SyncProviderEntry(DropboxSyncProviderInfo.TYPE,
|
||||
context.getString(R.string.sync_provider_name_dropbox),
|
||||
Intent(context, DropboxAuthStarterActivity::class.java)))
|
||||
list.add(SyncProviderEntry(GoogleDriveSyncProviderInfo.TYPE,
|
||||
context.getString(R.string.sync_provider_name_google_drive),
|
||||
Intent(context, GoogleDriveAuthActivity::class.java)))
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.dropbox
|
||||
|
||||
import android.content.Context
|
||||
import com.dropbox.core.DbxException
|
||||
import com.dropbox.core.v2.DbxClientV2
|
||||
import com.dropbox.core.v2.files.DeleteArg
|
||||
import com.dropbox.core.v2.files.FileMetadata
|
||||
import com.dropbox.core.v2.files.ListFolderErrorException
|
||||
import com.dropbox.core.v2.files.ListFolderResult
|
||||
import org.mariotaku.twidere.extension.model.filename
|
||||
import org.mariotaku.twidere.extension.model.readMimeMessageFrom
|
||||
import org.mariotaku.twidere.extension.model.writeMimeMessageTo
|
||||
import org.mariotaku.twidere.model.Draft
|
||||
import org.mariotaku.twidere.util.sync.FileBasedDraftsSyncAction
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
internal class DropboxDraftsSyncAction(
|
||||
context: Context,
|
||||
val client: DbxClientV2
|
||||
) : FileBasedDraftsSyncAction<FileMetadata>(context) {
|
||||
@Throws(IOException::class)
|
||||
override fun Draft.saveToRemote(): FileMetadata {
|
||||
try {
|
||||
client.newUploader("/Drafts/$filename", this.timestamp).use {
|
||||
this.writeMimeMessageTo(context, it.outputStream)
|
||||
return it.finish()
|
||||
}
|
||||
} catch (e: DbxException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun Draft.loadFromRemote(info: FileMetadata): Boolean {
|
||||
try {
|
||||
client.files().download(info.pathLower).use {
|
||||
val parsed = this.readMimeMessageFrom(context, it.inputStream)
|
||||
if (parsed) {
|
||||
this.timestamp = info.draftTimestamp
|
||||
this.unique_id = info.draftFileName.substringBeforeLast(".eml")
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
} catch (e: DbxException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun removeDrafts(list: List<FileMetadata>): Boolean {
|
||||
try {
|
||||
return client.files().deleteBatch(list.map { DeleteArg(it.pathLower) }) != null
|
||||
} catch (e: DbxException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun removeDraft(info: FileMetadata): Boolean {
|
||||
try {
|
||||
return client.files().delete(info.pathLower) != null
|
||||
} catch (e: DbxException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override val FileMetadata.draftTimestamp: Long get() = this.clientModified.time
|
||||
|
||||
override val FileMetadata.draftFileName: String get() = this.name
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun listRemoteDrafts(): List<FileMetadata> {
|
||||
val result = ArrayList<FileMetadata>()
|
||||
try {
|
||||
var listResult: ListFolderResult = client.files().listFolder("/Drafts/")
|
||||
while (true) {
|
||||
// Do something with files
|
||||
listResult.entries.mapNotNullTo(result) { it as? FileMetadata }
|
||||
if (!listResult.hasMore) break
|
||||
listResult = client.files().listFolderContinue(listResult.cursor)
|
||||
}
|
||||
} catch (e: DbxException) {
|
||||
if (e is ListFolderErrorException) {
|
||||
if (e.errorValue?.pathValue?.isNotFound ?: false) {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
throw IOException(e)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.dropbox
|
||||
|
||||
import android.content.Context
|
||||
import com.dropbox.core.DbxDownloader
|
||||
import com.dropbox.core.v2.DbxClientV2
|
||||
import com.dropbox.core.v2.files.FileMetadata
|
||||
import com.dropbox.core.v2.files.UploadUploader
|
||||
import org.mariotaku.twidere.extension.model.initFields
|
||||
import org.mariotaku.twidere.extension.model.parse
|
||||
import org.mariotaku.twidere.extension.model.serialize
|
||||
import org.mariotaku.twidere.extension.newPullParser
|
||||
import org.mariotaku.twidere.extension.newSerializer
|
||||
import org.mariotaku.twidere.model.FiltersData
|
||||
import org.mariotaku.twidere.util.sync.FileBasedFiltersDataSyncAction
|
||||
|
||||
internal class DropboxFiltersDataSyncAction(
|
||||
context: Context,
|
||||
val client: DbxClientV2
|
||||
) : FileBasedFiltersDataSyncAction<DbxDownloader<FileMetadata>, DropboxUploadSession<FiltersData>>(context) {
|
||||
override fun DbxDownloader<FileMetadata>.getRemoteLastModified(): Long {
|
||||
return result.clientModified.time
|
||||
}
|
||||
|
||||
private val filePath = "/Common/filters.xml"
|
||||
|
||||
override fun newLoadFromRemoteSession(): DbxDownloader<FileMetadata> {
|
||||
return client.newDownloader(filePath)
|
||||
}
|
||||
|
||||
override fun DbxDownloader<FileMetadata>.loadFromRemote(): FiltersData {
|
||||
val data = FiltersData()
|
||||
data.parse(inputStream.newPullParser(charset = Charsets.UTF_8))
|
||||
data.initFields()
|
||||
return data
|
||||
}
|
||||
|
||||
override fun DropboxUploadSession<FiltersData>.setRemoteLastModified(lastModified: Long) {
|
||||
this.localModifiedTime = lastModified
|
||||
}
|
||||
|
||||
override fun DropboxUploadSession<FiltersData>.saveToRemote(data: FiltersData): Boolean {
|
||||
return this.uploadData(data)
|
||||
}
|
||||
|
||||
override fun newSaveToRemoteSession(): DropboxUploadSession<FiltersData> {
|
||||
return object : DropboxUploadSession<FiltersData>(filePath, client) {
|
||||
override fun performUpload(uploader: UploadUploader, data: FiltersData) {
|
||||
data.serialize(uploader.outputStream.newSerializer(charset = Charsets.UTF_8, indent = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.dropbox
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.dropbox.core.DbxDownloader
|
||||
import com.dropbox.core.v2.DbxClientV2
|
||||
import com.dropbox.core.v2.files.FileMetadata
|
||||
import com.dropbox.core.v2.files.UploadUploader
|
||||
import org.mariotaku.twidere.extension.newPullParser
|
||||
import org.mariotaku.twidere.extension.newSerializer
|
||||
import org.mariotaku.twidere.util.sync.FileBasedPreferencesValuesSyncAction
|
||||
import java.util.*
|
||||
|
||||
internal class DropboxPreferencesValuesSyncAction(
|
||||
context: Context,
|
||||
val client: DbxClientV2,
|
||||
preferences: SharedPreferences,
|
||||
processor: Processor,
|
||||
val filePath: String
|
||||
) : FileBasedPreferencesValuesSyncAction<DbxDownloader<FileMetadata>,
|
||||
DropboxUploadSession<Map<String, String>>>(context, preferences, processor) {
|
||||
override fun DbxDownloader<FileMetadata>.getRemoteLastModified(): Long {
|
||||
return result.clientModified.time
|
||||
}
|
||||
|
||||
override fun DbxDownloader<FileMetadata>.loadFromRemote(): MutableMap<String, String> {
|
||||
val data = HashMap<String, String>()
|
||||
data.parse(inputStream.newPullParser())
|
||||
return data
|
||||
}
|
||||
|
||||
override fun newLoadFromRemoteSession(): DbxDownloader<FileMetadata> {
|
||||
return client.newDownloader(filePath)
|
||||
}
|
||||
|
||||
override fun newSaveToRemoteSession(): DropboxUploadSession<Map<String, String>> {
|
||||
return object : DropboxUploadSession<Map<String, String>>(filePath, client) {
|
||||
override fun performUpload(uploader: UploadUploader, data: Map<String, String>) {
|
||||
data.serialize(uploader.outputStream.newSerializer(charset = Charsets.UTF_8,
|
||||
indent = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun DropboxUploadSession<Map<String, String>>.saveToRemote(data: MutableMap<String, String>): Boolean {
|
||||
return this.uploadData(data)
|
||||
}
|
||||
|
||||
override fun DropboxUploadSession<Map<String, String>>.setRemoteLastModified(lastModified: Long) {
|
||||
this.localModifiedTime = lastModified
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.dropbox
|
||||
|
||||
import com.dropbox.core.DbxDownloader
|
||||
import com.dropbox.core.DbxException
|
||||
import com.dropbox.core.v2.DbxClientV2
|
||||
import com.dropbox.core.v2.files.DownloadErrorException
|
||||
import com.dropbox.core.v2.files.FileMetadata
|
||||
import com.dropbox.core.v2.files.UploadUploader
|
||||
import com.dropbox.core.v2.files.WriteMode
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2017/1/10.
|
||||
*/
|
||||
|
||||
@Throws(IOException::class)
|
||||
internal fun DbxClientV2.newUploader(path: String, clientModified: Long): UploadUploader {
|
||||
try {
|
||||
return files().uploadBuilder(path).withMode(WriteMode.OVERWRITE).withMute(true)
|
||||
.withClientModified(Date(clientModified)).start()
|
||||
} catch (e: DbxException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
internal fun DbxClientV2.newDownloader(path: String): DbxDownloader<FileMetadata> {
|
||||
try {
|
||||
return files().downloadBuilder(path).start()
|
||||
} catch (e: DownloadErrorException) {
|
||||
if (e.errorValue?.pathValue?.isNotFound ?: false) {
|
||||
throw FileNotFoundException(path)
|
||||
}
|
||||
throw IOException(e)
|
||||
} catch (e: DbxException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.dropbox;
|
||||
|
||||
import android.content.Context
|
||||
import com.dropbox.core.DbxRequestConfig
|
||||
import com.dropbox.core.v2.DbxClientV2
|
||||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.mariotaku.twidere.BuildConfig
|
||||
import org.mariotaku.twidere.util.TaskServiceRunner
|
||||
import org.mariotaku.twidere.util.sync.ISyncAction
|
||||
import org.mariotaku.twidere.util.sync.SyncTaskRunner
|
||||
import org.mariotaku.twidere.util.sync.UserColorsSyncProcessor
|
||||
import org.mariotaku.twidere.util.sync.UserNicknamesSyncProcessor
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2017/1/6.
|
||||
*/
|
||||
|
||||
class DropboxSyncTaskRunner(context: Context, val authToken: String) : SyncTaskRunner(context) {
|
||||
|
||||
override fun onRunningTask(action: String, callback: (Boolean) -> Unit): Boolean {
|
||||
val requestConfig = DbxRequestConfig.newBuilder("twidere-android/${BuildConfig.VERSION_NAME}")
|
||||
.build()
|
||||
val client = DbxClientV2(requestConfig, authToken)
|
||||
val syncAction: ISyncAction = when (action) {
|
||||
TaskServiceRunner.ACTION_SYNC_DRAFTS -> DropboxDraftsSyncAction(context, client)
|
||||
TaskServiceRunner.ACTION_SYNC_FILTERS -> DropboxFiltersDataSyncAction(context, client)
|
||||
TaskServiceRunner.ACTION_SYNC_USER_COLORS -> DropboxPreferencesValuesSyncAction(context,
|
||||
client, userColorNameManager.colorPreferences, UserColorsSyncProcessor,
|
||||
"/Common/user_colors.xml")
|
||||
TaskServiceRunner.ACTION_SYNC_USER_NICKNAMES -> DropboxPreferencesValuesSyncAction(context,
|
||||
client, userColorNameManager.nicknamePreferences, UserNicknamesSyncProcessor,
|
||||
"/Common/user_nicknames.xml")
|
||||
else -> null
|
||||
} ?: return false
|
||||
task {
|
||||
syncAction.execute()
|
||||
}.successUi {
|
||||
callback(true)
|
||||
}.failUi {
|
||||
callback(false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.dropbox
|
||||
|
||||
import com.dropbox.core.v2.DbxClientV2
|
||||
import com.dropbox.core.v2.files.UploadUploader
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
|
||||
abstract internal class DropboxUploadSession<in Data>(val fileName: String, val client: DbxClientV2) : Closeable {
|
||||
private var uploader: UploadUploader? = null
|
||||
|
||||
var localModifiedTime: Long = 0
|
||||
|
||||
override fun close() {
|
||||
uploader?.close()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
abstract fun performUpload(uploader: UploadUploader, data: Data)
|
||||
|
||||
fun uploadData(data: Data): Boolean {
|
||||
uploader = client.newUploader(fileName, localModifiedTime).apply {
|
||||
performUpload(this, data)
|
||||
this.finish()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.google
|
||||
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 1/21/17.
|
||||
*/
|
||||
|
||||
internal class CloseableAny<T>(val obj: T) : Closeable {
|
||||
override fun close() {
|
||||
if (obj is Closeable) {
|
||||
obj.close()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.google
|
||||
|
||||
import java.util.*
|
||||
|
||||
internal data class DriveFileInfo(val fileId: String, val name: String, val modifiedDate: Date)
|
|
@ -1,112 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.google
|
||||
|
||||
import android.content.Context
|
||||
import com.google.api.client.util.DateTime
|
||||
import com.google.api.services.drive.Drive
|
||||
import com.google.api.services.drive.model.File
|
||||
import org.mariotaku.twidere.extension.model.filename
|
||||
import org.mariotaku.twidere.extension.model.readMimeMessageFrom
|
||||
import org.mariotaku.twidere.extension.model.writeMimeMessageTo
|
||||
import org.mariotaku.twidere.model.Draft
|
||||
import org.mariotaku.twidere.util.sync.FileBasedDraftsSyncAction
|
||||
import org.mariotaku.twidere.util.tempFileInputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
|
||||
internal class GoogleDriveDraftsSyncAction(
|
||||
context: Context,
|
||||
val drive: Drive
|
||||
) : FileBasedDraftsSyncAction<DriveFileInfo>(context) {
|
||||
|
||||
val draftsFolderName = "Drafts"
|
||||
val draftMimeType = "message/rfc822"
|
||||
|
||||
private lateinit var folderId: String
|
||||
private val files = drive.files()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun Draft.saveToRemote(): DriveFileInfo {
|
||||
tempFileInputStream(context) { os ->
|
||||
this.writeMimeMessageTo(context, os)
|
||||
}.use {
|
||||
val driveId = this.remote_extras
|
||||
val fileConfig: (File) -> Unit = {
|
||||
it.modifiedTime = DateTime(timestamp)
|
||||
}
|
||||
val file = if (driveId != null) {
|
||||
drive.files().performUpdate(driveId, filename, draftMimeType, stream = it, fileConfig = fileConfig)
|
||||
} else {
|
||||
drive.updateOrCreate(name = filename, mimeType = draftMimeType, parent = folderId,
|
||||
spaces = appDataFolderSpace, stream = it, fileConfig = fileConfig)
|
||||
}
|
||||
return DriveFileInfo(file.id, file.name, Date(file.modifiedTime.value))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun Draft.loadFromRemote(info: DriveFileInfo): Boolean {
|
||||
val get = files.get(info.fileId)
|
||||
get.executeMediaAsInputStream().use {
|
||||
val parsed = this.readMimeMessageFrom(context, it)
|
||||
if (parsed) {
|
||||
this.timestamp = info.draftTimestamp
|
||||
this.unique_id = info.draftFileName.substringBeforeLast(".eml")
|
||||
this.remote_extras = info.fileId
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun removeDrafts(list: List<DriveFileInfo>): Boolean {
|
||||
val batch = drive.batch()
|
||||
val callback = SimpleJsonBatchCallback<Void>()
|
||||
list.forEach { info ->
|
||||
files.delete(info.fileId).queue(batch, callback)
|
||||
}
|
||||
batch.execute()
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun removeDraft(info: DriveFileInfo): Boolean {
|
||||
files.delete(info.fileId).execute()
|
||||
return true
|
||||
}
|
||||
|
||||
override val DriveFileInfo.draftTimestamp: Long get() = this.modifiedDate.time
|
||||
|
||||
override val DriveFileInfo.draftFileName: String get() = this.name
|
||||
|
||||
override val DriveFileInfo.draftRemoteExtras: String? get() = this.fileId
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun listRemoteDrafts(): List<DriveFileInfo> {
|
||||
val result = ArrayList<DriveFileInfo>()
|
||||
var nextPageToken: String? = null
|
||||
do {
|
||||
val listResult = files.basicList(appDataFolderSpace).apply {
|
||||
this.q = "'$folderId' in parents and mimeType = '$draftMimeType' and trashed = false"
|
||||
if (nextPageToken != null) {
|
||||
this.pageToken = nextPageToken
|
||||
}
|
||||
}.execute()
|
||||
listResult.files.filter { file ->
|
||||
file.mimeType == draftMimeType
|
||||
}.mapTo(result) { file ->
|
||||
DriveFileInfo(file.id, file.name, Date(file.modifiedTime.value))
|
||||
}
|
||||
nextPageToken = listResult.nextPageToken
|
||||
} while (nextPageToken != null)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun setup(): Boolean {
|
||||
folderId = drive.getFileOrCreate(name = draftsFolderName, mimeType = folderMimeType,
|
||||
parent = appDataFolderName, spaces = appDataFolderSpace,
|
||||
conflictResolver = ::resolveFoldersConflict).id
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.google
|
||||
|
||||
import android.content.Context
|
||||
import com.google.api.services.drive.Drive
|
||||
import com.google.api.services.drive.model.File
|
||||
import org.mariotaku.twidere.extension.model.initFields
|
||||
import org.mariotaku.twidere.extension.model.parse
|
||||
import org.mariotaku.twidere.extension.model.serialize
|
||||
import org.mariotaku.twidere.extension.newPullParser
|
||||
import org.mariotaku.twidere.extension.newSerializer
|
||||
import org.mariotaku.twidere.model.FiltersData
|
||||
import org.mariotaku.twidere.util.sync.FileBasedFiltersDataSyncAction
|
||||
import org.mariotaku.twidere.util.tempFileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
internal class GoogleDriveFiltersDataSyncAction(
|
||||
context: Context,
|
||||
val drive: Drive
|
||||
) : FileBasedFiltersDataSyncAction<CloseableAny<File>, GoogleDriveUploadSession<FiltersData>>(context) {
|
||||
|
||||
private val fileName = "filters.xml"
|
||||
|
||||
private lateinit var commonFolderId: String
|
||||
private val files = drive.files()
|
||||
|
||||
override fun newLoadFromRemoteSession(): CloseableAny<File> {
|
||||
val file = drive.getFileOrNull(name = fileName, mimeType = xmlMimeType,
|
||||
parent = commonFolderId, spaces = appDataFolderSpace,
|
||||
conflictResolver = ::resolveFilesConflict) ?: run {
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
return CloseableAny(file)
|
||||
}
|
||||
|
||||
override fun CloseableAny<File>.getRemoteLastModified(): Long {
|
||||
return obj.modifiedTime?.value ?: throw IOException("Modified time should not be null")
|
||||
}
|
||||
|
||||
override fun CloseableAny<File>.loadFromRemote(): FiltersData {
|
||||
val data = FiltersData()
|
||||
data.parse(files.get(obj.id).executeMediaAsInputStream().newPullParser(charset = Charsets.UTF_8))
|
||||
data.initFields()
|
||||
return data
|
||||
}
|
||||
|
||||
override fun GoogleDriveUploadSession<FiltersData>.setRemoteLastModified(lastModified: Long) {
|
||||
this.localModifiedTime = lastModified
|
||||
}
|
||||
|
||||
override fun GoogleDriveUploadSession<FiltersData>.saveToRemote(data: FiltersData): Boolean {
|
||||
return this.uploadData(data)
|
||||
}
|
||||
|
||||
override fun newSaveToRemoteSession(): GoogleDriveUploadSession<FiltersData> {
|
||||
return object : GoogleDriveUploadSession<FiltersData>(fileName, commonFolderId, xmlMimeType, drive) {
|
||||
override fun FiltersData.toInputStream(): InputStream {
|
||||
return tempFileInputStream(context) {
|
||||
this.serialize(it.newSerializer(charset = Charsets.UTF_8, indent = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun setup(): Boolean {
|
||||
commonFolderId = drive.getFileOrCreate(name = commonFolderName, mimeType = folderMimeType,
|
||||
parent = appDataFolderName, spaces = appDataFolderSpace,
|
||||
conflictResolver = ::resolveFoldersConflict).id
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.google
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.google.api.services.drive.Drive
|
||||
import com.google.api.services.drive.model.File
|
||||
import org.mariotaku.twidere.extension.newPullParser
|
||||
import org.mariotaku.twidere.extension.newSerializer
|
||||
import org.mariotaku.twidere.util.sync.FileBasedPreferencesValuesSyncAction
|
||||
import org.mariotaku.twidere.util.tempFileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
internal class GoogleDrivePreferencesValuesSyncAction(
|
||||
context: Context,
|
||||
val drive: Drive,
|
||||
preferences: SharedPreferences,
|
||||
processor: Processor,
|
||||
val fileName: String
|
||||
) : FileBasedPreferencesValuesSyncAction<CloseableAny<File>,
|
||||
GoogleDriveUploadSession<Map<String, String>>>(context, preferences, processor) {
|
||||
|
||||
private lateinit var commonFolderId: String
|
||||
|
||||
private val files = drive.files()
|
||||
|
||||
override fun newLoadFromRemoteSession(): CloseableAny<File> {
|
||||
val file = drive.getFileOrNull(name = fileName, mimeType = xmlMimeType,
|
||||
parent = commonFolderId, spaces = appDataFolderSpace,
|
||||
conflictResolver = ::resolveFilesConflict) ?: run {
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
return CloseableAny(file)
|
||||
}
|
||||
|
||||
override fun CloseableAny<File>.getRemoteLastModified(): Long {
|
||||
return obj.modifiedTime?.value ?: throw IOException("Modified time should not be null")
|
||||
}
|
||||
|
||||
override fun CloseableAny<File>.loadFromRemote(): MutableMap<String, String> {
|
||||
val data = HashMap<String, String>()
|
||||
data.parse(files.get(obj.id).executeMediaAsInputStream().newPullParser())
|
||||
return data
|
||||
}
|
||||
|
||||
override fun newSaveToRemoteSession(): GoogleDriveUploadSession<Map<String, String>> {
|
||||
return object : GoogleDriveUploadSession<Map<String, String>>(fileName, commonFolderId, xmlMimeType, drive) {
|
||||
override fun Map<String, String>.toInputStream(): InputStream {
|
||||
return tempFileInputStream(context) {
|
||||
this.serialize(it.newSerializer(charset = Charsets.UTF_8, indent = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun GoogleDriveUploadSession<Map<String, String>>.saveToRemote(data: MutableMap<String, String>): Boolean {
|
||||
return this.uploadData(data)
|
||||
}
|
||||
|
||||
|
||||
override fun GoogleDriveUploadSession<Map<String, String>>.setRemoteLastModified(lastModified: Long) {
|
||||
this.localModifiedTime = lastModified
|
||||
}
|
||||
|
||||
override fun setup(): Boolean {
|
||||
commonFolderId = drive.getFileOrCreate(name = commonFolderName, mimeType = folderMimeType,
|
||||
parent = appDataFolderName, spaces = appDataFolderSpace,
|
||||
conflictResolver = ::resolveFoldersConflict).id
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,260 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.google
|
||||
|
||||
import com.google.api.client.googleapis.json.GoogleJsonError
|
||||
import com.google.api.client.googleapis.json.GoogleJsonResponseException
|
||||
import com.google.api.client.http.HttpHeaders
|
||||
import com.google.api.client.http.InputStreamContent
|
||||
import com.google.api.services.drive.Drive
|
||||
import com.google.api.services.drive.model.File
|
||||
import com.google.common.collect.HashMultimap
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 1/21/17.
|
||||
*/
|
||||
|
||||
|
||||
internal const val folderMimeType = "application/vnd.google-apps.folder"
|
||||
internal const val xmlMimeType = "application/xml"
|
||||
internal const val requiredRequestFields = "id, name, parents, mimeType, modifiedTime"
|
||||
internal const val requiredFilesRequestFields = "files($requiredRequestFields)"
|
||||
internal const val commonFolderName = "Common"
|
||||
internal const val appDataFolderName = "appDataFolder"
|
||||
internal const val rootFolderName = "root"
|
||||
internal const val appDataFolderSpace = appDataFolderName
|
||||
|
||||
internal fun Drive.getFileOrNull(
|
||||
name: String,
|
||||
mimeType: String?,
|
||||
parent: String? = rootFolderName,
|
||||
spaces: String? = null,
|
||||
trashed: Boolean = false,
|
||||
conflictResolver: ((Drive, List<File>, String?) -> File)? = null
|
||||
): File? {
|
||||
val result = findFilesOrNull(name, mimeType, parent, spaces, trashed) ?: return null
|
||||
if (result.size > 1 && conflictResolver != null) {
|
||||
return conflictResolver(this, result, spaces)
|
||||
}
|
||||
return result.firstOrNull()
|
||||
}
|
||||
|
||||
internal fun Drive.findFilesOrNull(
|
||||
name: String,
|
||||
mimeType: String?,
|
||||
parent: String? = rootFolderName,
|
||||
spaces: String? = null,
|
||||
trashed: Boolean = false
|
||||
): List<File>? {
|
||||
val find = files().basicList(spaces)
|
||||
var query = "name = '$name'"
|
||||
if (parent != null) {
|
||||
query += " and '$parent' in parents"
|
||||
}
|
||||
if (mimeType != null) {
|
||||
query += " and mimeType = '$mimeType'"
|
||||
}
|
||||
query += " and trashed = $trashed"
|
||||
find.q = query
|
||||
try {
|
||||
val files = find.execute().files
|
||||
if (files.isEmpty()) return null
|
||||
return files
|
||||
} catch (e: GoogleJsonResponseException) {
|
||||
if (e.statusCode == 404) {
|
||||
return null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Drive.getFileOrCreate(
|
||||
name: String,
|
||||
mimeType: String,
|
||||
parent: String = rootFolderName,
|
||||
spaces: String? = null,
|
||||
trashed: Boolean = false,
|
||||
conflictResolver: ((Drive, List<File>, String?) -> File)? = null
|
||||
): File {
|
||||
val result = findFilesOrCreate(name, mimeType, parent, spaces, trashed)
|
||||
if (result.size > 1 && conflictResolver != null) {
|
||||
return conflictResolver(this, result, spaces)
|
||||
}
|
||||
return result.first()
|
||||
}
|
||||
|
||||
internal fun Drive.findFilesOrCreate(
|
||||
name: String,
|
||||
mimeType: String,
|
||||
parent: String = rootFolderName,
|
||||
spaces: String? = null,
|
||||
trashed: Boolean = false
|
||||
): List<File> {
|
||||
return findFilesOrNull(name, mimeType, parent, spaces, trashed) ?: run {
|
||||
val file = File()
|
||||
file.name = name
|
||||
file.mimeType = mimeType
|
||||
file.parents = listOf(parent)
|
||||
val create = files().create(file)
|
||||
return@run listOf(create.execute())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Drive.updateOrCreate(
|
||||
name: String,
|
||||
mimeType: String,
|
||||
parent: String = rootFolderName,
|
||||
spaces: String? = null,
|
||||
trashed: Boolean = false,
|
||||
stream: InputStream,
|
||||
fileConfig: ((file: File) -> Unit)? = null
|
||||
): File {
|
||||
val files = files()
|
||||
return run {
|
||||
val find = files.basicList(spaces)
|
||||
find.q = "name = '$name' and '$parent' in parents and mimeType = '$mimeType' and trashed = $trashed"
|
||||
val fileId = try {
|
||||
find.execute().files.firstOrNull()?.id ?: return@run null
|
||||
} catch (e: GoogleJsonResponseException) {
|
||||
if (e.statusCode == 404) {
|
||||
return@run null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return@run files.performUpdate(fileId, name, mimeType, stream, fileConfig)
|
||||
} ?: run {
|
||||
val file = File()
|
||||
file.name = name
|
||||
file.mimeType = mimeType
|
||||
file.parents = listOf(parent)
|
||||
fileConfig?.invoke(file)
|
||||
val create = files.create(file, InputStreamContent(mimeType, stream))
|
||||
create.fields = requiredRequestFields
|
||||
return@run create.execute()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Drive.Files.performUpdate(
|
||||
fileId: String,
|
||||
name: String,
|
||||
mimeType: String,
|
||||
stream: InputStream,
|
||||
fileConfig: ((file: File) -> Unit)? = null
|
||||
): File {
|
||||
val file = File()
|
||||
file.name = name
|
||||
file.mimeType = mimeType
|
||||
fileConfig?.invoke(file)
|
||||
val update = update(fileId, file, InputStreamContent(mimeType, stream))
|
||||
update.fields = requiredRequestFields
|
||||
return update.execute()
|
||||
}
|
||||
|
||||
internal fun resolveFilesConflict(client: Drive, list: List<File>, spaces: String?): File {
|
||||
// Pick newest file
|
||||
val newest = list.maxBy { it.modifiedTime.value }!!
|
||||
|
||||
// Delete all others
|
||||
val batch = client.batch()
|
||||
val callback = SimpleJsonBatchCallback<Void>()
|
||||
val files = client.files()
|
||||
list.filterNot { it == newest }.forEach { files.delete(it.id).queue(batch, callback) }
|
||||
batch.execute()
|
||||
return newest
|
||||
}
|
||||
|
||||
internal fun resolveFoldersConflict(client: Drive, list: List<File>, spaces: String?): File {
|
||||
val files = client.files()
|
||||
|
||||
// Pick newest folder
|
||||
val newest = list.maxBy { it.modifiedTime.value }!!
|
||||
|
||||
// Build a map with all conflicting folders
|
||||
val query = list.joinToString(" or ") { "'${it.id}' in parents" }
|
||||
val filesList = ArrayList<File>()
|
||||
|
||||
val conflictFilesMap = HashMultimap.create<String, File>()
|
||||
var nextPageToken: String? = null
|
||||
do {
|
||||
val result = files.basicList(spaces).apply {
|
||||
this.q = query
|
||||
if (nextPageToken != null) {
|
||||
this.pageToken = nextPageToken
|
||||
}
|
||||
}.execute()
|
||||
result.files.forEach { file ->
|
||||
file.parents.forEach { parentId ->
|
||||
if (parentId == newest.id) {
|
||||
filesList.add(file)
|
||||
} else {
|
||||
conflictFilesMap.put(parentId, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
nextPageToken = result.nextPageToken
|
||||
} while (nextPageToken != null)
|
||||
|
||||
// Files in this list will be moved to newest folder
|
||||
val insertList = ArrayList<File>()
|
||||
// Files in this list will be removed
|
||||
val removeList = ArrayList<File>()
|
||||
|
||||
for ((k, l) in conflictFilesMap.asMap()) {
|
||||
for (v in l) {
|
||||
val find = filesList.find { it.name == v.name }
|
||||
if (find == null) {
|
||||
insertList.add(v)
|
||||
} else if (find.modifiedTime.value > v.modifiedTime.value) {
|
||||
// Our file is newer, remove `v`
|
||||
removeList.add(v)
|
||||
} else {
|
||||
// `v` is newer, update ours
|
||||
insertList.add(v)
|
||||
removeList.add(find)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.filterNotTo(removeList) { it == newest }
|
||||
|
||||
if (insertList.isNotEmpty()) {
|
||||
val callback = object : SimpleJsonBatchCallback<File>() {
|
||||
override fun onFailure(error: GoogleJsonError, headers: HttpHeaders) {
|
||||
throw IOException(error.message)
|
||||
}
|
||||
}
|
||||
client.batch().apply {
|
||||
insertList.forEach { file ->
|
||||
files.update(file.id, File()).apply {
|
||||
this.addParents = newest.id
|
||||
this.removeParents = file.parents?.joinToString(",")
|
||||
}.queue(this, callback)
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
if (removeList.isNotEmpty()) {
|
||||
val callback = SimpleJsonBatchCallback<Void>()
|
||||
client.batch().apply {
|
||||
removeList.forEach { file ->
|
||||
files.delete(file.id).queue(this, callback)
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
return newest
|
||||
|
||||
}
|
||||
|
||||
internal fun Drive.Files.basicList(spaces: String? = null): Drive.Files.List {
|
||||
return list().apply {
|
||||
this.fields = requiredFilesRequestFields
|
||||
if (spaces != null) {
|
||||
this.spaces = spaces
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.google
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
|
||||
import com.google.api.client.http.javanet.NetHttpTransport
|
||||
import com.google.api.client.json.jackson2.JacksonFactory
|
||||
import com.google.api.services.drive.Drive
|
||||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.mariotaku.twidere.BuildConfig
|
||||
import org.mariotaku.twidere.model.sync.GoogleDriveSyncProviderInfo
|
||||
import org.mariotaku.twidere.util.DebugLog
|
||||
import org.mariotaku.twidere.util.TaskServiceRunner
|
||||
import org.mariotaku.twidere.util.sync.*
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2017/1/6.
|
||||
*/
|
||||
|
||||
class GoogleDriveSyncTaskRunner(context: Context, val refreshToken: String) : SyncTaskRunner(context) {
|
||||
override fun onRunningTask(action: String, callback: (Boolean) -> Unit): Boolean {
|
||||
val httpTransport = NetHttpTransport.Builder().build()
|
||||
val jsonFactory = JacksonFactory.getDefaultInstance()
|
||||
val credential = GoogleCredential.Builder()
|
||||
.setTransport(httpTransport)
|
||||
.setJsonFactory(jsonFactory)
|
||||
.setClientSecrets(GoogleDriveSyncProviderInfo.WEB_CLIENT_ID, GoogleDriveSyncProviderInfo.WEB_CLIENT_SECRET)
|
||||
.build()
|
||||
credential.refreshToken = refreshToken
|
||||
val drive = Drive.Builder(httpTransport, JacksonFactory.getDefaultInstance(), credential).build()
|
||||
val syncAction: ISyncAction = when (action) {
|
||||
TaskServiceRunner.ACTION_SYNC_DRAFTS -> GoogleDriveDraftsSyncAction(context, drive)
|
||||
TaskServiceRunner.ACTION_SYNC_FILTERS -> GoogleDriveFiltersDataSyncAction(context, drive)
|
||||
TaskServiceRunner.ACTION_SYNC_USER_COLORS -> GoogleDrivePreferencesValuesSyncAction(context,
|
||||
drive, userColorNameManager.colorPreferences, UserColorsSyncProcessor,
|
||||
"user_colors.xml")
|
||||
TaskServiceRunner.ACTION_SYNC_USER_NICKNAMES -> GoogleDrivePreferencesValuesSyncAction(context,
|
||||
drive, userColorNameManager.nicknamePreferences, UserNicknamesSyncProcessor,
|
||||
"user_nicknames.xml")
|
||||
else -> null
|
||||
} ?: return false
|
||||
task {
|
||||
syncAction.execute()
|
||||
}.successUi {
|
||||
callback(true)
|
||||
}.failUi {
|
||||
DebugLog.w(LOGTAG_SYNC, "Sync $action failed", it)
|
||||
callback(false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.google
|
||||
|
||||
import com.dropbox.core.v2.files.UploadUploader
|
||||
import com.google.api.client.util.DateTime
|
||||
import com.google.api.services.drive.Drive
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
abstract internal class GoogleDriveUploadSession<in Data>(
|
||||
val name: String,
|
||||
val parentId: String,
|
||||
val mimeType: String,
|
||||
val drive: Drive
|
||||
) : Closeable {
|
||||
private var uploader: UploadUploader? = null
|
||||
|
||||
var localModifiedTime: Long = 0
|
||||
|
||||
override fun close() {
|
||||
uploader?.close()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
abstract fun Data.toInputStream(): InputStream
|
||||
|
||||
fun uploadData(data: Data): Boolean {
|
||||
data.toInputStream().use {
|
||||
drive.updateOrCreate(name = name, mimeType = mimeType, parent = parentId,
|
||||
spaces = appDataFolderSpace, stream = it, fileConfig = {
|
||||
it.modifiedTime = DateTime(localModifiedTime)
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package org.mariotaku.twidere.util.sync.google
|
||||
|
||||
import com.google.api.client.googleapis.batch.json.JsonBatchCallback
|
||||
import com.google.api.client.googleapis.json.GoogleJsonError
|
||||
import com.google.api.client.googleapis.json.GoogleJsonResponseException
|
||||
import com.google.api.client.http.HttpHeaders
|
||||
import com.google.api.client.http.HttpResponseException
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 1/22/17.
|
||||
*/
|
||||
|
||||
internal open class SimpleJsonBatchCallback<T> : JsonBatchCallback<T>() {
|
||||
@Throws(IOException::class)
|
||||
override fun onFailure(error: GoogleJsonError, headers: HttpHeaders) {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun onSuccess(result: T, headers: HttpHeaders) {
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package org.mariotaku.twidere.view.controller.premium
|
||||
|
||||
import android.view.View
|
||||
import org.mariotaku.twidere.R
|
||||
import org.mariotaku.twidere.TwidereConstants.REQUEST_PURCHASE_EXTRA_FEATURES
|
||||
import org.mariotaku.twidere.activity.PremiumDashboardActivity
|
||||
import org.mariotaku.twidere.fragment.ExtraFeaturesIntroductionDialogFragment
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2017/2/4.
|
||||
*/
|
||||
|
||||
abstract class AbsGoogleInAppItemViewController : PremiumDashboardActivity.ExtraFeatureViewController() {
|
||||
abstract val title: String
|
||||
abstract val summary: String
|
||||
abstract val feature: String
|
||||
abstract val availableLabel: String
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
titleView.text = title
|
||||
messageView.text = summary
|
||||
|
||||
button1.setText(R.string.action_purchase)
|
||||
button2.text = availableLabel
|
||||
|
||||
button1.setOnClickListener {
|
||||
ExtraFeaturesIntroductionDialogFragment.show(activity.supportFragmentManager,
|
||||
feature = this.feature, requestCode = REQUEST_PURCHASE_EXTRA_FEATURES)
|
||||
}
|
||||
button2.setOnClickListener {
|
||||
onAvailableButtonClick()
|
||||
}
|
||||
|
||||
updateEnabledState()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateEnabledState()
|
||||
}
|
||||
|
||||
abstract fun onAvailableButtonClick()
|
||||
|
||||
private fun updateEnabledState() {
|
||||
if (extraFeaturesService.isEnabled(feature)) {
|
||||
button1.visibility = View.GONE
|
||||
button2.visibility = View.VISIBLE
|
||||
} else {
|
||||
button1.visibility = View.VISIBLE
|
||||
button2.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package org.mariotaku.twidere.view.controller.premium
|
||||
|
||||
import android.widget.Toast
|
||||
import org.mariotaku.twidere.R
|
||||
import org.mariotaku.twidere.util.IntentUtils
|
||||
import org.mariotaku.twidere.util.premium.ExtraFeaturesService
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2017/2/4.
|
||||
*/
|
||||
|
||||
class GoogleFiltersImportViewController : AbsGoogleInAppItemViewController() {
|
||||
override val feature: String
|
||||
get() = ExtraFeaturesService.FEATURE_FILTERS_IMPORT
|
||||
override val summary: String
|
||||
get() = context.getString(R.string.extra_feature_description_filters_import)
|
||||
override val title: String
|
||||
get() = context.getString(R.string.extra_feature_title_filters_import)
|
||||
override val availableLabel: String
|
||||
get() = context.getString(R.string.action_import)
|
||||
|
||||
override fun onAvailableButtonClick() {
|
||||
IntentUtils.openFilters(context, "users")
|
||||
Toast.makeText(context, R.string.message_toast_filters_import_hint, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package org.mariotaku.twidere.view.controller.premium
|
||||
|
||||
import org.mariotaku.twidere.R
|
||||
import org.mariotaku.twidere.util.IntentUtils
|
||||
import org.mariotaku.twidere.util.premium.ExtraFeaturesService
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 2017/2/4.
|
||||
*/
|
||||
|
||||
class GoogleFiltersSubscriptionsViewController : AbsGoogleInAppItemViewController() {
|
||||
override val feature: String
|
||||
get() = ExtraFeaturesService.FEATURE_FILTERS_SUBSCRIPTION
|
||||
override val summary: String
|
||||
get() = context.getString(R.string.extra_feature_description_filters_subscription)
|
||||
override val title: String
|
||||
get() = context.getString(R.string.extra_feature_title_filters_subscription)
|
||||
override val availableLabel: String
|
||||
get() = context.getString(R.string.action_filter_subscriptions_card_manage)
|
||||
|
||||
override fun onAvailableButtonClick() {
|
||||
IntentUtils.openFilters(context, "settings")
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Twidere - Twitter client for Android
|
||||
~
|
||||
~ Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/map_fragment"
|
||||
android:layout_height="match_parent" />
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Twidere - Twitter client for Android
|
||||
~
|
||||
~ Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@id/center"
|
||||
android:icon="@drawable/ic_action_my_location"
|
||||
app:showAsAction="always"
|
||||
android:title="@string/action_center"/>
|
||||
|
||||
</menu>
|
|
@ -1 +0,0 @@
|
|||
org.mariotaku.twidere.util.FabricAnalyzer
|
|
@ -1 +0,0 @@
|
|||
org.mariotaku.twidere.util.GoogleMapFragmentFactory
|
|
@ -1 +0,0 @@
|
|||
org.mariotaku.twidere.util.premium.GooglePlayExtraFeaturesService
|
|
@ -1,2 +0,0 @@
|
|||
org.mariotaku.twidere.util.sync.NonFreeSyncProviderInfoFactory
|
||||
org.mariotaku.twidere.util.sync.OpenSourceSyncProviderInfoFactory
|
Loading…
Reference in New Issue